当前位置: 首页 > news >正文

多层感知机(深度学习-李沐-学习笔记)

多层感知机

隐藏层

线性模型可能会出错

对于深度神经网络,我们使用观测数据来联合学习隐藏层表示和应用于该表示的线性预测器。

在网络中加入隐藏层

可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。把前L-1层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP。 

一个单隐藏层的多层感知机,具有5个隐藏单元:

这个多层感知机有4个输入3个输出,其隐藏层包含5个隐藏单元。 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 因此,这个多层感知机中的层数为2。 注意,这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。

从线性到非线性

注意在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。

隐藏单元由输入的仿射函数给出, 而输出(softmax操作前)只是隐藏单元的仿射函数。 仿射函数的仿射函数本身就是仿射函数, 但是我们之前的线性模型已经能够表示任何仿射函数。

为了发挥多层架构的潜力, 我们还需要一个额外的关键要素: 在仿射变换之后对每个隐藏单元应用非线性的激活函数(activation function)。 激活函数的输出被称为活性值(activations)。 一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型

本节应用于隐藏层的激活函数通常不仅按行操作,也按元素操作。 这意味着在计算每一层的线性部分之后,我们可以计算每个活性值, 而不需要查看其他隐藏单元所取的值。对于大多数激活函数都是这样。

通用近似定理

多层感知机可以通过隐藏神经元,捕捉到输入之间复杂的相互作用, 这些神经元依赖于每个输入的值。在一对输入上进行基本逻辑操作,多层感知机是通用近似器。事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。

激活函数

激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。大多数激活函数都是非线性的。 

ReLU函数

最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU, 因为它实现简单,同时在各种预测任务中表现良好。 ReLU提供了一种非常简单的非线性变换

给定元素x,ReLU函数被定义为该元素与0的最大值。

ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。激活函数是分段线性的。

当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。 在此时,我们默认使用左侧的导数,即当输入为0时导数为0。

使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。 

ReLU函数有许多变体,包括参数化ReLU(Parameterized ReLU,pReLU 函数。 该变体为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过。

sigmoid函数

对于一个定义域在R中的输入, sigmoid函数将输入变换为区间(0, 1)上的输出。 因此,sigmoid通常称为挤压函数(squashing function): 它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值。

阈值单元在其输入低于某个阈值时取值0,当输入超过阈值时取值1。

sigmoid函数是一个自然的选择,因为它是一个平滑的、可微的阈值单元近似

输出视作二元分类问题的概率时, sigmoid仍然被广泛用作输出单元上的激活函数 (sigmoid可以视为softmax的特例)。

当输入接近0时,sigmoid函数接近线性变换

当输入为0时,sigmoid函数的导数达最大值0.25; 输入在任一方向上越远离0点,导数越接近0。

tanh函数

与sigmoid函数类似, tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。

输入在0附近时,tanh函数接近线性变换。 函数的形状类似于sigmoid函数, 不同的是tanh函数关于坐标系原点中心对称。

当输入接近0时,tanh函数的导数接近最大值1。 与我们在sigmoid函数图像中看到的类似, 输入在任一方向上越远离0点,导数越接近0。

小结

  • 多层感知机在输出层和输入层之间增加一个或多个全连接隐藏层,并通过激活函数转换隐藏层的输出。

  • 常用的激活函数包括ReLU函数、sigmoid函数和tanh函数。

多层感知机的从零开始实现

import torch
from torch import nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from d2l import torch as d2l# batch_size = 256
# train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# d2l 1.0.3 版本的 load_data_fashion_mnist 自身默认用 '../data' 目录,这个目录没权限,所以报错。def load_data_fashion_mnist(batch_size, root='e:/temp/data'):transform = transforms.ToTensor()train_dataset = datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)test_dataset = datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)train_iter = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)test_iter = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)return train_iter, test_iterbatch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)# 实现一个具有单隐藏层的多层感知机
# 784个输入特征、10个类、256个隐藏单元
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]def relu(X):a = torch.zeros_like(X)return torch.max(X, a)# 使用reshape将每个二维图像转换为一个长度为num_inputs的向量
def net(X):X = X.reshape((-1, num_inputs))H = relu(X@W1 + b1)  # 这里“@”代表矩阵乘法return (H@W2 + b2)# 直接使用高级API中的内置函数来计算softmax和交叉熵损失
loss = nn.CrossEntropyLoss(reduction='none')# 替代 train_ch3
def train(net, train_iter, test_iter, loss, num_epochs, updater):animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):net.train()metric = d2l.Accumulator(3)for X, y in train_iter:y_hat = net(X)l = loss(y_hat, y).sum()updater.zero_grad()l.backward()updater.step()metric.add(float(l), d2l.accuracy(y_hat, y), y.numel())test_acc = d2l.evaluate_accuracy(net, test_iter)animator.add(epoch + 1, (metric[0] / metric[2],metric[1] / metric[2],test_acc))print(f'loss {metric[0] / metric[2]:.3f}, train acc {metric[1] / metric[2]:.3f}, 'f'test acc {test_acc:.3f}')# 替代 predict_ch3
def predict(net, test_iter, n=6):for X, y in test_iter:breaknet.eval()y_hat = net(X).argmax(axis=1)true_labels = d2l.get_fashion_mnist_labels(y)pred_labels = d2l.get_fashion_mnist_labels(y_hat)titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]d2l.show_images(X[:n].reshape((n, 28, 28)).cpu(), 1, n, titles=titles[:n])# 训练:将迭代周期数设置为10,并将学习率设置为0.1
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
train(net, train_iter, test_iter, loss, num_epochs, updater)# 显示预测图像
predict(net, test_iter)

