4. 多层感知机
4.1 多层感知机
4.1.1 隐藏层
线性意味着任何特征的变化都是单调的,可以在在网络中加入多个隐藏层来处理更普遍的函数关系。最简单的方法是将许多全连接层堆叠在一起。这种架构称为多层感知机(multilayer perceptron, MLP)。表示
个样本的小批量,
表示具有
个隐藏单元的单隐藏层的输出。
%7D%E2%88%88R%5E%7Bd%C3%97h%7D#card=math&code=%5Cmathbf%7BW%7D%5E%7B%281%29%7D%E2%88%88R%5E%7Bd%C3%97h%7D&id=xTsRY) 和
%7D%E2%88%88R%5E%7Bh%C3%97q%7D#card=math&code=%5Cmathbf%7BW%7D%5E%7B%282%29%7D%E2%88%88R%5E%7Bh%C3%97q%7D&id=r2r5B)为隐藏层权重,
%7D%E2%88%88R%5E%7B1%C3%97h%7D#card=math&code=%5Cmathbf%7Bb%7D%5E%7B%281%29%7D%E2%88%88R%5E%7B1%C3%97h%7D&id=ETr2D)和
%7D%E2%88%88R%5E%7B1%C3%97q%7D#card=math&code=%5Cmathbf%7Bb%7D%5E%7B%282%29%7D%E2%88%88R%5E%7B1%C3%97q%7D&id=vof2s)为隐藏层偏置,
为输出:
%7D%2B%5Cmathbf%7Bb%7D%5E%7B(1)%7D%5C%5C%0A%5Cmathbf%7BO%7D%3D%5Cmathbf%7BHW%7D%5E%7B(2)%7D%2B%5Cmathbf%7Bb%7D%5E%7B(2)%7D%5C%5C%0A#card=math&code=%5Cmathbf%7BH%7D%3D%5Cmathbf%7BXW%7D%5E%7B%281%29%7D%2B%5Cmathbf%7Bb%7D%5E%7B%281%29%7D%5C%5C%0A%5Cmathbf%7BO%7D%3D%5Cmathbf%7BHW%7D%5E%7B%282%29%7D%2B%5Cmathbf%7Bb%7D%5E%7B%282%29%7D%5C%5C%0A&id=eK8UY)
上面隐藏单元的输入H由仿射函数给出, 而输出O是隐藏单元的仿射函数。 仿射函数的仿射函数本身就是仿射函数,没有隐藏层的线性模型已经能够表示任何仿射函数。因此在仿射变换之后对每个隐藏单元应用非线性的激活函数(activation function)σ,从而防止将多层感知机退化成线性模型:%7D%2B%5Cmathbf%7Bb%7D%5E%7B(1)%7D)%5C%5C%0A%5Cmathbf%7BO%7D%3D%5Cmathbf%7BHW%7D%5E%7B(2)%7D%2B%5Cmathbf%7Bb%7D%5E%7B(2)%7D%5C%5C%0A#card=math&code=%5Cmathbf%7BH%7D%3D%CF%83%28%5Cmathbf%7BXW%7D%5E%7B%281%29%7D%2B%5Cmathbf%7Bb%7D%5E%7B%281%29%7D%29%5C%5C%0A%5Cmathbf%7BO%7D%3D%5Cmathbf%7BHW%7D%5E%7B%282%29%7D%2B%5Cmathbf%7Bb%7D%5E%7B%282%29%7D%5C%5C%0A&id=HlAGf)
单隐藏层给定足够的神经元和正确的权重可以对任意函数建模,但通过使用更深(而不是更广)的网络可以更容易地逼近许多函数。
4.1.2 激活函数
4.1.2.1 ReLU函数
修正线性单元(Rectified linear unit,ReLU)是最受欢迎的激活函数。%3Dmax(x%2C0)%0A#card=math&code=ReLU%28x%29%3Dmax%28x%2C0%29%0A&id=l7gJi)
当输入为负时,导数为0;而当输入为正时,导数为1。当输入值等于0时,函数不可导,使用0代替。
ReLU要么让参数消失,要么让参数通过。 这使得优化表现得更好,并且减轻了梯度消失问题。ReLU有许多变体,包括参数化ReLU(Parameterized ReLU,pReLU) [He et al., 2015]。 该变体添加了一个线性项,这使得负参数仍然可以通过一些信息:%3Dmax(0%2Cx)%2B%CE%B1min(0%2Cx)%0A#card=math&code=pReLU%28x%29%3Dmax%280%2Cx%29%2B%CE%B1min%280%2Cx%29%0A&id=F77Yt)
4.1.2.2 sigmoid函数
sigmoid将输入变换为区间(0, 1)上的输出。该函数在隐藏层中已经较少使用。%3D%5Cfrac%7B1%7D%7B1%2Bexp(-x)%7D%0A#card=math&code=sigmoid%28x%29%3D%5Cfrac%7B1%7D%7B1%2Bexp%28-x%29%7D%0A&id=z58Yw)
%3D%5Cfrac%7Bexp(-x)%7D%7B(1%2Bexp(-x))%5E%7B2%7D%7D%3Dsigmoid(x)(1-sigmoid(x))%0A#card=math&code=%5Cfrac%7Bd%7D%7Bdx%7Dsigmoid%28x%29%3D%5Cfrac%7Bexp%28-x%29%7D%7B%281%2Bexp%28-x%29%29%5E%7B2%7D%7D%3Dsigmoid%28x%29%281-sigmoid%28x%29%29%0A&id=RQVOW)
4.1.2.3 tanh函数
tanh(双曲正切)函数将输入转换到区间(-1, 1)上。tanh函数关于坐标系原点中心对称。%3D%5Cfrac%7B1-exp(-2x)%7D%7B1%2Bexp(-2x)%7D%0A#card=math&code=tanh%28x%29%3D%5Cfrac%7B1-exp%28-2x%29%7D%7B1%2Bexp%28-2x%29%7D%0A&id=NVlDB)
%3D1-tanh%5E%7B2%7D(x)%0A#card=math&code=%5Cfrac%7Bd%7D%7Bdx%7Dtanh%28x%29%3D1-tanh%5E%7B2%7D%28x%29%0A&id=zAPBw)
4.2 多层感知机的实现
4.2.1 从零实现
import torch
from torch import nn
from d2l import torch as d2l
# 使用Fashion-MNIST图像分类数据集
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
由于内存在硬件中的分配和寻址方式,通常选择2的若干次幂作为层的宽度以使计算更高效。
num_inputs, num_outputs, num_hiddens = 784, 10, 256 # 具有256个隐藏单元的隐藏层
# 定义权重与偏置项并随机分配参数
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]
# 定义激活函数
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)
4.2.2 简洁实现
import torch
from torch import nn
import d2l
# 模型
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256), # 包含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)
4.3 模型选择、欠拟合和过拟合
4.3.1 训练误差和泛化误差
训练误差指模型在训练集上计算得到的误差。泛化误差指模型应用在从原始样本的分布中抽取无限多数据样本时的误差。我们通过随机选取未曾在训练集中出现的数据样本构成测试集来估计泛化误差。我们期望泛化误差与训练误差相近。
独立同分布假设意味着任意两个采样样本几乎是无关的。因此,训练样本与测试样本必须基于同样的独立同分布。例如,只用大学生人脸数据训练人脸识别系统来监测老人不太可能有效,因为有很大的不同。
统计学家认为,能够轻松解释任意事实的模型是复杂的, 而表达能力有限但仍能很好地解释数据的模型更有现实用途。
4.3.2 K折交叉验证
训练数据稀缺可能无法提供足够的数据来构成一个合适的验证集。 一个流行的解决方案是采用K折交叉验证。原始训练数据被分成K个不重叠的子集,然后执行K次模型训练和验证,每次在K−1个子集上进行训练, 并在剩余的一个子集上验证。 最后,通过对K次实验的结果取平均来估计训练和验证误差。
4.3.3 欠拟合与过拟合
如果模型不能降低训练误差可能意味模型过于简单, 被称为欠拟合(underfitting)。当训练误差明显低于验证误差时表明严重的过拟合(overfitting)。过拟合并不总是坏事,最好的预测模型在训练数据上的表现往往比在验证数据上好得多。但我们通常更关心验证误差,而不是训练误差和验证误差之间的差距。
- 当可调整参数的数量(自由度)很大时,模型往往更容易过拟合。
- 当权重的取值范围较大时,模型可能更容易过拟合。
- 当训练样本的数量过少时,模型更容易过拟合。
- 当模型的复杂度不足时,模型更容易欠拟合。
更多的数据不会有什么坏处, 如果没有足够的数据,简单的模型可能更有用。对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。
4.4 权重衰减
收集更多的训练数据来缓解过拟合成本很高,假设已经拥有很多的高质量数据,可以将重点放在正则化(regularization)上。
4.4.1 范数与权重衰减
权重衰减(weight decay)也称为L2正则化,通过函数与零的距离来衡量函数的复杂度。要保证权重向量比较小,最常用方法是将其范数作为惩罚项加到最小化损失的问题中。现在的训练目标为最小化预测损失和惩罚项之和。如果权重向量增长的太大, 学习算法可能会更集中于最小化权重范数‖w‖2。3.1节中线性回归的损失:%3D%5Cfrac%7B1%7D%7Bn%7D%20%5Csum%7Bi%3D1%7D%5E%7Bn%7D%20%5Cfrac%7B1%7D%7B2%7D%5Cleft(%5Cmathbf%7Bw%7D%5E%7B%5Ctop%7D%20%5Cmathbf%7Bx%7D%5E%7B(i)%7D%2Bb-y%5E%7B(i)%7D%5Cright)%5E%7B2%7D%0A#card=math&code=L%28%5Cmathbf%7Bw%7D%2C%20b%29%3D%5Cfrac%7B1%7D%7Bn%7D%20%5Csum%7Bi%3D1%7D%5E%7Bn%7D%20%5Cfrac%7B1%7D%7B2%7D%5Cleft%28%5Cmathbf%7Bw%7D%5E%7B%5Ctop%7D%20%5Cmathbf%7Bx%7D%5E%7B%28i%29%7D%2Bb-y%5E%7B%28i%29%7D%5Cright%29%5E%7B2%7D%0A&id=rYZET)
在损失函数中添加以惩罚权重向量的大小,并通过非负超参数λ来平衡惩罚的损失:
%2B%5Cfrac%7B%CE%BB%7D%7B2%7D%E2%88%A5w%E2%88%A5%5E%7B2%7D%0A#card=math&code=L%28w%2Cb%29%2B%5Cfrac%7B%CE%BB%7D%7B2%7D%E2%88%A5w%E2%88%A5%5E%7B2%7D%0A&id=jrrrk)
使用平方范数(即欧几里得距离)而不是标准范数是为了便于计算。首先使用L2范数的一个原因是它对权重向量的大分量施加了巨大的惩罚,这使得学习算法偏向于在大量特征上均匀分布权重,对单个变量中的观测误差更为稳定。相比之下,L1惩罚会导致模型将权重集中在一小部分特征上,而将其他权重清除为零。 这称为特征选择(feature selection),可能是其他场景下需要的。L2正则化回归的小批量随机梯度下降更新如下式:%20%5Cmathbf%7Bw%7D-%5Cfrac%7B%5Ceta%7D%7B%7C%5Cmathcal%7BB%7D%7C%7D%20%5Csum%7Bi%20%5Cin%20%5Cmathcal%7BB%7D%7D%20%5Cmathbf%7Bx%7D%5E%7B(i)%7D%5Cleft(%5Cmathbf%7Bw%7D%5E%7B%5Ctop%7D%20%5Cmathbf%7Bx%7D%5E%7B(i)%7D%2Bb-y%5E%7B(i)%7D%5Cright)%0A#card=math&code=%5Cmathbf%7Bw%7D%20%5Cleftarrow%281-%5Ceta%20%5Clambda%29%20%5Cmathbf%7Bw%7D-%5Cfrac%7B%5Ceta%7D%7B%7C%5Cmathcal%7BB%7D%7C%7D%20%5Csum%7Bi%20%5Cin%20%5Cmathcal%7BB%7D%7D%20%5Cmathbf%7Bx%7D%5E%7B%28i%29%7D%5Cleft%28%5Cmathbf%7Bw%7D%5E%7B%5Ctop%7D%20%5Cmathbf%7Bx%7D%5E%7B%28i%29%7D%2Bb-y%5E%7B%28i%29%7D%5Cright%29%0A&id=kg9Vv)
该公式根据估计值与观测值之间的差异来更新w,同时也在试图将w的大小缩小到零。是否对相应的偏置进行惩罚在不同的实践中会有所不同,在神经网络的不同层中也会有所不同。通常,网络输出层的偏置项不会被正则化。
4.4.2 示例
使用下列公式生成数据,为了使过拟合的效果更加明显,将维数增加到d=200, 并仅生成20个训练样本:%0A#card=math&code=y%3D0.05%2B%5Csum%7Bi%3D1%7D%5E%7Bd%7D0.01x%7Bi%7D%2B%CF%B5%20%20%5C%5C%CF%B5%20%E2%88%BCN%280%2C0.01%5E%7B2%7D%29%0A&id=xbFMm)
import torch
from torch import nn
import 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)
4.4.3 简洁实现
下面的代码在实例化优化器时通过weight_decay
指定weight decay超参数。 默认情况下,PyTorch同时衰减权重和偏移。 这里我们只为权重设置了weight_decay
,所以偏置参数b不会衰减。
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},
{"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())
train_concise(0)
train_concise(3)
4.5 暂退法(Dropout)
4.5.1 暂退法
在整个训练过程的每一次迭代中,标准暂退法在计算下一层之前将当前层中的一些节点置零。神经网络过拟合与每一层都依赖于前一层激活值相关,称这种情况为“共适应性”。作者认为,暂退法会破坏共适应性。在标准暂退法正则化中,每个中间活性值h以随机变量h′替换,如下所示:
这是一种无偏向(unbiased)注入噪声的方式。在固定住其他层时,每一层的期望值等于没有噪音时的值,即E[h′]=h。下图中删除了和
, 因此输出的计算不再依赖于
和
,并且它们各自的梯度在执行反向传播时也会消失。 这样,输出层的计算不能过度依赖于
的任何一个元素。
通常在测试时不用暂退法。也有一些例外:一些研究人员在测试时使用暂退法, 用于估计神经网络预测的“不确定性”: 如果通过许多不同的暂退法遮盖后得到的预测结果都是一致的,那么网络发挥更稳定。
在底层实现时,暂退法应用于每个隐藏层的输出(激活函数之后),并且只在训练期间有效。通常在靠近输入层的地方设置较低的暂退概率。例如,包含两个隐藏层的暂退概率可分别设置为0.2和0.5。
4.5.2 从零实现
def dropout_layer(X, dropout):
"""以dropout的概率丢弃张量输入X中的元素,将剩余部分除以1.0-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() # float函数将True、False转化为1、0
return mask * X / (1.0 - dropout)
4.5.3 简洁实现
PyTorch只需在每个全连接层之后添加一个Dropout
层,将暂退概率作为唯一的参数传递给它的构造函数。 Dropout
层仅在训练时生效。
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))
4.7 前向传播、反向传播和计算图
4.7.1 前向传播
前向传播指从输入层到输出层计算和存储神经网络中每层的结果。下面的例子假设不包含偏置项:%7D%5Cmathbf%7Bx%7D%5C%5C%0A%5Cmathbf%7Bh%7D%3D%CF%95(%5Cmathbf%7Bz%7D)%5C%5C%0A%5Cmathbf%7Bo%3DW%5E%7B(2)%7Dh%7D%5C%5C%0A#card=math&code=%5Cmathbf%7Bz%7D%3D%5Cmathbf%7BW%7D%5E%7B%281%29%7D%5Cmathbf%7Bx%7D%5C%5C%0A%5Cmathbf%7Bh%7D%3D%CF%95%28%5Cmathbf%7Bz%7D%29%5C%5C%0A%5Cmathbf%7Bo%3DW%5E%7B%282%29%7Dh%7D%5C%5C%0A&id=cXQRt)
其中,输入样本是 ,
%7D%E2%88%88R%5E%7Bh%20%5Ctimes%20d%7D#card=math&code=%5Cmathbf%7BW%7D%5E%7B%281%29%7D%E2%88%88R%5E%7Bh%20%5Ctimes%20d%7D&id=puml5)、
%7D%E2%88%88R%5E%7Bq%5Ctimes%20h%7D#card=math&code=%5Cmathbf%7BW%7D%5E%7B%282%29%7D%E2%88%88R%5E%7Bq%5Ctimes%20h%7D&id=nt2bX)是隐藏层的权重参数。
假设损失函数为,样本标签为
,我们可以计算单个数据样本的损失项:
%0A#card=math&code=L%3Dl%28%5Cmathbf%7Bo%7D%2Cy%29%0A&id=z4klw)
根据L2正则化的定义,给定超参数λ,正则化项为:%7D%5Cright%5C%7C%7BF%7D%5E%7B2%7D%2B%5Cleft%5C%7C%5Cmathbf%7BW%7D%5E%7B(2)%7D%5Cright%5C%7C%7BF%7D%5E%7B2%7D%5Cright)%0A#card=math&code=s%3D%5Cfrac%7B%5Clambda%7D%7B2%7D%5Cleft%28%5Cleft%5C%7C%5Cmathbf%7BW%7D%5E%7B%281%29%7D%5Cright%5C%7C%7BF%7D%5E%7B2%7D%2B%5Cleft%5C%7C%5Cmathbf%7BW%7D%5E%7B%282%29%7D%5Cright%5C%7C%7BF%7D%5E%7B2%7D%5Cright%29%0A&id=YOwhm)
最后,模型在给定数据样本上的正则化损失为(目标函数):
4.7.2 反向传播
反向传播指计算神经网络参数梯度的方法,该方法按相反的顺序从输出层到输入层遍历网络。 该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。
反向传播的目的是计算梯度%7D#card=math&code=%E2%88%82J%2F%E2%88%82%5Cmathbf%7BW%7D%5E%7B%281%29%7D&id=ZplUd)和
%7D#card=math&code=%E2%88%82J%2F%E2%88%82%5Cmathbf%7BW%7D%5E%7B%282%29%7D&id=vRTIR)。
- 计算目标函数
相对于损失项L和正则项s的梯度。
。
- 根据链式法则计算目标函数关于输出层变量o的梯度:
%3D%5Cfrac%7B%5Cpartial%20L%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bq%7D#card=math&code=%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%3D%5Coperatorname%7Bprod%7D%5Cleft%28%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20L%7D%2C%20%5Cfrac%7B%5Cpartial%20L%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%5Cright%29%3D%5Cfrac%7B%5Cpartial%20L%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bq%7D&id=YCiNq)
- 计算正则化项相对于两个参数的梯度:
%7D%7D%7D%3D%CE%BB%5Cmathbf%7BW%5E%7B1%7D%7Dand%5Cfrac%7B%5Cpartial%20s%7D%7B%5Cpartial%20%5Cmathbf%7BW%5E%7B(2)%7D%7D%7D%3D%CE%BB%5Cmathbf%7BW%5E%7B2%7D%7D#card=math&code=%5Cfrac%7B%5Cpartial%20s%7D%7B%5Cpartial%20%5Cmathbf%7BW%5E%7B%281%29%7D%7D%7D%3D%CE%BB%5Cmathbf%7BW%5E%7B1%7D%7Dand%5Cfrac%7B%5Cpartial%20s%7D%7B%5Cpartial%20%5Cmathbf%7BW%5E%7B%282%29%7D%7D%7D%3D%CE%BB%5Cmathbf%7BW%5E%7B2%7D%7D&id=SX7sa)。
- 现在可以计算最接近输出层的模型参数的梯度
%7D%7D%7D%5Cin%20%5Cmathbb%7BR%7D%5E%7Bq%20%5Ctimes%20h%7D#card=math&code=%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7BW%5E%7B%282%29%7D%7D%7D%5Cin%20%5Cmathbb%7BR%7D%5E%7Bq%20%5Ctimes%20h%7D&id=FBYQl)。使用链式法则得出:
%7D%7D%3D%5Coperatorname%7Bprod%7D%5Cleft(%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%2C%20%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B(2)%7D%7D%5Cright)%2B%5Coperatorname%7Bprod%7D%5Cleft(%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20s%7D%2C%20%5Cfrac%7B%5Cpartial%20s%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B(2)%7D%7D%5Cright)%3D%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%20%5Cmathbf%7Bh%7D%5E%7B%5Ctop%7D%2B%5Clambda%20%5Cmathbf%7BW%7D%5E%7B(2)%7D#card=math&code=%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B%282%29%7D%7D%3D%5Coperatorname%7Bprod%7D%5Cleft%28%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%2C%20%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B%282%29%7D%7D%5Cright%29%2B%5Coperatorname%7Bprod%7D%5Cleft%28%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20s%7D%2C%20%5Cfrac%7B%5Cpartial%20s%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B%282%29%7D%7D%5Cright%29%3D%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%20%5Cmathbf%7Bh%7D%5E%7B%5Ctop%7D%2B%5Clambda%20%5Cmathbf%7BW%7D%5E%7B%282%29%7D&id=JD8kY)。
- 隐藏层输出的梯度
由下式给出:
%3D%5Cmathbf%7BW%7D%5E%7B(2)%7D%20%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D#card=math&code=%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bh%7D%7D%3D%5Coperatorname%7Bprod%7D%5Cleft%28%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%2C%20%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D%7B%5Cpartial%20%5Cmathbf%7Bh%7D%7D%5Cright%29%3D%5Cmathbf%7BW%7D%5E%7B%282%29%7D%20%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bo%7D%7D&id=jgnwB)。
- 由于激活函数ϕ是按元素计算的, 中间变量z的梯度
需要使用按元素乘法运算符,用⊙表示:
%3D%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bh%7D%7D%20%5Codot%20%5Cphi%5E%7B%5Cprime%7D(%5Cmathbf%7Bz%7D)#card=math&code=%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bz%7D%7D%3D%5Coperatorname%7Bprod%7D%5Cleft%28%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bh%7D%7D%2C%20%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bh%7D%7D%7B%5Cpartial%20%5Cmathbf%7Bz%7D%7D%5Cright%29%3D%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bh%7D%7D%20%5Codot%20%5Cphi%5E%7B%5Cprime%7D%28%5Cmathbf%7Bz%7D%29&id=dKcyJ)。
- 最后可以得到最接近输入层的模型参数的梯度
。 根据链式法则,我们得到:
%7D%7D%3D%5Coperatorname%7Bprod%7D%5Cleft(%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bz%7D%7D%2C%20%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bz%7D%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B(1)%7D%7D%5Cright)%2B%5Coperatorname%7Bprod%7D%5Cleft(%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20s%7D%2C%20%5Cfrac%7B%5Cpartial%20s%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B(1)%7D%7D%5Cright)%3D%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bz%7D%7D%20%5Cmathbf%7Bx%7D%5E%7B%5Ctop%7D%2B%5Clambda%20%5Cmathbf%7BW%7D%5E%7B(1)%7D#card=math&code=%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B%281%29%7D%7D%3D%5Coperatorname%7Bprod%7D%5Cleft%28%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bz%7D%7D%2C%20%5Cfrac%7B%5Cpartial%20%5Cmathbf%7Bz%7D%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B%281%29%7D%7D%5Cright%29%2B%5Coperatorname%7Bprod%7D%5Cleft%28%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20s%7D%2C%20%5Cfrac%7B%5Cpartial%20s%7D%7B%5Cpartial%20%5Cmathbf%7BW%7D%5E%7B%281%29%7D%7D%5Cright%29%3D%5Cfrac%7B%5Cpartial%20J%7D%7B%5Cpartial%20%5Cmathbf%7Bz%7D%7D%20%5Cmathbf%7Bx%7D%5E%7B%5Ctop%7D%2B%5Clambda%20%5Cmathbf%7BW%7D%5E%7B%281%29%7D&id=Cz8Fd).
注意,反向传播重复利用前向传播中存储的中间值,以避免重复计算。 带来的影响之一是我们需要保留中间值,直到反向传播完成。 这也是训练比单纯的预测需要更多的内存(显存)的原因之一。 此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。 因此,使用更大的批量来训练更深层次的网络更容易导致内存不足(out of memory)。
4.8 梯度消失和梯度爆炸
参数初始化方案的选择对保持数值稳定性至关重要。选择哪个函数以及如何初始化参数可以决定优化算法收敛的速度。糟糕选择可能会导致在训练时遇到梯度爆炸或梯度消失。
深层神经网络在反向传播进行连续求导时,因为不同的乘积矩阵拥有不同的数值,其可能很小,也可能很大,过多的矩阵乘积可能会导致运算结果非常小或者非常大。
不稳定梯度会威胁优化算法的稳定性。可能导致梯度爆炸(gradient exploding)问题: 参数更新过大,破坏了模型的稳定收敛; 或者是梯度消失(gradient vanishing)问题: 参数更新过小,在每次更新时几乎不会移动,导致模型无法学习。
4.8.1 梯度消失
早期人工神经网络受到生物神经网络的启发, 神经元要么完全激活要么完全不激活,因此经常使用sigmoid激活函数。然而,它却是导致梯度消失问题的一个常见的原因。
import torch
from d2l import torch as d2l
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.sigmoid(x)
y.backward(torch.ones_like(x))
d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
当sigmoid函数的输入很大或是很小时,它的梯度都会消失。因此,更稳定的ReLU系列函数已经成为从业者的默认选择(虽然在神经科学的角度看起来不太合理)。
4.8.2 梯度爆炸
M = torch.normal(0, 1, size=(4,4)) # 这里的主要原因是方差过大
print('一个矩阵 \n',M)
for i in range(100):
M = torch.mm(M,torch.normal(0, 1, size=(4, 4)))
print('乘以100个矩阵后\n', M)
"""
一个矩阵
tensor([[ 1.7106, 0.6991, 0.7239, 0.2631],
[ 1.1719, -1.6802, 0.7616, -0.1783],
[ 0.3981, -0.0867, -0.9208, -0.0751],
[ 0.7215, -0.8688, -0.2521, -0.6944]])
乘以100个矩阵后
tensor([[-7.7789e+23, -4.5248e+23, 1.7070e+24, -1.3195e+24],
[-1.1803e+24, -6.8656e+23, 2.5901e+24, -2.0021e+24],
[-3.6023e+22, -2.0954e+22, 7.9051e+22, -6.1103e+22],
[-1.2962e+23, -7.5398e+22, 2.8445e+23, -2.1987e+23]])
"""
此处梯度爆炸的出现就是因为深度网络的初始化参数导致的。我们没有机会让梯度下降优化器收敛。
4.8.3 解决办法
解决(或减轻)上述问题的一种方法是进行参数初始化, 优化期间适当的正则化也可以提高稳定性。先前采用正态分布来初始化权重值。如果我们不指定初始化方法, 框架将使用默认的随机初始化方法,对于中等难度的问题,这种方法通常很有效。
假设没有非线性的全连接输出的尺度分布,对于该层
输入
及其相关权重
,输出由下式给出:
权重均从同一分布中独立抽取,此外假设该分布有零均值和方差(并不意味必须是高斯分布)。假设层
的输入也具有零均值和方差
,并且它们独立于
并且彼此独立。此时,可按如下方式计算
的均值和方差:
保持任意隐藏层方差不变的方法是设置。在反向传播时使用与前向传播相同的推断,可以得到只有
才能保证方差固定,其中
是该层输出的数量。然而由于
与
分别来自前后两层,这往往是不能满足的。但我们只需满足下式即可:
%5Csigma%5E%7B2%7D%3D1%0A#card=math&code=%5Cfrac%7B1%7D%7B2%7D%28n%7Bin%7D%2Bn%7Bout%7D%29%5Csigma%5E%7B2%7D%3D1%0A&id=hDyTo)
这就是现在标准且实用的Xavier初始化的基础。 通常,Xavier初始化从均值为零,方差 的高斯分布中采样权重。
尽管在上述数学推理中,“不存在非线性”的假设在神经网络中很容易被违反, 但Xavier初始化方法在实践中被证明是有效的。
上面的推理仅仅触及了现代参数初始化方法的皮毛。 深度学习框架通常实现十几种不同的启发式方法以应对不同的领域需求。 此外,参数初始化一直是深度学习基础研究的热点领域。 例如,Xiao等人演示了通过使用精心设计的初始化方法 [Xiao et al., 2018], 可以无须架构上的技巧而训练10000层神经网络的可能性。
4.9 环境和分布偏移
有时,根据测试集的精度衡量,模型表现得非常出色。 但是当数据分布突然改变时,模型在部署中会出现灾难性的失败。部分人可能会利用模型的缺陷来使模型做出“错误”的判断,例如早先用照片破解人脸解锁的问题。下面简述一些常见问题以启发批判性思维: