文章目录
01 多层感知机
1、感知机
感知机(perceptron)是二类分类的线性分类模型,其输入为实例的特征向量,输出为实例的类别。
与之前的线性回归的输出不同:
- 线性回归:实数
- 感知机:离散的类
感知机就是二分类问题,上图所示函数中,输出也可以改为+1、-1
感知机是1957年,由Rosenblatt提出会,是神经网络和支持向量机的基础。
感知机 (Perceptron)是二分类的线性分类模型,其输入为实例的特征向量,输出为实例的类别,取+1和-1。感知机对应于输入空间(特征空间)中将实例划为正负两类的分离超平面,属于判别模型。感知机预测是用学习得到的感知机模型对新的输入实例进行分类,是神经网络与支持向量机的基础。
从下图大概看一下
我们大概从下图看下感知机的训练过程。
线性可分的过程:
线性不可分的过程:
[
以猫狗分类为例子,讲解感知机:
感知机的问题:不能你和XOR问题,它只能产生线性分割面
总结
- 感知机是一个二分类模型,是最早的AI模型之一
- 它的求解算法等价于使用批量大小为1的梯度下降
- 它不能拟合XOR函数,导致第一次AI寒冬
2、多层感知机
多层感知机(MLP,Multilayer Perceptron)也叫人工神经网络(ANN,Artificial Neural Network
首先,学习一下XOR(异或问题):
对于单层感知机,它只能表示由一条直线分割的空间,无法表示异或门
但我们可以用双层感知机来实现,单层感知机无法表示的东西,通过增加层数来实现。
反过来讲,通过叠加层,感知机能够表达更多更有意义的事情,例如:加法运算,进制转换,…。
2.1、隐藏层
我们描述了仿射变换, 它是一种带有偏置项的线性变换。
如果我们的标签通过仿射变换后确实与我们的输入数据相关,那么这种方法确实足够了。
但是,仿射变换中的线性是一个很强的假设。
2.1.1 线性模型可能会出错
2.1.2 在网络中加入隐藏层
我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。
要做到这一点,最简单的方法是将许多全连接层堆叠在一起。
每一层都输出到上面的层,直到生成最后的输出。 我们可以把前L−1层看作表示,把最后一层看作线性预测器。
这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP。
如下图:
这是一个单隐藏层的多层感知机, 其中输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算,因此我们说多层感知机的层数时,忽略输入层,如上图层数为2。注意,这两个层都是全连接的。
2.1.3 从线性到非线性
核心点:
Q:为什么激活函数是非线性的:
A:如果使用线性激活函数,那么输入跟输出之间的关系为线性的,无论神经网络有多少层都是线性组合。
2.1.4 通用近似定理
俗地来讲,多层感知机可以看成是一个万能的函数近似器。
3、激活函数
在神经元中,输入的 inputs 通过加权,求和后,还被作用了一个函数,这个函数就是激活函数。引入激活函数是为了增加神经网络模型的非线性。如果不用激活函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合,这种情况就是最原始的感知机(Perceptron)。激活函数给神经元引入了非线性因素,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以应用到众多的非线性模型中。
激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。 大多数激活函数都是非线性的。 由于激活函数是深度学习的基础,下面简要介绍一些常见的激活函数。
%matplotlib inline
import torch
from d2l import torch as d2l
3.1、ReLU
ReLU:最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU), 因为它实现简单,同时在各种预测任务中表现良好。
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函数的导数:
当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。 在此时,我们默认使用左侧的导数,即当输入为0时导数为0。
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的收敛速度比 sigmoid 和 tanh 快;
- 函数在x>0区域上,梯度不会饱和,解决了梯度消失问题;
- 计算复杂度低,不需要进行指数运算,只要一个阈值就可以得到激活值;
- 适合用于后向传播。
缺:
- ReLU的输出不是zero-centered(0均值);
- Dead ReLU Problem(神经元坏死现象):在x<0时,梯度为0。这个神经元及之后的神经元梯度永远为0,不再对任何数据有所响应,导致相应参数永远不会被更新。。
- 产生这种现象的两个原因:1、参数初始化问题;2、learning rate太高导致在训练过程中参数更新太大。
- 解决方法:采用Xavier初始化方法,以及避免将learning rate设置太大或使用adagrad等自动调节learning rate的算法。
- ReLU不会对数据做幅度压缩,所以数据的幅度会随着模型层数的增加不断扩张。
3.2、sigmoid
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
# 清除以前的梯度
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))
优缺点:
优:
- 连续函数,便于求导的平滑函数;
- 能压缩数据,保证数据幅度不会有问题;
- 适合用于前向传播。
缺:
-
容易出现**梯度消失(gradient vanishing)**的现象:当激活函数接近饱和区时,变化太缓慢,导数接近0,根据后向传递的数学依据是微积分求导的链式法则,当前导数需要之前各层导数的乘积,几个比较小的数相乘,导数结果很接近0,从而无法完成深层网络的训练。
在反向传播时,当梯度接近于0,权重基本不会更新,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练。
-
Sigmoid的输出不是0均值(zero-centered)的:这会导致后层的神经元的输入是非0均值的信号,这会对梯度产生影响。以 f=sigmoid(wx+b)为例, 假设输入均为正数(或负数),那么对w的导数总是正数(或负数),这样在反向传播过程中要么都往正方向更新,要么都往负方向更新,导致有一种捆绑效果,使得收敛缓慢。
-
计算复杂度高,因为sigmoid函数是指数形式。幂运算相对耗时
3.3、tanh
Tanh函数相当于 sigmoid 的变形
y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))
# 清除以前的梯度
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))
优缺点:
优:
- tanh函数将输入值压缩到 -1~1 的范围,因此它是0均值的,解决了Sigmoid函数的非zero-centered问题
缺:
- 存在梯度消失和幂运算的问题(梯度饱和与exp计算的问题)。
总结
- 多层感知机使用隐藏层和激活函数来得到非线性模型
- 常用激活函数是Sigmoid,Tanh,ReLU
- 使用Softmax来处理多类分类
- 超参数为隐藏层数,和各个隐藏层大小
02 多层感知机的从零开始实现
这里我们基于实现一个多层感知机(MLP),完成对ashion-MNIST图像分类数据集的分类.
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)
1、初始化模型参数
"""
参数含义:
1、每个图像视为具有784个输入特征;
2、10个类别;
3、我们将实现一个具有单隐藏层的多层感知机, 它包含256个隐藏单元。
"""
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]
2、激活函数
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
3、模型
# 由于我们忽略了图像的空间结构,所以我们使用reshape将每个二维图像转换为一个长度为num_inputs的向量。
def net(X):
X = X.reshape((-1, num_inputs))
H = relu(X@W1 + b1) # 这里“@”代表矩阵乘法
return (H@W2 + b2)
4、损失函数
loss = nn.CrossEntropyLoss(reduction='none')
5、训练
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)
效果展示:
这里可以看到,相比之前基于softmax实现对Fashion-MNIST数据集的训练,loss下降了,但acc几乎一致,后续沐神会讲述。
6、评估
d2l.predict_ch3(net, test_iter)
03 多层感知机的简洁实现
可以通过高级API更简洁地实现多层感知机。
import torch
from torch import nn
from d2l import torch as d2l
1、模型
与softmax回归的简洁实现不同,唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。
- 第一层是隐藏层
- 它包含256个隐藏单元,并使用了ReLU激活函数。
- 第二层是输出层
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)
2、训练
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)
效果展示:
04 模型选择、欠拟合和过拟合
1、训练误差和泛化误差
我们需要了解两个误差:
- 训练误差(training error):模型在训练数据集上计算得到的误差
- 泛化误差(generalization error):模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望
PS:实际上我们永远不能准确地计算出泛化误差,因为我们没有无限多的数据样本
1.1、统计学理论
由于泛化是机器学习中的基本问题, 许多数学家和理论家毕生致力于研究描述这一现象的形式理论。在同名定理(eponymous theorem)中, 格里文科和坎特利推导出了训练误差收敛到泛化误差的速率。 在一系列开创性的论文中, Vapnik和Chervonenkis 将这一理论扩展到更一般种类的函数。 这项工作为统计学习理论奠定了基础。
在我们目前已探讨、并将在之后继续探讨的监督学习情景中, 我们假设训练数据和测试数据都是从相同的分布中独立提取的。 这通常被称为独立同分布假设(i.i.d. assumption), 这意味着对数据进行采样的过程没有进行“记忆”。 换句话说,抽取的第2个样本和第3个样本的相关性, 并不比抽取的第2个样本和第200万个样本的相关性更强。
当然,这个假设很容易找到失效的情况。
VC维
VC维的用处:
- 提供为什么一个模型好的理论依据
- 它可以衡量训练误差和泛化误差之间的隔阂
- 但深度学习中很少用
- 衡量不是很准确
- 计算深度学习模型的VC维很困难
1.2、模型复杂性
- 当我们有简单的模型和大量的数据时,我们期望泛化误差与训练误差相近。
- 当我们有更复杂的模型和更少的样本时,我们预计训练误差会下降,但泛化误差会增大。
模型复杂性由什么构成是一个复杂的问题。 一个模型是否能很好地泛化取决于很多因素。
我们很难比较本质上不同大类的模型之间(例如,决策树与神经网络)的复杂性。而通常对于神经网络,我们认为需要更多训练迭代的模型比较复杂, 而需要“早停”(early stopping)的模型(即较少训练迭代周期)就不那么复杂。
估计模型容量大小的关键:
- 参数个数
- 参数值的选择范围
重点介绍几个倾向于影响模型泛化的因素:
- 可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。
- 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
- 训练样本的数量。即使你的模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。
1.3、模型的选择
在机器学习中,我们通常在评估几个候选模型后选择最终的模型, 这个过程叫做模型选择。
例如,训练多层感知机模型时,我们可能希望比较具有 不同数量的隐藏层、不同数量的隐藏单元以及不同的激活函数组合的模型。 为了确定候选模型中的最佳模型,我们通常会使用验证集。
1.3.1 验证集
如果我们过拟合了测试数据,我们又该怎么知道呢?
解决此问题的常见做法是将我们的数据分成三份:训练集、测试集、验证集(validation dataset)。
1.3.2 K折交叉验证
当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。 这个问题的一个流行的解决方案是采用K折交叉验证。
- 原始训练数据被分成K个不重叠的子集
- 执行K次模型训练和验证,每次在K−1个子集上进行训练
- 在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证
- 通过对K次实验的结果取平均来估计训练和验证误差
1.4、欠拟合还是过拟合
当我们比较训练和验证误差时,我们要注意两种常见的情况。
- 训练误差和验证误差都很严重, 但它们之间仅有一点差距。
- 如果模型不能降低训练误差,这可能意味着模型过于简单(即表达能力不足), 无法捕获试图学习的模式。 此外,由于我们的训练和验证误差之间的泛化误差很小, 我们有理由相信可以用一个更复杂的模型降低训练误差。 这种现象被称为欠拟合(underfitting)
- 训练误差明显低于验证误差时要小心, 这表明严重的过拟合(overfitting)
- 注意,过拟合并不总是一件坏事。
是否过拟合或欠拟合可能取决于模型复杂性和可用训练数据集的大小, 这两个点将在下面进行讨论。
1.4.1 模型复杂度
1.4.2 数据集大小
训练数据集中的样本越少,我们就越有可能(且更严重地)过拟合。 随着训练数据量的增加,泛化误差通常会减小。
对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。 从一定程度上来说,深度学习目前的生机要归功于 廉价存储、互联设备以及数字化经济带来的海量数据集。
总结
- 模型容量需要匹配数据负复杂度,否则可能导致过拟合和欠拟合
- 统计机器学习提供数学工具来衡量模型复杂度
- 实际中一般观察训练误差和验证误差
2、多项回归
我们现在可以通过多项式拟合来探索这些概念。
import math
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l
2.1 生成数据集
max_degree = 20 # 多项式的最大阶数
n_train, n_test = 100, 100 # 训练和测试数据集大小
true_w = np.zeros(max_degree) # 分配大量的空间
true_w[0:4] = np.array([5, 1.2, -3.4, 5.6])
features = np.random.normal(size=(n_train + n_test, 1))
np.random.shuffle(features)
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
for i in range(max_degree):
poly_features[:, i] /= math.gamma(i + 1) # gamma(n)=(n-1)!
# labels的维度:(n_train+n_test,)
labels = np.dot(poly_features, true_w)
labels += np.random.normal(scale=0.1, size=labels.shape)
同样,存储在poly_features
中的单项式由gamma函数重新缩放, 其中Γ(n)=(n−1)!。 从生成的数据集中查看一下前2个样本, 第一个值是与偏置相对应的常量特征。
# NumPy ndarray转换为tensor
true_w, features, poly_features, labels = [torch.tensor(x, dtype=
torch.float32) for x in [true_w, features, poly_features, labels]]
features[:2], poly_features[:2, :], labels[:2]
运行结果展示:
(tensor([[ 1.2913],
[-0.7288]]),
tensor([[ 1.0000e+00, 1.2913e+00, 8.3372e-01, 3.5886e-01, 1.1585e-01,
2.9919e-02, 6.4390e-03, 1.1878e-03, 1.9172e-04, 2.7508e-05,
3.5521e-06, 4.1698e-07, 4.4870e-08, 4.4570e-09, 4.1109e-10,
3.5389e-11, 2.8561e-12, 2.1695e-13, 1.5563e-14, 1.0577e-15],
[ 1.0000e+00, -7.2878e-01, 2.6556e-01, -6.4513e-02, 1.1754e-02,
-1.7132e-03, 2.0809e-04, -2.1665e-05, 1.9736e-06, -1.5982e-07,
1.1647e-08, -7.7166e-10, 4.6865e-11, -2.6272e-12, 1.3676e-13,
-6.6447e-15, 3.0266e-16, -1.2975e-17, 5.2533e-19, -2.0150e-20]]),
tensor([5.8820, 2.9533]))
2.2 对模型进行训练和测试
首先定义一个函数评估模型在给定数据集上的损失
def evaluate_loss(net, data_iter, loss): #@save
"""评估给定数据集上模型的损失"""
metric = d2l.Accumulator(2) # 损失的总和,样本数量
for X, y in data_iter:
out = net(X)
y = y.reshape(out.shape)
l = loss(out, y)
metric.add(l.sum(), l.numel())
return metric[0] / metric[1]
定义训练函数:
def train(train_features, test_features, train_labels, test_labels,
num_epochs=400):
loss = nn.MSELoss(reduction='none')
input_shape = train_features.shape[-1]
# 不设置偏置,因为我们已经在多项式中实现了它
net = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
batch_size = min(10, train_labels.shape[0])
train_iter = d2l.load_array((train_features, train_labels.reshape(-1,1)),
batch_size)
test_iter = d2l.load_array((test_features, test_labels.reshape(-1,1)),
batch_size, is_train=False)
trainer = torch.optim.SGD(net.parameters(), lr=0.01)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', yscale='log',
xlim=[1, num_epochs], ylim=[1e-3, 1e2],
legend=['train', 'test'])
for epoch in range(num_epochs):
d2l.train_epoch_ch3(net, train_iter, loss, trainer)
if epoch == 0 or (epoch + 1) % 20 == 0:
animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss),
evaluate_loss(net, test_iter, loss)))
print('weight:', net[0].weight.data.numpy())
2.3 三阶多项式函数拟合(正常)
我们将首先使用三阶多项式函数,它与数据生成函数的阶数相同。 结果表明,该模型能有效降低训练损失和测试损失。 学习到的模型参数也接近真实值w=[5,1.2,−3.4,5.6]。
# 从多项式特征中选择前4个维度,即1,x,x^2/2!,x^3/3!
train(poly_features[:n_train, :4], poly_features[n_train:, :4],
labels[:n_train], labels[n_train:])
效果展示:
2.4 线性函数拟合(欠拟合)
让我们再看看线性函数拟合,从下图可以看出减少该模型的训练损失相对困难。 在最后一个迭代周期完成后,训练损失仍然很高。 当用来拟合非线性模式(如这里的三阶多项式函数)时,线性模型容易欠拟合。
# 从多项式特征中选择前2个维度,即1和x
train(poly_features[:n_train, :2], poly_features[n_train:, :2],
labels[:n_train], labels[n_train:])
效果展示:
2.5 高阶多项式函数拟合(过拟合)
现在,让我们尝试使用一个阶数过高的多项式来训练模型。 在这种情况下,没有足够的数据用于学到高阶系数应该具有接近于零的值。 因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。 虽然训练损失可以有效地降低,但测试损失仍然很高。 结果表明,复杂模型对数据造成了过拟合。
# 从多项式特征中选取所有维度
train(poly_features[:n_train, :], poly_features[n_train:, :],
labels[:n_train], labels[n_train:], num_epochs=1500)
效果展示:
在接下来的章节中,我们将继续讨论过拟合问题和处理这些问题的方法,例如权重衰减和dropout。
05 权重衰减
**权重衰减:**防拟合的常用方式,在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为L2正则化。
**正则化 Regularize:**机器学习中经常会在损失函数中加入正则项,称之为正则化。
1、额外补充几个概念:
- 归一化 Normalization:简而言之,归一化的目的就是方便数据处理,使得预处理的数据被限定在一定的范围内(比如[0,1]或者[-1,1]),从而消除奇异样本数据导致的不良影响。
- 标准化 Standardization:数据的标准化是将数据按比例缩放,使之落入一个小的特定区间。z-score标准化,即零-均值标准化(常用方法),y=(x-μ)/σ
2、正则化、归一化、标准化各自的要点:
- 正则化:先验知识。正则化而是利用先验知识,在处理过程中引入正则化因子(regulator),增加引导约束的作用,比如在逻辑回归中使用正则化,可有效降低过拟合的现象;
- 归一化:消除不同数据之间的量纲。归一化是为了消除不同数据之间的量纲,方便数据比较和共同处理,比如在神经网络中,归一化可以加快训练网络的收敛性;
- 标准化:方便数据的下一步处理。标准化是为了方便数据的下一步处理,而进行的数据缩放等变换,并不是为了方便与其他数据一同处理或比较,比如数据经过零-均值标准化后,更利于使用标准正态分布的性质,进行处理。
1、权重衰减
1.1 使用均方范数作为硬性限制/柔性限制
更常用的是柔性限制:
演示:
1.2 参数更新法则
总结
- 权重衰退通过L2正则项使得模型参数不会过大,从而控制模型复杂度
- 正则项权重是控制模型复杂度的超参数
下面来看看代码实现。
2、高维线性回归
通过一个简单的例子演示权重衰减
%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)
3、从零开始实现
下面我们将从头开始实现权重衰减,只需将L2的平方惩罚添加到原始目标函数中。
3.1 初始化模型参数
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]
3.2 定义L2范数惩罚
实现这一惩罚最方便的方法是对所有项求平方后并将它们求和。
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
3.3 定义训练代码实现
下面的代码将模型拟合训练数据集,并在测试数据集上进行评估。 从 3节以来,线性网络和平方损失没有变化, 所以我们通过d2l.linreg
和d2l.squared_loss
导入它们。 唯一的变化是损失现在包括了惩罚项。
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())
3.4 忽略正则化直接训练
我们现在用lambd = 0
禁用权重衰减后运行这个代码。 注意,这里训练误差有了减少,但测试误差没有减少, 这意味着出现了严重的过拟合。
train(lambd=0)
效果展示:
3.5 使用权重衰减
我们使用权重衰减来运行代码。 注意,在这里训练误差增大,但测试误差减小。 这正是我们期望从正则化中得到的效果。
train(lambd=3)
效果展示:
4、简洁实现
由于权重衰减在神经网络优化中很常用, 深度学习框架为了便于我们使用权重衰减, 将权重衰减集成到优化算法中,以便与任何损失函数结合使用。
在下面的代码中,我们在实例化优化器时直接通过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)
效果展示:
5、小结
- 正则化是处理过拟合的常用方法:在训练集的损失函数中加入惩罚项,以降低学习到的模型的复杂度。
- 保持模型简单的一个特别的选择是使用L2惩罚的权重衰减。这会导致学习算法更新步骤中的权重衰减。
- 权重衰减功能在深度学习框架的优化器中提供。
- 在同一训练代码实现中,不同的参数集可以有不同的更新行为。
06 丢弃法(Dropout)
1、Dropout
Dropout 出现的动机:
一个好的模型需要对输入数据的扰动鲁棒
- 1995年,克里斯托弗·毕晓普证明了 具有输入噪声的训练等价于Tikhonov正则化 [Bishop, 1995]。 这项工作用数学证实了“要求函数光滑”和“要求函数对输入的随机噪声具有适应性”之间的联系。
- 丢弃法:在层之间加入噪音
训练中的Dropout:
- 通常将Dropout作用在隐藏全连接层的输出上
推理中的Dropout:
- 推理中不使用Dropout,直接返回输入
实践中的Dropout:
总结
- Dropout奖一些输出项随机置0来控制模型复杂度
- 常作用在多层感知机的隐藏层输出上
- 丢弃概率是控制模型复杂度的超参数
2、代码实现
2.1、从零开始实现
要实现单层的暂退法函数, 我们从均匀分布U[0,1]中抽取样本,样本数与这层神经网络的维度一致。 然后我们保留那些对应样本大于p的节点,把剩下的丢弃。
在下面的代码中,我们实现 dropout_layer
函数, 该函数以dropout
的概率丢弃张量输入X
中的元素, 如上所述重新缩放剩余部分:将剩余部分除以1.0-dropout
。
import torch
from torch import nn
from d2l import torch as d2l
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)
我们可以通过下面几个例子来测试dropout_layer
函数。 我们将输入X
通过暂退法操作,暂退概率分别为0、0.5和1。
X= torch.arange(16, dtype = torch.float32).reshape((2, 8))
print(X)
print(dropout_layer(X, 0.))
print(dropout_layer(X, 0.5))
print(dropout_layer(X, 1.))
运行结果:
tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 8., 9., 10., 11., 12., 13., 14., 15.]])
tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 8., 9., 10., 11., 12., 13., 14., 15.]])
tensor([[ 0., 0., 4., 0., 8., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 26., 28., 30.]])
tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.]])
2.1.1 定义模型参数
我们定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元。
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
2.1.2 定义模型
我们可以将暂退法应用于每个隐藏层的输出(在激活函数之后), 并且可以为每一层分别设置暂退概率: 常见的技巧是在靠近输入层的地方设置较低的暂退概率。
- 下面的模型将第一个和第二个隐藏层的暂退概率分别设置为0.2和0.5, 并且暂退法只在训练期间有效。
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)
2.1.3 训练和测试
num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
效果展示:
2.2 简洁实现
对于深度学习框架的高级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)
效果展示:
3、小结
- 暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元。
- 暂退法可以避免过拟合,它通常与控制权重向量的维数和大小结合使用的。
- 暂退法将活性值h替换为具有期望值h的随机变量。
- 暂退法仅在训练期间使用。
07 前向计算、反向传播和计算图
我们已经学习了如何用小批量随机梯度下降训练模型。 然而当实现该算法时,我们只考虑了通过前向传播(forward propagation)所涉及的计算。 在计算梯度时,我们只调用了深度学习框架提供的反向传播函数,而不知其所以然。
梯度的自动计算(自动微分)大大简化了深度学习算法的实现。 在自动微分之前,即使是对复杂模型的微小调整也需要手工重新计算复杂的导数, 学术论文也不得不分配大量页面来推导更新规则。
在本节中,我们将通过一些基本的数学和计算图, 深入探讨反向传播的细节。 首先,我们将重点放在带权重衰减(L2正则化)的单隐藏层多层感知机上。
1、前向计算
前向传播(forward propagation或forward pass) 指的是:按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。
2、前向传播计算图
计算图(computational graph)是一种有向无环图(directed acyclic graph,DAG)。计算图用节点表示变量,用有向边(directed edge)表示计算。有向边的目的节点称为子节点,源节点称为父节点,计算图定义如何用父节点计算子节点。
绘制计算图有助于我们可视化计算中操作符和变量的依赖关系。 图4.7.1 是与上述简单网络相对应的计算图, 其中正方形表示变量,圆圈表示操作符。 左下角表示输入,右上角表示输出。 注意显示数据流的箭头方向主要是向右和向上的。
3、反向传播
反向传播(backward propagation或backpropagation)指的是计算神经网络参数梯度的方法。 简言之,该方法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。 该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。
4、训练神经网络
在训练神经网络时,前向传播和反向传播相互依赖。
- 对于前向传播,我们沿着依赖的方向遍历计算图并计算其路径上的所有变量。
- 然后将这些用于反向传播,其中计算顺序与计算图的相反。
以上述简单网络为例:一方面,在前向传播期间计算正则项 (4.7.5)取决于模型参数W(1)和 W(2)的当前值。 它们是由优化算法根据最近迭代的反向传播给出的。 另一方面,反向传播期间参数 (4.7.11)的梯度计算, 取决于由前向传播给出的隐藏变量h的当前值。
因此,在训练神经网络时,在初始化模型参数后, 我们交替使用前向传播和反向传播,利用反向传播给出的梯度来更新模型参数。 注意,反向传播重复利用前向传播中存储的中间值,以避免重复计算。 带来的影响之一是我们需要保留中间值,直到反向传播完成。 这也是训练比单纯的预测需要更多的内存(显存)的原因之一。 此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。 因此,使用更大的批量来训练更深层次的网络更容易导致内存不足(out of memory)错误。
5、小结
- 前向传播在神经网络定义的计算图中按顺序计算和存储中间变量,它的顺序是从输入层到输出层。
- 反向传播按相反的顺序(从输出层到输入层)计算和存储神经网络的中间变量和参数的梯度。
- 在训练深度学习模型时,前向传播和反向传播是相互依赖的。
- 训练比预测需要更多的内存。
08 数值稳定性和模型初始化
1、梯度爆炸与梯度消失
不稳定梯度带来的风险不止在于数值表示; 不稳定梯度也威胁到我们优化算法的稳定性。
我们遇到的问题分两类:
- 梯度爆炸(gradient exploding)问题: 参数更新过大,破坏了模型的稳定收敛;
- 度消失(gradient vanishing)问题: 参数更新过小,在每次更新时几乎不会移动,导致模型无法学习。
1.1、梯度消失
sigmoid函数常导致梯度消失问题,下面我们看一下原因:
%matplotlib inline
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函数的输入很大或是很小时,它的梯度都会消失。 此外,当反向传播通过许多层时,除非我们在刚刚好的地方, 这些地方sigmoid函数的输入接近于零,否则整个乘积的梯度可能会消失。
当我们的网络有很多层时,除非我们很小心,否则在某一层可能会切断梯度。 事实上,这个问题曾经困扰着深度网络的训练。 因此,更稳定的ReLU系列函数已经成为从业者的默认选择(虽然在神经科学的角度看起来不太合理)。
1.2、梯度爆炸
为了更好的说明梯度爆炸问题,们生成100个高斯随机矩阵,并将它们与某个初始矩阵相乘。 对于我们选择的尺度(方差σ2=1),矩阵乘积发生爆炸。 当这种情况是由于深度网络的初始化所导致时,我们没有机会让梯度下降优化器收敛。
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([[ 0.6131, -1.1533, -0.9395, 2.2195],
[-0.1078, 0.1556, 0.7314, 1.4778],
[ 1.0574, 0.1852, -1.8686, 0.0204],
[ 0.3815, 0.2866, -1.8963, -0.6128]])
乘以100个矩阵后
tensor([[ 1.0932e+23, -3.8850e+21, -9.4527e+22, -2.6858e+23],
[-3.3857e+23, 1.2032e+22, 2.9277e+23, 8.3183e+23],
[ 6.8809e+23, -2.4454e+22, -5.9500e+23, -1.6906e+24],
[ 7.0203e+23, -2.4949e+22, -6.0705e+23, -1.7248e+24]])
1.3、打破对称性
2、参数初始化
解决(或至少减轻)上述问题的一种方法是进行参数初始化, 优化期间的注意和适当的正则化也可以进一步提高稳定性。
pass