多层感知机的简洁实现

import torch
from torch import nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from d2l import torch as d2ldef load_data_fashion_mnists(batch_size, root='e:/temp/data'):transform = transforms.ToTensor()train_dataset = datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)test_dataset = datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)train_iter = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)test_iter = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)return train_iter, test_iter# 2个全连接层: 第一层是隐藏层,它包含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);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 = load_data_fashion_mnists(batch_size)# 替代 train_ch3
def train(net, train_iter, test_iter, loss, num_epochs, updater):animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):net.train()metric = d2l.Accumulator(3)for X, y in train_iter:y_hat = net(X)l = loss(y_hat, y).sum()updater.zero_grad()l.backward()updater.step()metric.add(float(l), d2l.accuracy(y_hat, y), y.numel())test_acc = d2l.evaluate_accuracy_gpu(net, test_iter)animator.add(epoch + 1, (metric[0] / metric[2],metric[1] / metric[2],test_acc))print(f'loss {metric[0] / metric[2]:.3f}, train acc {metric[1] / metric[2]:.3f}, 'f'test acc {test_acc:.3f}')train(net, train_iter, test_iter, loss, num_epochs, trainer)

模型选择、欠拟合和过拟合

将模型在训练数据上拟合的比在潜在分布中更接近的现象称为过拟合(overfitting), 用于对抗过拟合的技术称为正则化(regularization)

在实验中调整模型架构或超参数时会发现: 如果有足够多的神经元、层数和训练迭代周期, 模型最终可以在训练集上达到完美的精度,此时测试集的准确性却下降了。

训练误差和泛化误差

训练误差(training error)是指, 模型在训练数据集上计算得到的误差。 泛化误差(generalization error)是指, 模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。

在实际中,我们只能通过将模型应用于一个独立的测试集来估计泛化误差, 该测试集由随机选取的、未曾在训练集中出现的数据样本构成。

统计学习理论

当我们训练模型时,我们试图找到一个能够尽可能拟合训练数据的函数。 但是如果它执行地“太好了”,而不能对看不见的数据做到很好泛化,就会导致过拟合。

模型复杂性

通常对于神经网络,我们认为需要更多训练迭代的模型比较复杂, 而需要早停(early stopping)的模型(即较少训练迭代周期)就不那么复杂。

影响模型泛化的因素。

  1. 可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。

  2. 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。

  3. 训练样本的数量。即使模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。

模型选择

在机器学习中,我们通常在评估几个候选模型后选择最终的模型。 这个过程叫做模型选择

为了确定候选模型中的最佳模型,我们通常会使用验证集

