不到10年时间,人工智能革命已从研究实验室席卷至广阔的工业界,并触及我们日常生活的方方面面。
线性回归
线性模型
一个实际的线性模型如下:(经典买房)
模型假设:
- 假设1:影响房价的关键因素是卧室个数、卫生间个数和居住面积,分别记为:、、。
- 假设2:成交价是关键因素的加权和:
更广泛的定义:
- 给定n维输入:
- 线性模型有一个n维权重和一个标量偏差:
- 输出是输入的加权和:
- 向量版本(简化表达):
表示向量 和 的 内积 ,在欧几里得空间中它等同于点积 。
线性模型可以看做是单层神经网络:

- 输入的维度为:d
- 输出的维度为:1
- 每个箭头表示一个权重w。
衡量预估质量
比较真实值和预估值,例如房屋售价和估价。
假设y是真实值, 是估计值。
一个常见的损失为:(平方损失)
训练数据
收集一些数据点来决定参数值(权重和偏差),例如过去6个月卖的房子。这些数据就称为训练数据。
训练数据通常越多越好。
假设我们有n个样本记为:
参数学习
根据求所有估计值的损失平均值,我们得到损失函数:

计算目标:找到一个w,b的对应值,最小化损失函数:
:星号通常表示 最优值 ,即让损失函数达到最小的参数组合。
基础优化方法
梯度下降
步骤:
-
挑选一个随机初始值
-
重复迭代参数t = 1, 2, 3
- 沿梯度方向将增加损失函数值
- 学习率:步长的超参数
超参数:需要人为设置的参数。
小批量随机梯度下降
我们很少直接使用梯度下降法,因为在整个训练集上计算梯度代价很大。一个深度神经网络模型可能需要数分钟至数小时才能计算一次梯度。
我们可以随机采样b个样本来近似损失:
b是批量大小,也是一个重要的超参数,
批量大小的设置准则:
- 不能太小:每次计算量太小,不适合并行来最大利用计算资源。
- 不能太大:内存消耗增加,浪费计算。
小批量随机梯度下降是深度学习框架默认的求解算法
线性回归的从零实现
导入包:
%matplotlib inline #将绘图结果直接嵌入到 Notebook 的单元格下方,而不是弹出一个独立的窗口。
import random
import torch
from d2l import torch as d2l
生成数据集
为了简单起见,我们将根据带有噪声的线性模型构造一个人造数据集。人造数据集的一个好处是我们知道真实的参数w和b。
我们使用线性模型参数和噪声项生成数据集以及标签:
生成数据集函数:
def synthetic_data(w, b, num_examples): #@save
"""生成y=Xw+b+噪声"""
# torch.normal()产生服从标准正太分布(均值为0,方差为1)的随机数。
# 形状为(样本数量num_examples,特征维度len(w))
X = torch.normal(0, 1, (num_examples, len(w)))
# torch.matmul()计算X * w,然后加上偏置b
y = torch.matmul(X, w) + b
# 添加一个服从正太分布(0, 0,01)的随机噪声
y += torch.normal(0, 0.01, y.shape)
# 返回特征X和处理后的y
# 确保y.reshpe((-1, 1))是一个列向量
return X, y.reshape((-1, 1))
torch.matmul():是一个高度优化的、全能型的乘法函数。
- 处理一维向量 :如果两个输入都是一维的,它计算的是 点积 。
- 处理二维矩阵 :行为与
torch.mm一致。- 处理高维张量(广播机制) :如果输入维度大于 2(例如 Batch 数据 ),它会执行 批处理矩阵乘法 。它会保持前面的 Batch 维度不变,只对最后两个维度进行矩阵乘法。
生成数据:


其中第二个特征 features[: , 1] 和 labels的散点图,可以直观观察到两者之间的线性关系:

