仿射变换:通过加权和对特征进行线性变换,并通过偏置项进行平移
隐藏层
我们可以通过在网络中加入一个或多个隐藏层来突破线性模型的限制,使其能处理更普遍的函数关系类型。最简单的方法是将许多全连接层堆叠在一起。每一层都输出到其上面的层,直到生成最后的输出。我们可以把前L-1层看作表示,把最后一层看作线性预测器。这种架构通常称为多层感知机。
多层感知机可以通过隐藏层中的神经元捕获输入之间复杂的相互作用,这些神经元依赖每个输入的值
激活函数
通过计算加权和并加上偏置项来确定神经元是否应该被激活,它们将输入信号转换为输出的可微运算。大多数激活函数都是非线性的。
模型选择、欠拟合和过拟合
过拟合:将模型在训练数据上拟合的比在潜在分布中更接近的现象
正则化:用于对抗过拟合的技术
训练误差:模型在训练集上计算得到的误差
泛化误差:模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望
影响模型泛化的因素
- 可调整参数的数量:当可调整的参数很大时,模型往往更容易过拟合
- 参数的取值:当权重的取值范围较大时,模型可能更容易过拟合
- 训练样本的数量:即使模型很简单,也很容易过拟合只包含一两个样本的数据集
验证集
我们将数据分为3份,除了训练数据集和测试数据集,再增加一个验证数据集,也称为验证集
K折交叉验证
原始训练数据被分为K个不重叠的子集;然后,执行K次模型训练和验证,然后在K-1个子集上进行训练,并在剩余的一个子集上进行验证;最后,通过对K次实验的结果取平均值来估计训练误差和验证误差
权重衰减
范数与权重衰减
在训练参数化机器学习模型时,权重衰减是使用最广泛的正则化技术之一,通常也被称为L2正则化。
要保证权重向量较小,最常用的方法是将其范数作为惩罚项添加到最小化损失中,将原来的训练目标最小化训练标签上的预测损失,调整为最小化预测损失和惩罚项之和
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
# num_inputs:每个数据点的特征数量;也就是问题的维数
# n_train:训练数据的样本数
n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
# `true_w`: 真实的权重矩阵,大小为`(num_inputs, 1)`,所有元素初始化为0.01
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
# 使用d2l.synthetic_data函数生成训练数据。
# 这个函数接受真实的权重和偏置以及训练样本数作为输入,并返回相应数量的合成数据
train_data = d2l.synthetic_data(true_w, true_b, n_train)
# d2l.load_array函数将训练数据加载为一个迭代器。
# 这个迭代器可以每次返回一个大小为batch_size的小批量数据,以便在训练过程中逐批次地提供数据给模型
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
# is_train=False指的是数据不会被随机打乱,数据增强不会被应用
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`:定义了线性回归模型,输入是`x`,输出是`d2l.linreg(x, w, b)`,即`x`与权重`w`的点积加上偏置`b`
# lambda x:指的是匿名函数和其参数
net, loss = lambda x: d2l.linreg(x, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003
# d2l.Animator创建了一个动画绘制器,用于在训练过程中绘制训练损失和测试损失的变化
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log', xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
# 在一个batch_size上面进行训练
for x, y in train_iter:
# 增加了L2范数惩罚项
# 广播机制使L2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(x), y) + lambd * l2_penalty(w)
# 反向传播:计算损失关于模型参数的梯度
l.sum().backward()
# 使用随机梯度下降(SGD)方法更新模型的权重和偏置
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())
禁用权重衰减
train(lambd=0)
使用权重衰减
train(lambd=3)
暂退法
当面对更多的特征而样本不足时,线性模型往往会过拟合;
当给出更多样本而不是特征时,通常线性模型不会过拟合
- 线性模型泛化的可靠性是有代价的。线性模型没有考虑到特征之间的交互作用
- 泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡
- 深度神经网络与线性模型不同,并不局限于单独查看每个特征,而是学习特征之间的交互
- 平滑性:函数不应该对其输入的微小变化敏感
在训练过程中,在计算后续层之前向网络的每一层注入噪声,因此当训练一个有多层的深度网络时,注入噪声只会在输入-输出映射上增强平滑性
暂退法:在前向传播过程中,计算每一内部层的同时注入噪声
- 从表面上看是在训练过程中丢弃一些神经元
- 标准暂退法包括在计算下一层之前将当前层中的一些节点置0
从零开始实现
# 要实现单层的暂退法函数,我们从均匀分布U[0,1]中抽取样本,样本数与这层神经网络的维度一致
# 保留那些对应样本大于p的节点,把剩下的节点丢弃
import torch
from torch import nn
from d2l import torch as d2l
标准暂退法正则化
# 该函数以dropout的概率丢弃张量输入x中的元素,将剩余部分除以(1.0-dropout)
# Dropout是一种正则化技术,用于防止神经网络过拟合。
# 在训练过程中,它会随机地“关闭”网络中的一部分神经元,这样可以使模型不会过度依赖于任何特定的神经元,从而提高其泛化能力
def dropout_layer(x, dropout):
# 使用assert语句确保dropout的值在0到1之间。如果不是,程序将抛出一个错误
assert 0 <= dropout <= 1
if dropout == 1:
# 如果dropout为1,则返回一个与x形状相同但所有元素都为0的张量。这意味着在这一层中,所有的神经元都被“关闭”了
return torch.zeros_like(x)
if dropout == 0:
# 如果dropout为0,则直接返回原始的输入x,不进行任何Dropout操作
return x
# 创建一个与x形状相同的随机张量,其值在0到1之间。
# 将这个随机张量中的每个值与dropout进行比较,得到一个布尔张量。
# 使用.float()将这个布尔张量转换为浮点数张量,其中True变为1.0,False变为0.0。
# 这样,mask张量中的每个元素都表示是否应该保留对应的x中的元素
mask = (torch.rand(x.shape) > dropout).float()
# 将mask张量与x张量相乘,
# x中对应于mask为0的位置的元素将被置为0(即被“关闭”),而对应于mask为1的位置的元素则保持不变
return mask * x / (1.0 - dropout)
暂退概率为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))
定义模型参数
我们定义具有两个隐藏层的多层感知机,每个隐藏层包含256个隐藏单元
num_inputs, num_outputs, num_hidden1, num_hidden2 = 784, 10, 256, 256
定义模型
我们可以将暂退法应用于每个隐藏层的输出(激活函数之后),可以为每一层分别设置暂退概率:常用的技巧是在靠近输入层的地方设置较低的暂退概率。
暂退法只在训练期间有效
dropout1, dropout2 = 0.2, 0.5
# 定义了一个名为Net的类,它继承了PyTorch的nn.Module类,这意味着Net是一个神经网络模型
class Net(nn.Module):
def __init__(self, num_inputs, num_outputs, num_hidden1, num_hidden2, is_training = True):
# 调用父类nn.Module的初始化函数
super(Net, self).__init__()
# 初始化两个实例变量,分别保存输入特征的数量和训练模式的状态
self.num_inputs = num_inputs
self.training = is_training
# lin1从输入层到第一个隐藏层,lin2从第一个隐藏层到第二个隐藏层,lin3从第二个隐藏层到输出层
self.lin1 = nn.Linear(num_inputs, num_hidden1)
self.lin2 = nn.Linear(num_hidden1, num_hidden2)
self.lin3 = nn.Linear(num_hidden2, num_outputs)
self.relu = nn.ReLU()
# 定义前向传播函数,该函数接受输入x,并返回模型的输出
def forward(self, x):
# 将输入x重塑为二维张量,其中每一行都代表一个样本,列数为num_inputs。
# 然后,通过第一个全连接层lin1进行线性变换,并使用ReLU激活函数进行非线性变换,得到第一隐藏层的输出H1
H1 = self.relu(self.lin1(x.reshape((-1, self.num_inputs))))
# 只有在训练模型时才使用暂退法
if self.training == True:
# 在第一个全连接层之后添加一个暂退层
H1 = dropout_layer(H1, dropout1)
# 将H1通过第二个全连接层lin2进行线性变换,并使用ReLU激活函数得到第二隐藏层的输出H2
H2 = self.relu(self.lin2(H1))
if self.training == True:
# 在第二个全连接层之后添加一个暂退层
H2 = dropout_layer(H2, dropout2)
out = self.lin3(H2)
return out
net = Net(num_inputs, num_outputs, num_hidden1, num_hidden2)
# 训练和测试
# batch_size:每个批次的数据量,即在每次更新时用于计算梯度的样本数
num_epochs, lr, batch_size = 10, 0.5, 256
# reduction='none' 表示不对损失进行任何归约,即返回每个样本的损失值
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# net.parameters() 提取了神经网络 net 的所有参数
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
暂退法的简洁实现
# 输入是形状为[batch_size, 28, 28]的图像数据(代表28x28像素的图像),该层会将其转换为形状为[batch_size, 784]的张量
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
# 在第一个全连接层之后添加一个暂退层
nn.Dropout(dropout1),
nn.Linear(256, 256),
nn.ReLU(),
# 在第二个全连接层之后添加一个暂退层
nn.Dropout(dropout2),
nn.Linear(256, 10))
# 初始化神经网络的权重
def init_weights(m):
if type(m) == nn.Linear:
# 如果m是全连接层,则使用正态分布(均值为0,标准差为0.01)来初始化其权重
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)
前向传播、反向传播和计算图
前向传播
按顺序(从输入层到输出层)计算和存储神经网络中每层的结果
为简单起见,我们假设输入样本是,并且隐藏层不包括偏置项。这里的中间变量是
其中是隐藏层的权重参数。将中间变量
通过激活函数
后,我们得到长度为h的隐藏层激活向量:
隐藏层激活向量h也是一个中间变量,假设输出层的参数只有权重,我们可以得到输出层变量,它是一个长度为q的向量:
假设损失函数为l,样本标签为y,我们可以计算单个数据样本的损失项:
L=l(o,y)
根据L2正则化的定义,给定超参数,正则化项为
其中,矩阵的弗罗贝尼乌斯范数时将矩阵展平为向量后应用的L2范数。最后,模型在给定数据样本上的正则化损失为J=L+s
反向传播
计算神经网络参数梯度的方法。该方法根据微积分中的链式法则,按相反的顺序从输出层到输入层遍历网络。该算法存储了计算某些参数梯度时所需的任何中间向量(偏导数)
训练神经网络
在初始化模型参数后,我们交替使用前向传播和反向传播,利用反向传播给出的梯度来更新模型参数。反向传播重复使用前向传播中存储的中间值,以避免重复计算,其带来的影响之一是我们需要保留中间值,直到反向传播完成。
数值稳定性和模型初始化
我们实现的每个模型都是根据某个预先指定的分布来初始化模型的参数
梯度消失和梯度爆炸
梯度消失
# 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 的值、y 的值和 x 的梯度
d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
legend=['sigmoid', 'gradient'],figsize=(4.5, 2.5))
梯度爆炸
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)