验证集

原则上,在我们确定所有的超参数之前,我们不希望用到测试集。

除了训练和测试数据集之外,还增加一个验证数据集(validation dataset), 也叫验证集(validation set)。

实际上是在使用应该被正确地称为训练数据和验证数据的数据集, 并没有真正的测试数据集。 因此,书中每次实验报告的准确度都是验证集准确度,而不是测试集准确度。

K折交叉验证

当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。

解决方案是采用K折交叉验证。 这里,原始训练数据被分成个K不重叠的子集。 然后执行K次模型训练和验证,每次在K-1个子集上进行训练, 并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。 最后,通过对K次实验的结果取平均来估计训练和验证误差。

欠拟合还是过拟合

训练误差和验证误差都很严重, 但它们之间仅有一点差距。 如果模型不能降低训练误差,这可能意味着模型过于简单(即表达能力不足), 无法捕获试图学习的模式。此外,由于我们的训练和验证误差之间的泛化误差很小, 我们有理由相信可以用一个更复杂的模型降低训练误差。 这种现象被称为欠拟合(underfitting)

当我们的训练误差明显低于验证误差时要小心, 这表明严重的过拟合(overfitting)

模型复杂性

数据集大小

训练数据集中的样本越少,我们就越有可能(且更严重地)过拟合。 随着训练数据量的增加,泛化误差通常会减小。 

对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。

多项式回归

import math
import numpy as np
import torch
import matplotlib.pyplot as plt
from torch import nn
from d2l import torch as d2lmax_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)# NumPy ndarray转换为tensor
true_w, features, poly_features, labels = [torch.tensor(x, dtype=torch.float32) for x in [true_w, features, poly_features, labels]]print(features[:2], poly_features[:2, :], labels[: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]# 新版推荐写法:自己写train_epoch函数
def train_epoch(net, train_iter, loss, updater):net.train()metric = d2l.Accumulator(2)  # total loss, number of samplesfor X, y in train_iter:y_hat = net(X)l = loss(y_hat, y).mean()  # 解决非标量 backward 报错updater.zero_grad()l.backward()updater.step()metric.add(float(l) * len(y), len(y))  # 使用样本数量平均损失return metric[0] / metric[1]def train(train_features, test_features, train_labels, test_labels,num_epochs=400):loss = nn.MSELoss()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 实例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):train_loss = train_epoch(net, train_iter, loss, trainer)if epoch == 0 or (epoch + 1) % 20 == 0:test_loss = evaluate_loss(net, test_iter, loss)animator.add(epoch + 1, (train_loss, test_loss))print('weight:', net[0].weight.data.numpy())plt.show()  # ⭐ 显示每一个动画图像# 从多项式特征中选择前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:])# 从多项式特征中选取所有维度
train(poly_features[:n_train, :], poly_features[n_train:, :],labels[:n_train], labels[n_train:], num_epochs=1500)

小结

  • 欠拟合是指模型无法继续减少训练误差。过拟合是指训练误差远小于验证误差。

  • 由于不能基于训练误差来估计泛化误差,因此简单地最小化训练误差并不一定意味着泛化误差的减小。机器学习模型需要注意防止过拟合,即防止泛化误差过大。

  • 验证集可以用于模型选择,但不能过于随意地使用它。

  • 我们应该选择一个复杂度适当的模型,避免使用数量不足的训练样本。

权重衰减

实际上,限制特征的数量是缓解过拟合的一种常用技术。 

多项式对多变量数据的自然扩展称为单项式(monomials), 也可以说是变量幂的乘积。

单项式的阶数是幂的和。 

在训练参数化机器学习模型时, 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为L2正则化

为什么我们首先使用L2范数,而不是L1范数。 事实上,这个选择在整个统计领域中都是有效的和受欢迎的。 L2正则化线性模型构成经典的岭回归(ridge regression)算法, 正则化线性回归是统计学中类似的基本模型, 通常被称为套索回归(lasso regression)。 使用L2范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。 这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。 

相比之下,L1惩罚会导致模型将权重集中在一小部分特征上, 而将其他权重清除为零。 这称为特征选择(feature selection)

高维线性回归

通过一个简单的例子来演示权重衰减

import torch
from torch import nn
from d2l import torch as d2l
import matplotlib.pyplot as plt# 标签同时被均值为0,标准差为0.01高斯噪声破坏
# 问题的维数增加到d=200,并使用一个只包含20个样本的小训练集
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)# 初始化模型参数
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, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_lossnum_epochs, lr = 100, 0.003animator = 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())# 忽略正则化直接训练
# 用lambd = 0禁用权重衰减后运行这个代码
# train(lambd=0)
# plt.show()
# 这里训练误差有了减少,但测试误差没有减少, 出现了严重的过拟合# 使用权重衰减
# train(lambd=3)
# plt.show()
# 在这里训练误差增大,但测试误差减小# 简洁实现-----# 实例化优化器时直接通过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)
# plt.show()train_concise(3)
plt.show()