读取数据集
训练模型时需要对数据集进行遍历,每次抽取一小批样本,并使用它们来更新我们的模型。由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。
我们定义一个 data_iter函数,该函数接受批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含一组特征和标签:
def data_iter(batch_size, features, labels):
# 获取样本的总数,此处是1000
num_examples = len(features)
# 生成一个从0 到 num_example-1的索引列表
indices = list(range(num_examples))
# 这些样本是随机读取的,没有特定的顺序
# 将索引顺序打乱
random.shuffle(indices)
# 开始循环,步长为batch_size
# 从0开始,每次跳过batch_size个样本,直到遍历完整个数据集
for i in range(0, num_examples, batch_size):
# 提取当前批次的索引
# 使用min(i + batch_size, num_examples)防止最后一个批次超出总数
# 将切片的索引转换成张量,以便后序进行高级索引操作
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
# 使用yield返回当前的特征和标签
# 这使得函数变成一个生成器,每次调用时只返回一个小批量,节省内存
yield features[batch_indices], labels[batch_indices]
当我们运行迭代时,我们会连续地获得不同的小批量,直至遍历完整个数据集。 上面实现的迭代对教学来说很好,但它的执行效率很低,可能会在实际问题上陷入麻烦。 例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多, 它可以处理存储在文件中的数据和数据流提供的数据。
初始化模型参数
我们将w随机初始化为满足(0, 0.01)正态分布的向量
将b初始化为0标量。

定义模型
作用:将模型的输入和参数同模型的输出关联起来。
线性模型定义:
def linreg(X, w, b): #@save
"""线性回归模型"""
return torch.matmul(X, w) + b
定义损失函数
损失函数我们使用均方损失:
def squared_loss(y_hat, y): #@save
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
我们将真实值
y的形状转为和预测y_hat相同的形状。
定义优化算法
优化算法我们选择使用小批量随机梯度下降法。
下面的函数实现小批量随机梯度下降更新。该函数接受模型参数集合、学习速率和批量大小作为输入。
def sgd(params, lr, batch_size): #@save
"""小批量随机梯度下降"""
# 开启一个上下文管理器,在该区域内不进行梯度计算和记录
# 更新参数的操作不属于模型学习的一部分,不需要计算更新过程本身的梯度,这可以节省内存和计算资源
with torch.no_grad():
# 遍历参数列表中的每一个参数(如权重矩阵w和偏置向量b)
for param in params:
# 根据梯度下降公式更新此参数
# 除以batch_size是为了取平均梯度,使其不随批量大小的改变而产生剧烈的波动
param -= lr * param.grad / batch_size
#手动将梯度归0
param.grad.zero_()
为什么要除以batch_size?
在《动手学深度学习》的实现中,损失函数(Loss)是直接求和(
l.sum())而不是取平均。这意味着:
- 如果
batch_size是 10,param.grad就是 10 个样本梯度的总和。- 如果
batch_size是 100,param.grad就是 100 个样本梯度的总和。 为了保证无论batch_size选多大,我们每一步更新的“力度”是基本稳定的,我们需要除以batch_size来获得 平均梯度 。
训练
在每次迭代中,
- 我们取一小批量训练样本,并通过我们的模型来获得一组预测。
- 计算损失
- 反向传播,存储每个参数的梯度
- 调用优化算法sgd来更新模型参数
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的小批量损失
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward() # 这里对应上面sgd中除以batch_size
sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
训练效果:

线性回归的简介实现
使用深度学习框架来简洁地实现线性回归模型。
生成数据集
生成数据集没有什么不同,与之前从0实现完全一致,不过我们额外导入了一些包:
import numpy as np
import torch
from torch.utils import data # 用于定义数据集、迭代数据、高效加载数据
from d2l import torch as d2l
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
我们可以使用框架中现有的API来读取数据。
def load_array(data_arrays, batch_size, is_train=True): #@save
"""构造一个PyTorch数据迭代器"""
# 将传入的多个数组(张量)封装进TensorDataset
# *data_arrays 表示对元组进行解包,等价于data.TensorDataset(features, labels)
dataset = data.TensorDataset(*data_arrays)
# 构造Pytorch 的DataLoader,用于按批次读取数据
# dataset: 刚才创建的数据集对象
# batch_size: 每次读取多少条数据
# shuffle: 为True则打乱数据顺序,增加模型的泛化能力
# 返回一个可迭代对象
return data.DataLoader(dataset, batch_size, shuffle=is_train)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
我们可以尝试通过迭代器获取数据第一项:等价于 next(iter(data_iter))

