多层感知机-动手学深度学习

动手学深度学习v2

课程链接:https://courses.d2l.ai/zh-v2/

多层感知机

感知机

线性回归:输出实数

Softmax:输出各分类概率

感知机:二分类

image-20250105174720241

等价于使用批量大小为1的梯度下降

损失函数:

不能拟合XOR函数,只能产生线性分割面

多层感知机

XOR学习

两个分类器进行组合

模型

单隐藏层

$ \sigma $的加入使得多层感知机不会退化为线性模型

多类分类

通过softmax处理

多隐藏层

超参数:

  • 隐藏层数
  • 每层隐藏层大小

激活函数

代码实现

依旧采用Fashion-MNIST图像分类数据集

手动实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 实现单隐藏层的多层感知机,隐藏单元为256个
num_inputs, num_outputs, num_hiddens = 784, 10, 256
# W1和W2乘0.01使得方差为0.01
W1 = nn.Parameter(torch.randn(
    num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
    num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]

# 实现ReLU
def relu(X):
    a = torch.zeros_like(X)
    return torch.max(X, a)

# 模型
def net(X):
    X = X.reshape((-1, num_inputs))
    H = relu(X@W1 + b1)  # 这里“@”代表矩阵乘法
    return (H@W2 + b2)

# 损失
loss = nn.CrossEntropyLoss(reduction='none')

# 训练
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)

简洁实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 模型
net = nn.Sequential(nn.Flatten(),
                    nn.Linear(784, 256),
                    nn.ReLU(),
                    nn.Linear(256, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)

train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

模型选择、欠拟合和过拟合

模型选择

误差

  • 训练误差:模型在训练数据上的误差
  • 泛化误差:新数据上的误差

我们更关心泛化误差

验证数据集和测试数据集

  • 验证数据集:评估模型好坏(不跟训练数据混在一起)
  • 测试数据集:只用一次、不可用来调参

K-则交叉验证

没有足够多数据时使用

算法:

  1. 数据分成K块(K常取5或10)
  2. for i = 1, …, K,使用第 i 块作为验证数据集,其余用来训练
  3. 取K个验证集误差的平均

过拟合和欠拟合

数据简单 数据复杂
模型容量低 正常 欠拟合
模型容量高 过拟合 正常

模型容量

  • 模型容量:拟合各种函数的能力
    • 低容量:难以拟合训练数据
    • 高容量:会记住所有训练数据

  • 估计模型容量:
    • 不同种类算法间难以比较
    • 主要因素:
      • 参数个数
      • 参数值选择范围

VC维(了解)

深度学习中衡量不准确

权重衰退 weight-decay

使用均方范数作为硬性限制

  • 限制偏移b没有明显效果
  • 较小的$ \theta $意味着更强的正则项

正则项(Regularization term)是机器学习和统计模型中用于防止模型过拟合(overfitting)的一种技术。它通过在损失函数(loss function)中加入一个额外的惩罚项,来约束模型的复杂度,迫使模型在训练时选择较为简单的解,以避免在训练集上表现得过好但在测试集上泛化能力差的问题。

正则项仅在训练过程中使用

使用均方范数作为柔性限制

对任意$ \theta $存在$ \lambda $使得之前的目标函数等于:

超参数$ \lambda $代表了正则项的重要程度

  • $ \lambda = 0 $:无作用

惩罚项的加入使得最优解向原点偏移

  • 梯度:
  • w随时间t更新:

通常衰退$ \eta\lambda < 1 $,因此称作权重衰退

代码实现

从零实现

根据公式生成数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
# 生成数据
n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)

# 初始化参数
def init_params():
    w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
    b = torch.zeros(1, requires_grad=True)
    return [w, b]

# 定义L2范数惩罚
def l2_penalty(w):
    return torch.sum(w.pow(2)) / 2

# 训练
def train(lambd):
    w, b = init_params()
    net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
    num_epochs, lr = 100, 0.003
    animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
                            xlim=[5, num_epochs], legend=['train', 'test'])
    for epoch in range(num_epochs):
        for X, y in train_iter:
            # 增加了L2范数惩罚项,
            # 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
            l = loss(net(X), y) + lambd * l2_penalty(w)
            l.sum().backward()
            d2l.sgd([w, b], lr, batch_size)
        if (epoch + 1) % 5 == 0:
            animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
                                     d2l.evaluate_loss(net, test_iter, loss)))
    print('w的L2范数是:', torch.norm(w).item())

# 分别取lambd = 0, 3
train(lambd=0)
train(lambd=3)

运行结果:

$ \lambda $ 0 3
w
loss