小结

  • 正则化是处理过拟合的常用方法:在训练集的损失函数中加入惩罚项,以降低学习到的模型的复杂度。

  • 保持模型简单的一个特别的选择是使用惩罚的权重衰减。这会导致学习算法更新步骤中的权重衰减。

  • 权重衰减功能在深度学习框架的优化器中提供。

  • 在同一训练代码实现中,不同的参数集可以有不同的更新行为。

暂退法(Dropout)

通过惩罚权重的L2范数来正则化统计模型的经典方法。

我们希望模型深度挖掘特征,即将其权重分散到许多特征中, 而不是过于依赖少数潜在的虚假关联。

重新审视过拟合

简单地说,线性模型没有考虑到特征之间的交互作用。 对于每个特征,线性模型必须指定正的或负的权重,而忽略其他特征。

泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡(bias-variance tradeoff)。

线性模型有很高的偏差:它们只能表示一小类函数。 然而,这些模型的方差很低:它们在不同的随机数据样本上可以得出相似的结果

 与线性模型不同,神经网络并不局限于单独查看每个特征,而是学习特征之间的交互

扰动的稳健性

我们期待“好”的预测模型能在未知的数据上有很好的表现: 经典泛化理论认为,为了缩小训练和测试性能之间的差距,应该以简单的模型为目标。

简单性的另一个角度是平滑性,即函数不应该对其输入的微小变化敏感。 

在训练过程中,他们建议在计算后续层之前向网络的每一层注入噪声。 因为当训练一个有多层的深层网络时,注入噪声只会在输入-输出映射上增强平滑性

这个想法被称为暂退法(dropout)

暂退法在前向传播过程中,计算每一内部层的同时注入噪声,这已经成为训练神经网络的常用技术。 这种方法之所以被称为暂退法,因为我们从表面上看是在训练过程中丢弃(drop out)一些神经元。 在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些节点置零

关键的挑战就是如何注入这种噪声。 一种想法是以一种无偏向(unbiased)的方式注入噪声。 这样在固定住其他层时,每一层的期望值等于没有噪音时的值。

实践中的暂退法

图中带有1个隐藏层和5个隐藏单元的多层感知机。 当我们将暂退法应用到隐藏层,以p的概率将隐藏单元置为零时, 结果可以看作一个只包含原始神经元子集的网络。 比如在图中,删除了h2和h5, 因此输出的计算不再依赖于h2或h5,并且它们各自的梯度在执行反向传播时也会消失。 这样,输出层的计算不能过度依赖于h1,h2,h3,h4,h5的任何一个元素。

从零开始实现和简洁实现