定义模型
对于标准的深度学习模型,我们可以使用框架的预定义好的层。这使我们只需要关注使用哪些层来构造模型,而不必关注层的实现细节。
# 从pytorch库中导入神经网络模块,并简写为nn
from torch import nn
# 定义一个名为net的网络
# nn.Sequential 是一个顺序容器,它会将放入其中的层按顺序串联起来
# nn.Linear(2,1) 定义了一个线性层(全连接层)
# - 第一个参数 2:输入特征的维度(即输入的 X 有 2 列)
# - 第二个参数 1:输出特征的维度(即输出的 y 有 1 列)
net = nn.Sequential(nn.Linear(2,1))
Sequential类将多个层串联到一起。当给定输入数据时,Sequential实例将数据传入第一层,然后将第一层的输出作为第二层的输入,以此类推。
在上面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential。但是由于以后的模型都是多层的,这里使用Sequential会让我们熟悉标准流水线。
初始化模型参数
在使用net之前,我们需要初始化模型参数。在这里为线性模型中的权重和偏置。
# 访问网络中的第一层(索引为0),获取其权重参数(weight)
# .data 表示直接操作张量数据
# .normal_(0.0.01)是一个原地操作(以_结尾)
# 将权重初始化为均值为0、标准差为0.01的正太分布随机数
net[0].weight.data.normal_(0, 0.01)
# 访问网络中的第一层0,获取其偏置参数(bias)
# .fill_(0) 是一个原地操作,将偏置的所有元素初始化为0
net[0].bias.data.fill_(0)
定义损失函数
计算均方误差使用MSELoss类,也被称为平方范数。默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()
定义优化算法
小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在 optim模块中实现了该算法的许多变种。
当我们实例化SGD实例时,我们需要指定优化的参数以及优化算法所需要的超参数字典。
# 定义一个优化器(optimizer),这里命名为trainer
# torch.optm.SGD: 选择随机梯度下降算法(Stochastic Gradient Descent)算法
# net.parameters(): 告诉优化器需要更新模型中的哪些参数
# lr = 0.03: 设置学习率(learn rate)为0.03
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
训练
在每个迭代周期里,我们将完整遍历一次数据集(train_data), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:
- 通过net(X)生成预测并计算损失l(前向传播)
- 通过反向传播来计算梯度
- 通过调用优化器来更新模型参数

softmax回归
softmax回归虽然名字叫做回归,但其实是一个分类问题。
回归:
- 单连续数值输出
- 自然区间R
- 跟真实值的区别作为损失

分类:
- 通常多个输出
- 输出i是预测为第i类的置信度
- 网络架构:

从回归到多类分类
多分类问题我们需要对类别进行编码。常见的编码方式有一位有效编码:
如果真实的类别为第i个,则,其他类别均为0
使用均方损失训练
最大值最为预测:
训练目标:需要更置信的识别正确类
softmax
神经网络直接计算出来的输出可以是任意实数(有正有负,有大有小),很难直接解释为概率。
为了解决,引入Softmax函数:
作用:
- 非负:通过指数函数 ,将所有输出变成正数。
- 和为1:通过除以所有项的总和 ,对数据进行归一化,使得所有类别的预测概率加起来等于 1。
其中概率和的区别作为损失。
交叉熵
交叉熵是信息论中的概念:用于衡量两个概率分布的相似度:
公式:
- (真实分布): 代表事实。例如这张图是猫, 就是 (猫的概率是1)。
- (预测分布): 代表模型的猜测。例如模型觉得是猫的概率是 0.7, 就是 。
直观理解: 如果预测 () 越接近真实 (),这个交叉熵的值就越小;反之越大。
损失函数
我们通过计算信息熵作为损失函数:
假设正确答案是第 类,那么只有 ,其他的 全是 0。
训练模型时,我们只需要关注 模型对“正确类别”预测了多大的概率 。如果正确类别的预测概率越高(接近1), 值就越小(损失越小)。
梯度
其梯度就是真实概率和预测概率的区别:
常见损失函数
L2 Loss

