一 多层感知机
最简单的深度网络称为多层感知机。多层感知机由 多层神经元 组成,每一层与它的上一层相连,从中接收输入;同时每一层也与它的下一层相连,影响当前层的神经元。
softmax 实现了 如何处理数据,如何将 输出转换为有效的概率分布,并应用适当的 损失函数,根据 模型参数最小化损失。
线性意味着单调假设:任何特征的增大都会导致模型输出的增大(如果对应的权重为正),或者导致模 型输出的减小(如果对应的权重为负)。
在网络中加入隐藏层,我们可以通过 在网络中加入一个或多个隐藏层来克服线性模型的限制,使其能处理更普遍的函数关系类型。 要做到这一点,最简单的方法是 将许多全连接层堆叠在一起。每一层都输出到上面的层,直到生成最后的输出。我们可以把前L−1层看作表示,把最后一层看作线性预测器。这种架构通常称为多层感知机(multilayer perceptron),通常缩写为 MLP。下面,我们以图的方式描述了多层感知机。
下面是一个单隐藏层的多层感知机,具有5个隐藏单元:
这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。输入层不涉及任何计算,因此使用此网络 产生输出 只需要实现隐藏层和输出层的计算。因此,这个多层感知机中的 层数为2。注意,这两个层都是全连 接的。每个输入都会影响隐藏层中的每个神经元,而隐藏层中的每个神经元又会影响输出层中的每个神经元。
线性模型公式:
不加激活函数,参数可能会组到一起:
加上激活函数 σ 后公式:
为了发挥多层架构的潜力,我们还需要一个额外的关键要素:在仿射变换之后对每个隐藏单元应用非线性的 激活函数(activation function)σ。激活函数的输出(例如,σ(·))被称为活性值(activations)。一般来说, 有了激活函数,就不可能再将我们的多层感知机退化成线性模型。
由于X中的每一行对应于小批量中的一个样本,出于记号习惯的考量,我们定义非线性函数σ也以按行的方 式作用于其输入,即一次计算一个样本。
但是 应用于隐藏层的激活函数通常不仅按行操作,也按元素操作。这意味着在计算每一层的线性部分之 后,我们可以 计算每个活性值,而不需要查看其他隐藏单元所取的值。对于大多数激活函数都是这样。 为了构建更通用的多层感知机,我们可以继续堆叠这样的隐藏层,一层叠一层,从而产生更有表达能力的模型。
而且,虽然一个单隐层网络能学习任何函数,但并不意味着我们应该尝试使用单隐藏层网络来解决所有问题。 事实上,通过 使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。
二 激活函数
激活函数(activation function)通过 计算加权和 并 加上偏置 来确定神经元是否应该被激活,它们将输入信号 转换为输出的可微运算。大多数激活函数都是非线性的。
%matplotlib inline
import torch
from d2l import torch as d2l
2.1 ReLU函数
最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU),因为它实现简单,同时在各种预测任务 中表现良好。ReLU提供了一种非常简单的非线性变换。给定元素x,ReLU函数被定义为该元素与0的最大值,ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。
x = torch.arange(-8.0, 8.0, 1, requires_grad=True)
print(x)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))# tensor([-8., -7., -6., -5., -4., -3., -2., -1., 0., 1., 2., 3., 4., 5.,
# 6., 7.], requires_grad=True)
当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。注意,当输入值精确等于0时, ReLU函数不可导。在此时,我们默认使用左侧的导数,即当输入为0时导数为0。
2.1.1 反向传播后查看X的梯度
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 减轻了困扰以往神经网络的梯度消失问题。
2.2 sigmoid 函数
对于一个定义域在R中的输入,sigmoid函数将输入变换为区间(0, 1) 上的输出。
sigmoid函数是一个自然的选择,因为它是一个 平滑的、可微 的阈值单元近似。当我们想要将输出视作二元分类问题的概率时,sigmoid仍然被广泛用作 输出单元上的激活函数(sigmoid可以视为softmax的特例)。sigmoid在隐藏层中已经较少使用,它在大部分时候被更简单、 更容易训练的ReLU 所取代。
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
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))
2.3 tanh函数
与sigmoid函数类似,tanh(双曲正切)函数 也能将其输入压缩转换到区间(‐1, 1)上。
下面我们绘制tanh函数。注意,当输入在0附近时,tanh函数接近线性变换。函数的形状类似于sigmoid函数, 不同的是tanh函数 关于 坐标系原点中心对称。
y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))
tanh函数的导数图像如下所示。当输入接近0时,tanh函数的导数接近最大值1。与我们在sigmoid函数图像 中看到的类似,输入在任一方向上越远离0点,导数越接近0。
- 多层感知机在输出层和输入层之间 增加一个或多个全连接隐藏层,并 通过激活函数转换隐藏层的输出。
- 常用的激活函数包括 ReLU函数、sigmoid函数和 tanh函数。
三 多层感知机从零开始实现
3.1 导入数据
Fashion‐MNIST中的每个图像由 28 × 28 = 784个灰度像素值组成。所有图像共分为10个类别。
import torch
from torch import nn
from d2l import torch as d2lbatch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
len(train_iter), len(test_iter)# (235, 40)
3.2 初始化模型参数
,Fashion‐MNIST中的每个图像由 28 × 28 = 784个灰度像素值组成。所有图像共分为10个类别。忽 略像素之间的空间结构,我们可以将每个图像视为具有784个输入特征和10个类的简单分类数据集。首先,我 们将实现一个具有单隐藏层的多层感知机,它包含256个隐藏单元。注意,我们可以将这两个变量都视为超参 数。通常,我们选择2的若干次幂作为层的宽度。因为内存在硬件中的分配和寻址方式,这么做往往可以在计 算上更高效。
num_inputs, num_outputs, num_hiddens = 784, 10, 256W1 = 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]
params
3.3 激活函数
使用 relu 函数:
def relu(x):a = torch.zeros_like(x)return torch.max(x, a)
3.4 定义模型
因为我们忽略了空间结构,所以我们使用 reshape将每个二维图像转换为一个长度为num_inputs的向量。
def net(x):x = x.reshape((-1, num_inputs))h = relu(x@W1 + b1)return (h@W2 + b2)
3.5 定义损失函数
因此在这里我们直接使用高级API中的内置函数来 计 算softmax和交叉熵损失。
loss = nn.CrossEntropyLoss(reduction='none')
3.6 执行训练
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)
3.7 执行预测
d2l.predict_ch3(net, test_iter)
四 直接调包实现 MLP
与softmax回归的简洁实现 相比,唯一的区别是我们 添加了2个全连接层(之前我们只添加了1个全 连接层)。第一层是隐藏层,它包含128个隐藏单元,并使用了ReLU激活函数。第二层是输出层。 (隐藏层单元个数可以改,保证两个全连接层输出和输入的层数要一致)
net = nn.Sequential(nn.Flatten(),nn.Linear(784, 128),nn.ReLU(),nn.Linear(128, 10))def init_weights(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)# Sequential(
# (0): Flatten(start_dim=1, end_dim=-1)
# (1): Linear(in_features=784, out_features=128, bias=True)
# (2): ReLU()
# (3): Linear(in_features=128, out_features=10, bias=True)
# )
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.1 模型拟合相关知识
将模型在 训练数据上拟合的比在潜在分布中更接近的现象称为过拟合(overfitting),用于对抗过拟合的技术 称为 正则化(regularization)。
我们需要了解训练误差和泛化误差。训练误差(training error)是指,模型在训 练数据集上计算得到的误差。泛化误差(generalization error)是指,模型应用在 同样从原始样本的分布中 抽取的无限多数据样本时,模型误差的期望。
为了评估模型,将我们的数据分成三份,除了训练和测试数据集之外,还增加一个 验证数据集(val‐ idation dataset),也叫验证集(validation set)。
训练误差和验证误差都很严重,但它们之间仅有一点差距。如果模型不能降低训练误差,这可能意味着模型过于简单(即 表达能力不足),无法捕获试图学习的模式。此外,由于我们的训练和验证误差之间的泛化误差很小,我们有 理由相信可以 用一个更复杂的模型 降低训练误差。这种现象被称为欠拟合(underfitting)。验证集可以用于模型选择,但不能过于随意地使用它。
另一方面,当我们的训练误差明显低于验证误差时要小心,这表明严重的 过拟合(overfitting)。最终,我们 通常更关心验证误差,而不是训练误差和验证误差之间的差距。
另一个重要因素是数据集的大小。训练数据集中的样本越少,我们就越有可能过拟合。随着 训练数据量的增加,泛化误差通常会减小。
我们应该选择一个复杂度适当的模型,避免使用数量不足的训练样本。
我们总是可以通过去收集更多的 训练数据来缓解过拟合。但这可能成本很高,耗时颇多,或者完全超出我们的控制,因而在短期内不可能做 到。假设我们已经拥有尽可能多的高质量数据,我们便可以将重点放在 正则化技术 上。
4.2 正则化相关
1 权重衰减(weight decay)是最广泛使用的正则化的技术之一,它通常也被 称为 L2正则化。
2 暂退法在前向传播过程中,计算每一内部层的同时注入噪声,这已经成为 训练神经网络的常用技术。这种方法之所以被称为暂退法,因为我们从表面上看是在训练过程中丢弃(drop out)一些神经元。在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前 将当前层中的一些节 点置零。
当我们将暂退法应用到隐藏层,以p的概率 将隐藏单元置为零时,结果可以看作一个只包含原始神经元子集的网络。比如在 图4.6.1中,删除了h2和h5, 因此输出的计算不再依赖于h2或h5,并且它们各自的梯度在执行反向传播时也会消失。这样,输出层的计算 不能过度依赖于h1, . . . , h5的任何一个元素。
- 暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元。
- 暂退法可以避免过拟合,它通常与控制权重向量的维数和大小结合使用的。
- •暂退法仅在训练期间使用。
五 前向传播、反向传播和计算图
前向传播(forward propagation或forward pass)指的是:按顺序(从输入层到输出层)计算和存储神经网 络中每层的结果。
反向传播(backward propagation或backpropagation)指的是计算神经网络参数梯度的方法。
该方 法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。
在训练神经网络时,前向传播和反向传播相互依赖。对于前向传播,我们沿着依赖的方向遍历计算图并计算 其路径上的所有变量。然后将这些用于反向传播,其中计算顺序与计算图的相反。一方面,在前向传播期间计算正则项取决于 模型参数W(1)和 W(2)的当前值。它 们是由优化算法根据最近迭代的反向传播给出的。另一方面,反向传播期间参数的梯度计算,取决于 由前向传播给出的隐藏变量h的当前值。
因此,在训练神经网络时,在初始化模型参数后,我们交替使用前向传播和反向传播,利用反向传播给出的 梯度来更新模型参数。注意,反向传播重复利用前向传播中存储的中间值,以避免重复计算。带来的影响之 一是我们 需要保留中间值,直到反向传播完成。这也是 训练比单纯的预测需要更多的内存(显存)的原因之 一。此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。因此,使用更大的批量来训练更深 层次的网络更容易导致内存不足(out of memory)错误。
- 前向传播在神经网络定义的计算图中按顺序计算和存储中间变量,它的顺序是从输入层到输出层。
- 反向传播按相反的顺序(从输出层到输入层)计算和存储神经网络的中间变量和参数的梯度。
- 在训练深度学习模型时,前向传播和反向传播是相互依赖的。
- 训练比预测需要更多的内存。
5.1 参数初始化
初始化方案的选择在神经网络学习中起着举足轻重的作用,它对保持数值稳定性至关重要。此外, 这些初始化方案的选择可以与非线性激活函数的选择有趣的结合在一起。我们选择哪个函数以及如何初始化 参数可以决定优化算法收敛的速度有多快。糟糕选择可能会导致我们在训练时遇到梯度爆炸或梯度消失。
神经网络设计中的另一个问题是 其参数化所固有的对称性。
默认初始化,我们使用正态分布来初始化权重值。如果我们不指定初始化方法,框架将 使用默认的随机初始化方法,对于中等难度的问题,这种方法通常很有效。
需要用启发式的初始化方法来确保初始梯度既不太大也不太小,ReLU激活函数缓解了梯度消失问题,这样可以加速收敛。随机初始化是保证在进行优化前打破对称性的关键。
Xavier初始化,,Xavier初始化从均值为零,方差 的高斯分布中采样权重。Xavier初始化表明,对于每一层,输出的方差不受输入数量的影响,任何梯度的方差不受输出数量的影 响。