import torch
from torch import nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from d2l import torch as d2l
import matplotlib.pyplot as pltdef load_data_fashion_mnist(batch_size, root='e:/temp/data'):transform = transforms.ToTensor()train_dataset = datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)test_dataset = datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)train_iter = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)test_iter = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)return train_iter, test_iterdef dropout_layer(X, dropout):assert 0 <= dropout <= 1# 在本情况中,所有元素都被丢弃if dropout == 1:return torch.zeros_like(X)# 在本情况中,所有元素都被保留if dropout == 0:return Xmask = (torch.rand(X.shape) > dropout).float()return mask * X / (1.0 - dropout)# 输入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.))# 定义模型参数
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256# 定义模型
dropout1, dropout2 = 0.2, 0.5class 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_inputsself.training = is_trainingself.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))))# 只有在训练模型时才使用dropoutif 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 outnet = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)def train(net, train_iter, test_iter, loss, num_epochs, updater):"""训练函数"""device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')net.to(device)print("training on", device)animator = d2l.Animator(xlabel='epoch', ylabel='loss',legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):net.training = True  # 训练模式,启用dropoutmetric = d2l.Accumulator(3)  # 训练损失总和,正确预测数,总预测数for X, y in train_iter:X, y = X.to(device), y.to(device)updater.zero_grad()y_hat = net(X)l = loss(y_hat, y).mean()l.backward()updater.step()metric.add(float(l) * y.numel(), d2l.accuracy(y_hat, y), y.numel())train_loss = metric[0] / metric[2]train_acc = metric[1] / metric[2]net.training = False  # 测试模式,关闭dropouttest_acc = d2l.evaluate_accuracy_gpu(net, test_iter)animator.add(epoch + 1, (train_loss, train_acc, test_acc))print(f'最终训练准确率: {train_acc:.3f}, 测试准确率: {test_acc:.3f}')# 训练和测试
num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
train(net, train_iter, test_iter, loss, num_epochs, trainer)
plt.show()# 简洁实现---
# 只需在每个全连接层之后添加一个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)
train(net, train_iter, test_iter, loss, num_epochs, trainer)
plt.show()

小结

  • 暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元

  • 暂退法可以避免过拟合,它通常与控制权重向量的维数和大小结合使用的。

  • 暂退法将活性值替换为具有期望值的随机变量

  • 暂退法仅在训练期间使用。

前向传播、反向传播和计算图

深入探讨反向传播 的细节。 首先,我们将重点放在带权重衰减(L2正则化)的单隐藏层多层感知机上。

前向传播(forward propagation或forward pass) 指的是:按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。

其中正方形表示变量,圆圈表示操作符。 左下角表示输入,右上角表示输出。 注意显示数据流的箭头方向主要是向右和向上的。

 反向传播

反向传播(backward propagation或backpropagation)指的是计算神经网络参数梯度的方法。 简言之,该方法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。 该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。

为此,我们应用链式法则,依次计算每个中间变量和参数的梯度。 计算的顺序与前向传播中执行的顺序相反,因为我们需要从计算图的结果开始,并朝着参数的方向努力。

训练神经网络

在训练神经网络时,前向传播和反向传播相互依赖。 对于前向传播,我们沿着依赖的方向遍历计算图并计算其路径上的所有变量。 然后将这些用于反向传播,其中计算顺序与计算图的相反。

以上述简单网络为例:一方面,在前向传播期间计算正则项取决于模型参数W(1)和W(2)的当前值。 它们是由优化算法根据最近迭代的反向传播给出的。 另一方面,反向传播期间参数 的梯度计算, 取决于由前向传播给出的隐藏变量h的当前值。

因此,在训练神经网络时,在初始化模型参数后, 我们交替使用前向传播和反向传播,利用反向传播给出的梯度来更新模型参数。 注意,反向传播重复利用前向传播中存储的中间值,以避免重复计算。 带来的影响之一是我们需要保留中间值,直到反向传播完成。 这也是训练比单纯的预测需要更多的内存(显存)的原因之一。 此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。 因此,使用更大的批量来训练更深层次的网络更容易导致内存不足(out of memory)错误。

因此,在训练神经网络时,在初始化模型参数后, 我们交替使用前向传播和反向传播,利用反向传播给出的梯度来更新模型参数。 注意,反向传播重复利用前向传播中存储的中间值,以避免重复计算。 带来的影响之一是我们需要保留中间值,直到反向传播完成。 这也是训练比单纯的预测需要更多的内存(显存)的原因之一。 此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。 因此,使用更大的批量来训练更深层次的网络更容易导致内存不足(out of memory)错误。

数值稳定性和模型初始化