最小化 MSE(平方),你在预测“平均值”(Mean)。此处不再证明推导。俺数学也不好T_T
均方损失MSE的数学解释
基础假设:世界是有“噪声”的。现实中的数据从来不是完美的直线。真实数据等于理想的预测值加上一个”噪声”
均方损失MSE中,我们假设这个噪声是服从正态分布(高斯分布)的。正太分布概率密度函数如下:
- :随机变量的具体取值。
- :这个分布的 均值 (也就是钟形曲线最高点对应的中心位置)。
- :表示“当前值”距离“中心”有多远。
概率密度函数用于描述连续性随机变量的分布情况。
对于离散变量(比如掷骰子),我们可以说“掷出 6 的概率是 1/6”;但对于连续变量(比如一个人的精确身高),由于取值有无限种可能, 取到某一个精确数值(如刚好 175.0000…cm)的概率在数学上是 0 。
概率密度本身不是概率,但它在某个区间上的积分才是概率。如果你想知道随机变量 落在区间 内的概率,只需要对 PDF 求积分。
函数图像一般如下:

现在回到线性回归的公式:
其中噪声 。
- 是一个 确定值 (一旦 给定,预测出的直线上的点就是固定的)。
- 是一个 随机波动 (它是以 0 为中心的)。
如果你把一个“固定值”加上一个“以 0 为中心的随机波动”,得到的结果 会是什么分布? 答案是: 也会服从正态分布,只是中心变了。
- 的均值(中心 ) = (也就是预测值)。
- 的方差(宽窄 ) = 噪声的方差 。
所以,我们可以得出结论:
因此我们可以写出通过给定的观测到特定的似然 (likelihood):
接下来,在线性归回预测中,我们通常要找到模型参数( 和 ),让这堆点出现在这条线周围的“可能性”最大。在数学上,我们将这一过程称为极大似然估计。
极大似然估计:既然我们已经看到了这一组数据( 和 ),那么我们要找的那个模型参数( 和 ),应该是让这组数据出现的概率最大的那个。
逻辑:寻找一个参数,使得实验结果出现的可能性(似然度)达到最大。
似然函数:
于是我们需要找到 和 的最优值是使整个数据集的似然函数最大的值:
为了计算这个“最大概率”,推导过程做了一系列数学变形:
- 连乘变连加: 因为所有样本的概率是要乘起来的,乘法很难算,所以科学家取了对数(log),把乘法变成了加法。
- 最大变最小: 我们原本想让概率(似然) 最大化 。但是数学上通常习惯求 最小值 (比如损失函数)。所以,给公式加了个负号,把“求最大”变成了“求最小负对数似然”。
为了计算这个“最大概率”,推导过程做了一系列数学变形:
- 连乘变连加: 因为所有样本的概率是要乘起来的,乘法很难算,所以科学家取了对数(log),把乘法变成了加法。
- 最大变最小: 我们原本想让概率(似然) 最大化 。但是数学上通常习惯求 最小值 (比如损失函数)。所以,给公式加了个负号,把“求最大”变成了“求最小负对数似然”。
因此可以得到数学公式:
现在我们只需要假设是某个固定常数就可以忽略第一项, 因为第一项不依赖于和。 现在第二项除了常数外,其余部分和前面介绍的均方误差是一样的。 幸运的是,上面式子的解并不依赖于。 因此,在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。
L1 Loss

L1 Loss:最小化 MAE(绝对值),你在预测“中位数”(Median)。
Huber’s Robust Loss

图像分类数据集
这里介绍Fashion-MNIST数据集,作为我们后续softmax回归而使用的数据集。
Fashion-MNIST 数据集 :
- 包含 10 类服装图像(如恤、裤子、裙子等)。
- 每张图像是 像素的灰度图。
- 训练集共有 60,000 个样本,测试集共有 10,000 个样本。
读取数据集
# 实例化一个ToTensor转换对象
# 它的作用是将PTL图片或numpy.ndarray 转换为形状为(C,H,W)的浮点张量
# 同时会自动将像素值从[0,255] 缩放到[0,1]范围内
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", #指定数据集下载或存储的相对路径
train=True, # 指定加载训练集
transform=trans, # 应用上面定义的trans转换操作,将图像转换为Tensor
download=True # 如果没有数据集则自动下载
)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data",
train=False,
transform=trans,
download=True
)
transforms.ToTensor():不仅改变数据类型,还进行了归一化处理。归一化:它的目的是将不同量纲(单位)或取值范围的数据,转换到同一个特定的区间内(通常是 或 )。
- 加速模型收敛
- 防止大数吃小数,特征权重的平衡
- 避免数值计算问题
我们来看一下Tensor的形状:

数据集由灰度图像组成,其通道数为1。
图像的通道:图像中某一类信息的独立数据层。你可以把一张图像想象成由多个透明的“图层”叠加而成,每个图层记录一种特定的信息,例如红色、绿色、蓝色、透明度等。
灰度图(1通道):
- 只有一个通道,记录亮度。
- 每个像素一个值:0(黑) ~ 255(白)
RGB图像(3通道):
- R 红
- G 绿
- B 蓝
读取小批量
我们使用内置的 data.DataLoader()读取数据。
batch_size = 256
def get_dataloader_workers(): #@save
"""使用4个进程来读取数据"""
return 4
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers())
遍历一遍数据的时间:

遍历一遍数据的时间为4.28秒。
有时,读取训练数据会成为训练瓶颈。一般要求数据读取速度大于训练速度。
我们可以汇总上述代码,写出一个完整的数据导入函数:
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
softmax回归的从零开始实现
首先导入包以及数据集:
import torch
from IPython import display
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化参数模型
和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。原始数据集中的每个样本都是28 * 28 的图像。 本节将展平每个图像,把它们看作长度为784(28的平方)的向量。
在softmax回归中:
- 输出与类别一样多,即有10个类别
- 输入维度为784
因此,权重将构成一个的矩阵,偏置将构成一个的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重W,偏置b初始化为0。
num_inputs = 784
num_outputs = 10
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
定义softmax操作
softmax表达式:
步骤:
- 对每个项求幂
- 对每一行求和(小批量中每一行是一个样本),得到每个样本的规范化常数。
- 将每一行除以其规范化常数,确保结果的和为1。
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制
定义模型
定义softmax操作后,我们可以实现softmax回归模型。下面的代码定义了输入如何通过网络映射到输出。
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
将数据传递到模型之前,我们使用reshape函数将每张原始图像展平为向量。
定义损失函数
回归问题的损失函数使用:交叉熵损失函数。
假设我们有一个数据样本y_hat,其中包含2个样本在3个类别的预测概率,以及它们对应的标签y。
y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
有了y,我们知道在第一个样本中,第一类是正确的预测;而在第二个样本中,第三类是正确的预测。
接下来我们要获取每个样本中对于正确预测的概率,我们可以使用for,循环,
# 使用 for 循环实现
results = []
for i in range(len(y)):
# i 是样本索引(0, 1)
# label 是该样本的真实类别索引(0, 2)
label = y[i]
# 从 y_hat 的第 i 行提取第 label 列的值
prob = y_hat[i, label]
results.append(prob)
# 将结果转回 Tensor 格式
result_tensor = torch.tensor(results)
print(result_tensor) # 输出: tensor([0.1000, 0.5000])
高级索引
然而for循环往往是低效的,我们可以使用高级索引,直接得到:
y_hat[[0, 1], y]
这种写法别称为整数数组索引,它是高级索引中的一种。
- 核心逻辑:提供每一维度的索引列表,来精确”挑出”张量中的特定元素。
确定行索引号:使用第一个列表[0,1]
确定列索引号:使用的是第二个列表y,即 [0.2]
一一对应配对:
- 第一对坐标:
(行索引[0], 列索引[0])(0, 0) - 第二对坐标:
(行索引[1], 列索引[1])(1, 2)
最后,它会从 y_hat 中取出这些坐标的值,并组成一个新的张量:
[y_hat[0, 0], y_hat[1, 2]]
现在,我们只需一行代码集合实现交叉熵损失函数:
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
分类精度
给定预测分布y_hat,当我们必须输出硬预测时,我们通常选择预测概率最高的类。
如Gmail必须将电子邮件分类为
- “Primary(主要邮件)”
- “Social(社交邮件)”
- “Updates(更新邮件)”
- “Forums(论坛邮件)”。
Gmail做分类时可能在内部估计概率,但最终它必须在类中选择一个。当预测与标签分类y一致时,即是正确的。
分类精度即正确预测数量与总预测数量之比。虽然直接优化精度可能很困难(因为精度的计算不可导),但是精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。
def accuracy(y_hat, y): #@save
"""计算预测正确的数量"""
# 检查 y_hat是否为二维矩阵
# 如果维度大于1且列数大于1,说明y_hat存储的是预测分数/概率
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
# 获取一行中最大值所在的索引,即预测的类别标签
y_hat = y_hat.argmax(axis=1)
# 将y_hat的数据类型转换为与真实标签y一致,并进行逐元素比较
# cmp是一个布尔类型的Tensor, 预测正确为True,错误为False
cmp = y_hat.type(y.dtype) == y
# 将布尔张量转换为与y相同类型的数字(True = 1, False = 0)
# 求和得到预测正确的总数,最后转化为float返回
return float(cmp.type(y.dtype).sum())
然后我们计算得到分类精度:
accuracy(y_hat, y) / len(y)
同样,对于任意数据迭代器 data_iter可访问的数据集, 我们可以评估在任意模型 net的精度。
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
# 初始化一个累加器Accumulator,用于存储2个变量
# metric[0]:预测正确的样本总数
# metric[1]:总样本数
metric = Accumulator(2)
# 禁用梯度计算,减少内存消耗并加快计算速度
with torch.no_grad():
# 遍历数据迭代器中的每一个Batch
for X, y in data_iter:
# net(X) 是模型的前向传播预测结果
# accuracy(...) 计算当前批次中样本预测正确的数量
# metric.add 将这两个值累加到累加器中
metric.add(accuracy(net(X), y), y.numel())
# 计算并返回总精度:预测正确数/样本总数
return metric[0] / metric[1]
其中Accumulator为一个自定义累加器,用于对多个变量进行累加。
class Accumulator: #@save """在n个变量上累加""" def __init__(self, n): # 初始化一个包含 n 个浮点数的列表,全部设为 0.0 # 每个位置对应一个你想要追踪的指标(如:损失、正确数、总样本数) self.data = [0.0] * n def add(self, *args): # 接收任意数量的参数,并将它们分别加到对应的 self.data 插槽中 # 例如:metric.add(a, b) 会执行 self.data[0] += a, self.data[1] += b self.data = [a + float(b) for a, b in zip(self.data, args)] def reset(self): # 重置所有累加器为 0.0 self.data = [0.0] * len(self.data) def __getitem__(self, idx): # 允许通过索引访问累加的数据,例如 metric[0] return self.data[idx]
训练
首先我们定义一个训练函数来代表一个迭代周期:
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期"""
# 初始化累加器,准备追踪3个变量:
# metric[0]:训练损失总和
# metric[1]:预测正确的样本数
# metric[2]:样本总数
metric = Accumulator(3)
# 遍历训练数据集的每一个Batch
for X, y in train_iter:
# 1. 前向传播:计算预测值y_hat
y_hat = net(X)
# 2. 计算损失:比较预测值 y_hat和真实标签 y
l = loss(y_hat, y)
# 3. 反向传播,更新参数
# 对损失综总和求导,并在更新是除以批量大小
l.sum().backward()
updater(X.shape[0])
# 4. 统计数据:将当前batch的损失总和、正确数和样本数累加到metric
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
其中updater函数与线性回归中的实现一致:
lr = 0.1 def updater(batch_size): return d2l.sgd([W, b], lr, batch_size)
然后我们可以实现一个训练函数:
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型"""
# 初始化动画绘制器 Animator,用于在训练过程中实时绘制损失和准确率曲线
# 不是与模型训练过程无关,先不关注,但在下面给出实现
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
# 按照指定的(num_epochs)进行循环训练
for epoch in range(num_epochs):
# 训练一个epch
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
# 评估模型,在测试级上计算准确率
test_acc = evaluate_accuracy(net, test_iter)
# 更新图表
animator.add(epoch + 1, train_metrics + (test_acc,))
# 训练结束后,获取最后一个epoch的训练指标
train_loss, train_acc = train_metrics
# --- 以下是代码检查(断言),确保训练结果符合预期,若不满足则报错 ---
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc
Animator:
class Animator: #@save """在动画中绘制数据""" def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)): # 增量地绘制多条线 if legend is None: legend = [] d2l.use_svg_display() self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: self.axes = [self.axes, ] # 使用lambda函数捕获参数 self.config_axes = lambda: d2l.set_axes( self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): # 向图表中添加多个数据点 if not hasattr(y, "__len__"): y = [y] n = len(y) if not hasattr(x, "__len__"): x = [x] * n if not self.X: self.X = [[] for _ in range(n)] if not self.Y: self.Y = [[] for _ in range(n)] for i, (a, b) in enumerate(zip(x, y)): if a is not None and b is not None: self.X[i].append(a) self.Y[i].append(b) self.axes[0].cla() for x, y, fmt in zip(self.X, self.Y, self.fmts): self.axes[0].plot(x, y, fmt) self.config_axes() display.display(self.fig) display.clear_output(wait=True)
现在我们可以进行10轮的训练:

