写在最前面
这章节内容太多了,预计分三次梳理完
公式用公式编辑器打出,并可直接复制进word
学习资料(《动手学深度学习》文档):http://zh.gluon.ai/chapter_how-to-use/how-to-use.html
第⼀部分包括基础知识和预备知识。
1节 提供深度学习的入门课程。
2节 中,快速介绍实践深度学习所需的前提条件,例如如何存储和处理数据
,以及如何应用基于线性代数、微积分和概率基本概念的各种数值运算。
3节 和 4节 涵盖了深度学习的最基本概念和技术,例如线性回归、多层感知机和正则化
。
第二部分,现代深度学习技术。
5节 描述了深度学习计算的各种关键组件
,并为我们随后 实现更复杂的模型奠定了基础。
6节 和 7节 中,卷积神经网络(convolutional neural network,CNN
),这是构成大多数现代计算机视觉系统骨干的强大工具。
8节 和 9节 中,循环神经网络(recurrent neural network,RNN
),这是⼀种利用数据中的时间或序列结构
的模型,通常用于自然语言处理和时间序列预测。
10节 中,注意力机制
的技术,最近它们已经开始在自然语言处理中取代循环神经网络。
这一部分将 帮助读者快速了解大多数现代深度学习应用背后的基本工具。
第三部分讨论可伸缩性、效率和应用程序
。
11节 中,用于训练深度学习模型的几种常用优化算法
。12节 将探讨影响深度学习代码计算性能的几个关键因素
。
13节 中,展示了深度学习在计算机视觉
中的主要应⽤。
14节 和 15节 中,展示如何预训练语言表示模型
并将其应用于自然语言处理任务。
3.线性神经网络
暂时跳过
3.7
4.多层感知器
最简单的深度网络称为多层感知机
。
多层感知机
由多层神经元组成, 每一层与它的上一层相连,从中接收输入; 同时每一层也与它的下一层相连,影响当前层的神经元。
当我们训练容量较大的模型时,我们面临着过拟合
的风险。
因此,本章将从基本的概念介绍开始讲起,包括过拟合、欠拟合和模型选择。
为了解决这些问题,本章将介绍权重衰减和暂退法
等正则化技术
。 我们还将讨论数值稳定性和参数初始化
相关的问题, 这些问题是成功训练深度网络的关键。
在本章的最后,我们将把所介绍的内容应用到一个真实的案例:房价预测
。 关于模型计算性能、可伸缩性和效率相关的问题,我们将放在后面的章节中讨论。
4.1.1 隐藏层
在 3.1.1.1节中描述了仿射变换
, 它是一种带有偏置项的线性变换。
图3.4.1 softmax回归的模型架构
该模型通过单个仿射变换将我们的输入直接映射到输出,然后进行softmax操作
。
如果我们的标签通过仿射变换后确实与我们的输入数据相关,那么这种方法确实足够了。 但是,仿射变换中的线性是一个很强的假设。
4.1.1.2 在网络中加入隐藏层
我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。
要做到这一点,最简单的方法是将许多全连接层堆叠
在一起。 每一层都输出到上面的层,直到生成最后的输出。
我们可以把前L-1层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机(multilayer perceptron)
,通常缩写为MLP
。
下面,我们以图的方式描述了多层感知机( 图4.1.1)。
图4.1.1 一个单隐藏层的多层感知机,具有5个隐藏单元
这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 因此,这个多层感知机中的层数为2。
注意,这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。
然而,正如 3.4.3节所说, 具有全连接层的多层感知机的参数开销可能会高得令人望而却步。
即使在不改变输入或输出大小的情况下, 可能在参数节约和模型有效性
之间进行权衡 (Zhang et al., 2021)。
4.1.1.3 从线性到非线性(激活函数)
注意在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。
本节应用于隐藏层的激活函数
通常不仅按行操作,也按元素操作。
这意味着在计算每一层的线性部分之后,我们可以计算每个活性值, 而不需要查看其他隐藏单元所取的值。
对于大多数激活函数都是这样。
4.1.2. 激活函数
激活函数(activation function)
通过计算加权
和并加上偏置
来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。
大多数激活函数都是非线性的。 由于激活函数是深度学习的基础,下面简要介绍一些常见的激活函数。
%matplotlib inline import torch from d2l import torch as d2l
4.1.2.1. ReLU函数
最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU)
, 因为它实现简单,同时在各种预测任务中表现良好。 ReLU提供了一种非常简单的非线性变换。
给定元素x,ReLU函数被定义为该元素与的最大值:
ReLU ( x ) = max ( x , 0 ) \operatorname{ReLU}(x) = \max(x, 0)ReLU(x)=max(x,0)
\operatorname{ReLU}(x) = \max(x, 0)
通俗地说,ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。
为了直观感受一下,我们可以画出函数的曲线图。 正如从图中所看到,激活函数是分段线性的。
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True) y = torch.relu(x) d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))
当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。
注意,当输入值精确等于0时,ReLU函数不可导。 在此时,我们默认使用左侧的导数,即当输入为0时导数为0。 我们可以忽略这种情况,因为输入可能永远都不会是0。
这里引用一句古老的谚语,“如果微妙的边界条件很重要,我们很可能是在研究数学而非工程”, 这个观点正好适用于这里。 下面我们绘制ReLU函数的导数。
y.backward(torch.ones_like(x), retain_graph=True) d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。
这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题(稍后将详细介绍)。
注意,ReLU函数有许多变体,包括参数化ReLU(Parameterized ReLU,pReLU) 函数 (He et al., 2015)。 该变体为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过:
pReLU ( x ) = max ( 0 , x ) + α min ( 0 , x ) \operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x)pReLU(x)=max(0,x)+αmin(0,x)
\operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x)
4.1.2.2. sigmoid函数
对于一个定义域在R中的输入, sigmoid函数
将输入变换为区间(0, 1)上的输出。 因此,sigmoid通常称为挤压函数(squashing function): 它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:
sigmoid ( x ) = 1 1 + exp ( − x ) \operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}sigmoid(x)=1+exp(−x)1
\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}
阈值单元在其输入低于某个阈值时取值0,当输入超过阈值时取值1。
当人们逐渐关注到到基于梯度的学习时, sigmoid函数是一个自然的选择,因为它是一个平滑的、可微的阈值单元近似。
将输出视作二元分类问题的概率时, sigmoid仍然被广泛用作输出单元上的激活函数 (sigmoid可以视为softmax的特例)。
然而,sigmoid在隐藏层中已经较少使用, 它在大部分时候被更简单、更容易训练的ReLU所取代。
在后面关于循环神经网络的章节中,我们将描述利用sigmoid单元来控制时序信息流的架构。
下面,我们绘制sigmoid函数。 注意,当输入接近0时,sigmoid函数接近线性变换。
y = torch.sigmoid(x) d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
sigmoid函数的导数为下面的公式:
d d x sigmoid ( x ) = exp ( − x ) ( 1 + exp ( − x ) ) 2 = sigmoid ( x ) ( 1 − sigmoid ( x ) ) . \frac{d}{dx} \operatorname{sigmoid}(x) = \frac{\exp(-x)}{(1 + \exp(-x))^2} = \operatorname{sigmoid}(x)\left(1-\operatorname{sigmoid}(x)\right).dxdsigmoid(x)=(1+exp(−x))2exp(−x)=sigmoid(x)(1−sigmoid(x)).
\frac{d}{dx} \operatorname{sigmoid}(x) = \frac{\exp(-x)}{(1 + \exp(-x))^2} = \operatorname{sigmoid}(x)\left(1-\operatorname{sigmoid}(x)\right).
sigmoid函数的导数图像如下所示。 注意,当输入为0时,sigmoid函数的导数达到最大值0.25; 而输入在任一方向上越远离0点时,导数越接近0。
# 清除以前的梯度 x.grad.data.zero_() y.backward(torch.ones_like(x),retain_graph=True) d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))
4.1.2.3. tanh函数
与sigmoid函数类似, tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。 tanh函数的公式如下:
tanh ( x ) = 1 − exp ( − 2 x ) 1 + exp ( − 2 x ) \operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}tanh(x)=1+exp(−2x)1−exp(−2x)
\operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}
下面我们绘制tanh函数。 注意,当输入在0附近时,tanh函数接近线性变换。 函数的形状类似于sigmoid函数, 不同的是tanh函数关于坐标系原点中心对称。
y = torch.tanh(x) d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))
tanh函数的导数是:
d d x tanh ( x ) = 1 − tanh 2 ( x ) \frac{d}{dx} \operatorname{tanh}(x) = 1 - \operatorname{tanh}^2(x)dxdtanh(x)=1−tanh2(x)
\frac{d}{dx} \operatorname{tanh}(x) = 1 - \operatorname{tanh}^2(x)
tanh函数的导数图像如下所示。
当输入接近0时,tanh函数的导数接近最大值1。
与我们在sigmoid函数图像中看到的类似, 输入在任一方向上越远离0点,导数越接近0。
# 清除以前的梯度 x.grad.data.zero_() y.backward(torch.ones_like(x),retain_graph=True) d2l.plot(x.detach(), x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))
4.1.3. 小结
多层感知机在输出层和输入层之间增加一个或多个全连接隐藏层
,并通过激活函数
转换隐藏层的输出。
常用的激活函数包括ReLU函数、sigmoid函数和tanh函数。
4.2 多层感知机的从零实现(可跳过,后面有API实现)
import torch from torch import nn from d2l import torch as d2l batch_size = 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
4.2.1 初始化模型参数
实现一个具有单隐藏层的多层感知机, 它包含256个隐藏单元。 注意,我们可以将这两个变量都视为超参数。
通常,我们选择2的若干次幂作为层的宽度
。 因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效。
我们用几个张量来表示我们的参数。 注意,对于每一层我们都要记录一个权重矩阵和一个偏置向量。 跟以前一样,我们要为损失关于这些参数的梯度分配内存。
num_inputs, num_outputs, num_hiddens = 784, 10, 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]
4.2.2 激活函数
实现ReLU激活函数, 而不是直接调用内置的relu函数。
def relu(X): a = torch.zeros_like(X) return torch.max(X, a)
4.2.3. 模型
因为我们忽略了空间结构, 所以我们使用reshape将每个二维图像转换为一个长度为num_inputs的向量。
def net(X): X = X.reshape((-1, num_inputs)) H = relu(X@W1 + b1) # 这里“@”代表矩阵乘法 return (H@W2 + b2)
4.2.4. 损失函数
从零实现过softmax函数( 3.6节)
直接使用高级API中的内置函数来计算softmax和交叉熵损失
。
3.7.2节中 对这些复杂问题的讨论。之后可以查看损失函数的源代码,以加深对实现细节的了解。
loss = nn.CrossEntropyLoss(reduction='none')
4.2.5. 训练
多层感知机的训练过程与softmax回归的训练过程完全相同。 可以直接调用d2l包的train_ch3函数(参见 3.6节 ), 将迭代周期数设置为10,并将学习率设置为0.1.
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)
为了对学习到的模型进行评估,我们将在一些测试数据上应用这个模型。
d2l.predict_ch3(net, test_iter)
4.3 多层感知机的简洁实现
本节将介绍通过高级API更简洁地实现多层感知机。
from mxnet import gluon, init, npx from mxnet.gluon import nn from d2l import mxnet as d2l npx.set_np()
4.3.1. 模型
与softmax回归的简洁实现( 3.7节)相比, 唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。
第一层是隐藏层,它包含256个隐藏单元,并使用了ReLU激活函数。
第二层是输出层。
net = nn.Sequential() net.add(nn.Dense(256, activation='relu'), nn.Dense(10)) net.initialize(init.Normal(sigma=0.01))
训练过程的实现与我们实现softmax回归时完全相同, 这种模块化设计使我们能够将与模型架构有关的内容独立出来。
batch_size, lr, num_epochs = 256, 0.1, 10 loss = gluon.loss.SoftmaxCrossEntropyLoss() trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 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.4 欠拟合与过拟合(跳过)
训练误差(training error)
是指, 模型在训练数据集上计算得到的误差。
泛化误差(generalization error)
是指, 模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。
模型选择
模型复杂性
。几个倾向于影响模型泛化的因素。
1、可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。
2、参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
3、训练样本的数量。即使模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。
因此,书中每次实验报告的准确度都是验证集准确度
,而不是测试集准确度。
当训练数据稀缺时
,我们甚至可能无法提供足够的数据来构成一个合适的验证集。 这个问题的一个流行的解决方案是采用K折交叉验证
。
这里,原始训练数据被分成K个不重叠的子集。 然后执行K次模型训练和验证,每次在K-1个子集上进行训练, 并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。 最后,通过对K次实验的结果取平均来估计训练和验证误差。
欠拟合和过拟合
欠拟合
是指模型无法继续减少训练误差。
过拟合
是指训练误差远小于验证误差。
我们应该选择一个复杂度适当的模型,避免使用数量不足的训练样本。
模型复杂度对欠拟合和过拟合的影响
正常
欠拟合
过拟合
数据集大小
另一个重要因素是数据集的大小。
训练数据集中的样本越少,我们就越有可能(且更严重地)过拟合。
随着训练数据量的增加,泛化误差通常会减小。 此外,一般来说,更多的数据不会有什么坏处。
对于固定的任务和数据分布,模型复杂性和数据集大小之间通常存在关系。 给出更多的数据,我们可能会尝试拟合一个更复杂的模型。 能够拟合更复杂的模型可能是有益的。
如果没有足够的数据,简单的模型可能更有用。 对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。 从一定程度上来说,深度学习目前的生机要归功于 廉价存储、互联设备以及数字化经济带来的海量数据集。
小结
由于不能基于训练误差来估计泛化误差,因此简单地最小化训练误差并不一定意味着泛化误差的减小。机器学习模型需要注意防止过拟合,即防止泛化误差过大。
验证集可以用于模型选择,但不能过于随意地使用它。
4.5 权重衰减
仅考虑惩罚项,优化算法在训练的每一步衰减权重。
与特征选择相比,权重衰减提供了一种连续的机制来调整函数的复杂度。
简洁实现
由于权重衰减在神经网络优化中很常用, 深度学习框架为了便于我们使用权重衰减, 将权重衰减集成到优化算法中,以便与任何损失函数结合使用。
此外,这种集成还有计算上的好处, 允许在不增加任何额外的计算开销的情况下向算法中添加权重衰减。
由于更新的权重衰减部分仅依赖于每个参数的当前值, 因此优化器必须至少接触每个参数一次。
训练误差增大,但测试误差减小。 这正是我们期望从正则化中得到的效果。
在下面的代码中,我们在实例化优化器时直接通过weight_decay指定weight decay超参数。 默认情况下,PyTorch同时衰减权重和偏移。 这里我们只为权重设置了weight_decay,所以偏置参数
不会衰减。
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)
w的L2范数: 14.670721054077148
train_concise(3)
w的L2范数: 0.3454631567001343
到目前为止,我们只接触到一个简单线性函数的概念。 此外,由什么构成一个简单的非线性函数可能是一个更复杂的问题。
例如,再生核希尔伯特空间(RKHS) 允许在非线性环境中应用为线性函数引入的工具。 不幸的是,基于RKHS的算法往往难以应用到大型、高维的数据。 在这本书中,我们将默认使用简单的启发式方法,即在深层网络的所有层上应用权重衰减。
4.5.4. 小结
正则化是处理过拟合的常用方法:在训练集的损失函数中加入惩罚项,以降低学习到的模型的复杂度。
保持模型简单的一个特别的选择是使用
惩罚的权重衰减。这会导致学习算法更新步骤中的权重衰减。
权重衰减功能在深度学习框架的优化器中提供。
在同一训练代码实现中,不同的参数集可以有不同的更新行为。
4.6 暂退法(Dropout)
在 4.5节 中, 我们介绍了通过惩罚权重的
范数来正则化统计模型的经典方法。
在概率角度看,我们可以通过以下论证来证明这一技术的合理性: 我们已经假设了一个先验,即权重的值取自均值为0的高斯分布。
更直观的是,我们希望模型深度挖掘特征,即将其权重分散到许多特征中, 而不是过于依赖少数潜在的虚假关联。
4.6.1 过拟合
当面对更多的特征而样本不足时,线性模型往往会过拟合。
相反,当给出更多样本而不是特征,通常线性模型不会过拟合。
但线性模型泛化的可靠性
是有代价的。
简单地说,线性模型没有考虑到特征之间的交互作用
。 对于每个特征,线性模型必须指定正的或负的权重,而忽略其他特征。
泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡(bias-variance tradeoff)
。
线性模型有很高的偏差:它们只能表示一小类函数。 然而,这些模型的方差很低:它们在不同的随机数据样本上可以得出相似的结果。
深度神经网络位于偏差-方差谱的另一端。
与线性模型不同,神经网络并不局限于单独查看每个特征,而是学习特征之间的交互
。
例如,神经网络可能推断“尼日利亚”和“西联汇款”一起出现在电子邮件中表示垃圾邮件, 但单独出现则不表示垃圾邮件。
即使我们有比特征多得多的样本,深度神经网络也有可能过拟合。
2017年,一组研究人员通过在随机标记的图像上训练深度网络。 这展示了神经网络的极大灵活性,因为人类很难将输入和随机标记的输出联系起来, 但通过随机梯度下降优化的神经网络可以完美地标记训练集中的每一幅图像。
想一想这意味着什么? 假设标签是随机均匀分配的,并且有10个类别,那么分类器在测试数据上很难取得高于10%的精度, 那么这里的泛化差距就高达90%,如此严重的过拟合。
深度网络的泛化性质令人费解,而这种泛化性质的数学基础仍然是悬而未决的研究问题。
本节,将着重对实际工具
的探究,这些工具倾向于改进深层网络的泛化性
。
4.6.2. 扰动的稳健性
期待“好”的预测模型能在未知的数据上有很好的表现: 经典泛化理论认为,为了缩小训练和测试性能之间的差距,应该以简单的模型为目标
。
简单性以较小维度的形式展现, 在 4.4节 讨论线性模型的单项式函数时探讨了这一点。
此外,在 4.5节 中讨论权重衰减(L2正则化)时看到的那样, 参数的范数
也代表了一种有用的简单性度量
。
简单性的另一个角度是平滑性
,即函数不应该对其输入的微小变化敏感
。
例如,当我们对图像进行分类时,我们预计向像素添加一些随机噪声应该是基本无影响的。
1995年,克里斯托弗·毕晓普证明了 具有输入噪声的训练等价于Tikhonov正则化 (Bishop, 1995)。
这项工作用数学证实了“要求函数光滑”和“要求函数对输入的随机噪声具有适应性”之间的联系。
然后在2014年,斯里瓦斯塔瓦等人 (Srivastava et al., 2014) 就如何将毕晓普的想法应用于网络的内部层提出了一个想法: 在训练过程中,他们建议在计算后续层之前向网络的每一层注入噪声。 因为当训练一个有多层的深层网络时,注入噪声只会在输入-输出映射上增强平滑性。
这个想法被称为暂退法(dropout)。 暂退法在前向传播过程中,计算每一内部层的同时注入噪声,这已经成为训练神经网络的常用技术。
这种方法之所以被称为暂退法,因为我们从表面上看是在训练过程中丢弃(drop out)一些神经元
。 在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些节点置零。
需要说明的是,暂退法的原始论文提到了一个关于有性繁殖的类比: 神经网络过拟合与每一层都依赖于前一层激活值相关,称这种情况为“共适应性”。 作者认为,暂退法会破坏共适应性,就像有性生殖会破坏共适应的基因一样。
那么关键的挑战就是如何注入这种噪声。 一种想法是以一种无偏向(unbiased)
的方式注入噪声
。 这样在固定住其他层时,每一层的期望值等于没有噪音时的值。
4.6.3. 实践中的暂退法
回想一下 图4.1.1中带有1个隐藏层和5个隐藏单元的多层感知机。
当我们将暂退法应用到隐藏层,以p的概率将隐藏单元置为零时, 结果可以看作一个只包含原始神经元子集的网络。
比如在 图4.6.1中,删除了h2和h5, 因此输出的计算不再依赖于h2或h5,并且它们各自的梯度在执行反向传播时也会消失。 这样,输出层的计算不能过度依赖于h1,h2,h3,h4,h5的任何一个元素。
图4.6.1 dropout前后的多层感知机
通常,我们在测试时不用暂退法
。
给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。
然而也有一些例外
:一些研究人员在测试时使用暂退法, 用于估计神经网络预测的“不确定性”: 如果通过许多不同的暂退法遮盖后得到的预测结果都是一致的,那么可以说网络发挥更稳定
。
4.6.5 简洁实现
对于深度学习框架的高级API,我们只需在每个全连接层之后添加一个Dropout层, 将暂退概率作为唯一的参数传递给它的构造函数。 在训练时,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)) def init_weights(m): if type(m) == nn.Linear: nn.init.normal_(m.weight, std=0.01) net.apply(init_weights);
接下来,我们对模型进行训练和测试。
trainer = torch.optim.SGD(net.parameters(), lr=lr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
4.6.6. 小结
暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元。
暂退法可以避免过拟合,它通常与控制权重向量的维数和大小结合使用的。
暂退法将活性值h替换为具有期望值h的随机变量。
暂退法仅在训练期间使用。