import torch
from d2l import torch as d2l
import matplotlib.pyplot as plt # 梯度消失
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))
# plt.show()# 梯度爆炸
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)
  • 梯度消失和梯度爆炸是深度网络中常见的问题。在参数初始化时需要非常小心,以确保梯度和参数可以得到很好的控制。

  • 需要用启发式的初始化方法来确保初始梯度既不太大也不太小。

  • ReLU激活函数缓解了梯度消失问题,这样可以加速收敛。

  • 随机初始化是保证在进行优化前打破对称性的关键。

  • Xavier初始化表明,对于每一层,输出的方差不受输入数量的影响,任何梯度的方差不受输出数量的影响

环境和分布偏移

分布偏移的类型

假设训练数据是从某个分布 pS(x,y) 中采样的, 但是测试数据将包含从不同分布 pT(x,y) 中抽取的未标记样本。 一个清醒的现实是:如果没有任何关于pS和pT之间相互关系的假设, 学习到一个分类器是不可能的。

协变量偏移

虽然输入的分布可能随时间而改变, 但标签函数(即条件分布P(y,x))没有改变。 统计学家称之为协变量偏移(covariate shift), 因为这个问题是由于协变量(特征)分布的变化而产生的。 

标签偏移

标签偏移(label shift)描述了与协变量偏移相反的问题。 这里我们假设标签边缘概率P(y)可以改变, 但是类别条件分布P(x|y)在不同的领域之间保持不变。 当我们认为导致时,标签偏移是一个合理的假设。

实战Kaggle比赛:预测房价