预测
现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。

softmax的简洁实现
通过深度学习框架的高级API也能更方便地实现softmax回归模型。
首先导入包与数据:
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)
初始化模型参数
softmax回归的输出是一个全连接层。因此,为了实现我们的模型,我们只需在Sequential中添加一个带有10给输出的的全连接层。
全连接层:前一层的所有神经元,都与当前层的所有神经元相连。
数学上的表达:
对于全连接层,每一个输出 都是所有输入 的线性组合。如果有 个输入和 个输出,其计算公式为:
# nn.Flatten():将多维的输入张量(如28x28的图像)展平为一维向量(784维)
# 这样做是因为后面的全连接层nn.Linear只能接受一维特征向量作为输入
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
def init_weights(m):
# 检查当前层m是否是nn.Linear(全连接层)类型
if type(m) == nn.Linear:
# 使用正太分布(均值为0, 标准差为0.01)来初始化该层的权重
nn.init.normal_(m.weight, std=0.01)
# 将init_weights函数递归地应用到net的每一个子层上
# 即使net中有很多层,apply也会自动遍历并对符合条件的层执行初始化逻辑
net.apply(init_weights);
softmax的实现
直接计算softmax函数:
上溢风险
如果输入的某个很大,会爆炸,变成 inf无穷大,导致无法计算。这种情况下我们无法得到一个明确定义的交叉熵值。
解决技巧:减去最大值
为了防止exp爆炸,在计算指数前,先让所有的减去它们中的最大值:
这样最大的指数就变成了 ,所有的值都缩放到 之间,彻底解决了上溢问题。
下溢风险
在减去和规范化步骤之后,可能有些具有较大的负值。由于精度受限将有接近0的值,即下溢。这些值可能会四舍五入为0。使得,并使 = -inf
。反向传播几步后,我们可能会发现自己面对一屏幕可怕的 nan结果。
解决技巧:Log-Sum-Exp技巧
尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。通过将softmax和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题。
如上面的等式所示,我们避免计算,而也可以直接使,因为被抵消了。
loss = nn.CrossEntropyLoss(reduction='none')
当我们使用PyTorch的 CrossEntropyLoss时,我们的模型最后一层不应该再加Softmax激活函数。我们应该直接把未规范化的预测值传递给损失函数,因为内部它会帮我们高效且稳定地完成图片里说的这些复杂计算。
优化算法
在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。 这与我们在线性回归例子中的相同,这说明了优化器的普适性。
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
训练函数
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
# 使用PyTorch内置的优化器和损失函数
# 梯度清零,防止梯度累加
updater.zero_grad()
# 反向传播,计算梯度
l.mean().backward()
# 更新模型参数
updater.step()
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc

bug
在最后的softmax回归的简洁实现中,遇到错误:

修复:
按住ctrl然后点击d2l,进入d2l.torch包中,在包中添加以下代码:
def accuracy(y_hat, y): #@save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step()
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc
def predict_ch3(net, test_iter, n=6): #@save
"""预测标签(定义见第3章)"""
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])