简洁实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def train_concise(wd):
    net = nn.Sequential(nn.Linear(num_inputs, 1))
    for param in net.parameters():
        param.data.normal_()
    loss = nn.MSELoss(reduction='none')
    num_epochs, lr = 100, 0.003
    # 偏置参数没有衰减
    trainer = torch.optim.SGD([
        {"params":net[0].weight,'weight_decay': wd}, #此处设置wd
        {"params":net[0].bias}], lr=lr)
    animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
                            xlim=[5, num_epochs], legend=['train', 'test'])
    for epoch in range(num_epochs):
        for X, y in train_iter:
            trainer.zero_grad()
            l = loss(net(X), y)
            l.mean().backward()
            trainer.step()
        if (epoch + 1) % 5 == 0:
            animator.add(epoch + 1,
                         (d2l.evaluate_loss(net, train_iter, loss),
                          d2l.evaluate_loss(net, test_iter, loss)))
    print('w的L2范数:', net[0].weight.norm().item())

丢弃法 dropout

原理

好的模型需要对输入数据的扰动鲁棒,考虑在层之间加入噪音

无偏差加入噪音:

  • 加入噪音前后的期望不变:E[x’] = x

$ x = 0 * p + (1 - p) * x / (1 - p) $

  • 丢弃法通常作用在隐藏全连接层的输出上,将一些输出项随机变成0

训练:

推理:

推理过程中不使用正则项,丢弃法直接返回输出

h = dropout(h)

代码实现

从零实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import torch
from torch import nn
from d2l import torch as d2l

# 实现以dropout的概率丢弃X中的元素
def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1
    # 在本情况中,所有元素都被丢弃
    if dropout == 1:
        return torch.zeros_like(X)
    # 在本情况中,所有元素都被保留
    if dropout == 0:
        return X
    mask = (torch.rand(X.shape) > dropout).float()
    return mask * X / (1.0 - dropout) # 直接做乘法运算比频繁读取的运行速度快

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 两个隐藏层的多层感知机,每层256个单元
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

# 定义模型
dropout1, dropout2 = 0.2, 0.5

class Net(nn.Module):
    def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
                 is_training = True):
        super(Net, self).__init__()
        self.num_inputs = num_inputs
        self.training = is_training
        self.lin1 = nn.Linear(num_inputs, num_hiddens1)
        self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
        self.lin3 = nn.Linear(num_hiddens2, num_outputs)
        self.relu = nn.ReLU()

    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
        # 只有在训练模型时才使用dropout
        if self.training == True:
            # 在第一个全连接层之后添加一个dropout层
            H1 = dropout_layer(H1, dropout1)
        H2 = self.relu(self.lin2(H1))
        if self.training == True:
            # 在第二个全连接层之后添加一个dropout层
            H2 = dropout_layer(H2, dropout2)
        out = self.lin3(H2)
        return out


net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)

训练结果:

简洁实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
net = nn.Sequential(nn.Flatten(),
        nn.Linear(784, 256),
        nn.ReLU(),
        # 在第一个全连接层之后添加一个dropout层
        nn.Dropout(dropout1),
        nn.Linear(256, 256),
        nn.ReLU(),
        # 在第二个全连接层之后添加一个dropout层
        nn.Dropout(dropout2),
        nn.Linear(256, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

数值稳定性

考虑一个d层的神经网络

则$ l $关于$ \textbf{W}_t $的梯度为:

梯度爆炸

例:MLP

使用ReLU作为激活函数

则梯度是Wi中值为1的项做乘法。当d-t较大时,梯度将会非常大

带来问题:

  1. 值超出值域(对16位浮点数来说尤为严重:6e-5 - 6e4)
  2. 对学习率敏感
    1. 过大:梯度更大
    2. 过小:训练无进展

梯度消失

sigmoid作为激活函数

则梯度为d-t个小数值的乘积

带来问题:

  1. 梯度值变成0
  2. 无论如何选择学习率,训练无进展
  3. 限制了神经网络的深度:对较深的神经网络,反向梯度计算使得底层训练效果不好

数值稳定

  • 如何让梯度值范围合理?
    • 乘法变加法:ResNet、LSTM
    • 梯度归一化、梯度剪裁
    • 合理的权重初始化和激活函数

我们希望每层的输出和梯度的均值和方差保持一致

在参数初始化时,最优解附近的表面更加平缓,较远处更容易数值不稳定。因此需要合理的初始化参数

对于正向情况,h与w独立,若使得期望为0,方差为$ \gamma t $,则有$ n{t-1}\gamma_t = 1 $

同样,对于反向情况,有$ n_{t}\gamma_t = 1 $

推导:模型初始化和激活函数_哔哩哔哩_bilibili

Xavier初始

$ (n_{t-1}\gamma_t + n_{t}\gamma_t)/2 = 1 $

则$ \gamma_t = 2/(n_{t-1}+n_t) $

则参数初始化需满足:

激活函数

假设激活函数为线性

反向的到相同结果,即 f(x) = x

将现有激活函数调整为满足零点附近近似 f(x) = x