import hashlib
import os
import tarfile
import zipfile
import requests
import numpy as np
import pandas as pd
import torch
import matplotlib.pyplot as plt 
from torch import nn
from d2l import torch as d2l#@save
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'def download(name, cache_dir=os.path.join('..', 'data')):  #@save"""下载一个DATA_HUB中的文件,返回本地文件名"""assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"url, sha1_hash = DATA_HUB[name]os.makedirs(cache_dir, exist_ok=True)fname = os.path.join(cache_dir, url.split('/')[-1])if os.path.exists(fname):sha1 = hashlib.sha1()with open(fname, 'rb') as f:while True:data = f.read(1048576)if not data:breaksha1.update(data)if sha1.hexdigest() == sha1_hash:return fname  # 命中缓存print(f'正在从{url}下载{fname}...')r = requests.get(url, stream=True, verify=True)with open(fname, 'wb') as f:f.write(r.content)return fnamedef download_extract(name, folder=None):  #@save"""下载并解压zip/tar文件"""fname = download(name)base_dir = os.path.dirname(fname)data_dir, ext = os.path.splitext(fname)if ext == '.zip':fp = zipfile.ZipFile(fname, 'r')elif ext in ('.tar', '.gz'):fp = tarfile.open(fname, 'r')else:assert False, '只有zip/tar文件可以被解压缩'fp.extractall(base_dir)return os.path.join(base_dir, folder) if folder else data_dirdef download_all():  #@save"""下载DATA_HUB中的所有文件"""for name in DATA_HUB:download(name)# 使用上面定义的脚本下载并缓存Kaggle房屋数据集
DATA_HUB['kaggle_house_train'] = (  #@saveDATA_URL + 'kaggle_house_pred_train.csv','585e9cc93e70b39160e7921475f9bcd7d31219ce')DATA_HUB['kaggle_house_test'] = (  #@saveDATA_URL + 'kaggle_house_pred_test.csv','fa19780a7b011d9b009e8bff8e99922a8ee2eb90')# 使用pandas分别加载包含训练数据和测试数据的两个CSV文件
train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))# 训练数据集包括1460个样本,每个样本80个特征和1个标签
# 而测试数据集包含1459个样本,每个样本80个特征
print(train_data.shape)
print(test_data.shape)# 看看前四个和最后两个特征,以及相应标签(房价)
print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]])# 在每个样本中,第一个特征是ID, 这有助于模型识别每个训练样本
# 虽然这很方便,但它不携带任何用于预测的信息
# 因此,在将数据提供给模型之前,将其从数据集中删除
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))# 数据预处理
# 将所有缺失的值替换为相应特征的平均值
# 若无法获得测试数据,则可根据训练数据计算均值和标准差
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(lambda x: (x - x.mean()) / (x.std()))
# 在标准化数据之后,所有均值消失,因此我们可以将缺失值设置为0
all_features[numeric_features] = all_features[numeric_features].fillna(0)# 处理离散值,用独热编码替换它们
# “Dummy_na=True”将“na”(缺失值)视为有效的特征值,并为其创建指示符特征
all_features = pd.get_dummies(all_features, dummy_na=True)
all_features.shape# 此转换会将特征的总数量从79个增加到331个
# 从pandas格式中提取NumPy格式,并将其转换为张量表示用于训练
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32)
train_labels = torch.tensor(train_data.SalePrice.values.reshape(-1, 1), dtype=torch.float32)# 训练
# 训练一个带有损失平方的线性模型
# 线性模型将作为基线(baseline)模型, 让我们直观地知道最好的模型有超出简单的模型多少
loss = nn.MSELoss()
in_features = train_features.shape[1]def get_net():net = nn.Sequential(nn.Linear(in_features,1))return netdef log_rmse(net, features, labels):# 为了在取对数时进一步稳定该值,将小于1的值设置为1clipped_preds = torch.clamp(net(features), 1, float('inf'))rmse = torch.sqrt(loss(torch.log(clipped_preds),torch.log(labels)))return rmse.item()# 训练函数将借助Adam优化器(Adam优化器的主要吸引力在于它对初始学习率不那么敏感)
def train(net, train_features, train_labels, test_features, test_labels,num_epochs, learning_rate, weight_decay, batch_size):train_ls, test_ls = [], []train_iter = d2l.load_array((train_features, train_labels), batch_size)# 这里使用的是Adam优化算法optimizer = torch.optim.Adam(net.parameters(),lr = learning_rate,weight_decay = weight_decay)for epoch in range(num_epochs):for X, y in train_iter:optimizer.zero_grad()l = loss(net(X), y)l.backward()optimizer.step()train_ls.append(log_rmse(net, train_features, train_labels))if test_labels is not None:test_ls.append(log_rmse(net, test_features, test_labels))return train_ls, test_ls# K折交叉验证
def get_k_fold_data(k, i, X, y):assert k > 1fold_size = X.shape[0] // kX_train, y_train = None, Nonefor j in range(k):idx = slice(j * fold_size, (j + 1) * fold_size)X_part, y_part = X[idx, :], y[idx]if j == i:X_valid, y_valid = X_part, y_partelif X_train is None:X_train, y_train = X_part, y_partelse:X_train = torch.cat([X_train, X_part], 0)y_train = torch.cat([y_train, y_part], 0)return X_train, y_train, X_valid, y_valid# 在K折交叉验证中训练K次后,返回训练和验证误差的平均值
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay,batch_size):train_l_sum, valid_l_sum = 0, 0for i in range(k):data = get_k_fold_data(k, i, X_train, y_train)net = get_net()train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,weight_decay, batch_size)train_l_sum += train_ls[-1]valid_l_sum += valid_ls[-1]if i == 0:d2l.plot(list(range(1, num_epochs + 1)), [train_ls, valid_ls],xlabel='epoch', ylabel='rmse', xlim=[1, num_epochs],legend=['train', 'valid'], yscale='log')print(f'折{i + 1},训练log rmse{float(train_ls[-1]):f}, 'f'验证log rmse{float(valid_ls[-1]):f}')return train_l_sum / k, valid_l_sum / k# 模型选择
# 有了足够大的数据集和合理设置的超参数,
# K折交叉验证往往对多次测试具有相当的稳定性
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr,weight_decay, batch_size)
print(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, 'f'平均验证log rmse: {float(valid_l):f}')# 训练和预测
def train_and_pred(train_features, test_features, train_labels, test_data,num_epochs, lr, weight_decay, batch_size):net = get_net()train_ls, _ = train(net, train_features, train_labels, None, None,num_epochs, lr, weight_decay, batch_size)d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',ylabel='log rmse', xlim=[1, num_epochs], yscale='log')print(f'训练log rmse:{float(train_ls[-1]):f}')# 将网络应用于测试集。preds = net(test_features).detach().numpy()# 将其重新格式化以导出到Kaggletest_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)submission.to_csv('submission.csv', index=False)train_and_pred(train_features, test_features, train_labels, test_data,num_epochs, lr, weight_decay, batch_size) 

小结

  • 真实数据通常混合了不同的数据类型,需要进行预处理

  • 常用的预处理方法:将实值数据重新缩放为零均值和单位方法用均值替换缺失值

  • 类别特征转化为指标特征,可以使我们把这个特征当作一个独热向量来对待。

  • 我们可以使用K折交叉验证来选择模型并调整超参数

  • 对数对于相对误差很有用

(1)零均值和单位方差(zero mean and unit variance)

这是标准化(standardization)的一种方式。

  • 零均值(mean = 0):将数据每一维的均值变为0。

  • 单位方差(variance = 1):将数据每一维的方差变为1。

处理方式:

其中 μ 是该特征的均值,σ 是标准差。

作用:

  • 让所有特征尺度一致,避免某些数值大的特征主导模型。

  • 对梯度下降类算法更友好,收敛更快。

(2)类别特征和指标特征(categorical features vs indicator features)

  • 类别特征(categorical features):表示某种分类信息的特征,例如:

    • 性别:男/女

    • 城市:北京/上海/广州

  • 指标特征(indicator features):通常指数值型的特征,比如年龄、身高、收入等

(3)独热向量(one-hot vector)

  • 是把类别特征转换成数值形式的一种方法。

  • 举个例子,假设“城市”这个特征有三个类别:

    • 北京 → [1, 0, 0]

    • 上海 → [0, 1, 0]

    • 广州 → [0, 0, 1]

作用:

  • 使模型能理解“类别”这个离散概念,不引入大小顺序误解。

  • 可用于线性模型、神经网络等。

(4)K折交叉验证(K-fold cross-validation)

  • 是一种评估模型性能和选择超参数的稳健方法。

  • 把训练集分成K份(folds),每次取其中1份做验证,剩下K-1份做训练,重复K次。

  • 最后将K次验证结果求平均,作为模型整体表现。

优点:

  • 充分利用数据:每个样本都有训练和验证的机会。

  • 能避免“过拟合”或“欠拟合”带来的单一验证偏差。

  • 可以帮助我们找到效果最优的模型配置和超参数

http://www.lryc.cn/news/599391.html

相关文章:

  • Oracle 的单体安装
  • SQLite中SQL的解析执行:Lemon与VDBE的作用解析
  • 扒网站工具 HTTrack Website Copier
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘streamlit’问题
  • 【SpringAI实战】实现仿DeepSeek页面对话机器人(支持多模态上传)
  • GPU 服务器ecc报错处理
  • yolov8通道级剪枝讲解(超详细思考版)
  • linux修改用户名和主目录及权限-linux029
  • vue2用elementUI做单选下拉树
  • 激光频率梳 3D 轮廓检测在深凹槽检测的应用有哪些
  • AI-调查研究-38-多模态大模型量化 主流视觉语言任务的量化评估策略分析
  • 在kdb+x中使用SQL
  • Python高效操作Kafka实战指南
  • 专为小靶面工业相机的抗振微距镜头
  • C++ string:准 STL Container
  • Java线程基础面试复习笔记
  • 相机ROI 参数
  • 力扣-32.最长有效括号
  • Python(32)Python内置函数全解析:30个核心函数的语法、案例与最佳实践
  • 188.买卖股票的最佳时机IV 309.最佳买卖股票时机含冷冻期 714.买卖股票的最佳时机含手续费
  • 《C++初阶之STL》【vector容器:详解 + 实现】
  • Python应用append()方法向列表末尾添加元素
  • 深入解析HBase如何保证强一致性:WAL日志与MVCC机制
  • selenium 元素定位
  • 【unitrix】 6.15 “非零非负一“的整数类型(NonZeroNonMinusOne)特质(non_zero_non_minus_one.rs)
  • XCTF-crypto-幂数加密
  • Docker 实战大纲
  • Windows Installer安全深度剖析
  • SQL基础⑭ | 变量、流程控制与游标篇
  • 解放生产力:Amazon API Gateway 与 Amazon Lambda 的优雅组合