0. Preface
本系列博文是 DataWhale 社区 2023年 3月《动手学深度学习(Pytorch)》组队学习活动的笔记,本篇为系列笔记的第二篇—— 线性回归和Softmax回归。
本文是学习李沐老师 B 站视频教程 动手学深度学习 PyTorch版 所记录的笔记。主要使用 Obsidian
软件并借助插件 Meida extended
插件,在 markdown 文件中生成时间戳,可以在后期温习笔记时,方便地定位到原视频所在位置。
原教程视频如下:
PDF
版本笔记见:D2L Note Chapter 2
本次活动面向的人员:
- 有Python基础
- 有高数,线代,概率论基础
- 本科大二左右,或者研一
学习资源整合
If we open a quarrel between past and present, we shall find that we have lost the future.
— Winston Churchill
1 线性回归
房价预测例子。00:02 线性回归


-
假设一:影响房价的关键因素是卧室个数,卫生间个数和居住面积,记为 , , 。
-
假设二:成交价是关键因素的加权和
权重和偏差的实际值在后文给出。
1.1 线性模型
1.1.1 模型设定

-
线性模型需要确定一个n维权重和一个标量偏差
-
输出 :
向量版本的是
Note: 线性模型可以看作是单层神经网络(图片)


1.1.2 衡量估计质量
-
需要估计模型的预估值和真实值之间的差距,例如房屋售价和股价
-
假设是真实值,是估计值,我们可以比较
这个叫做平方损失
1.1.3 训练数据
-
收集一些数据点来决定参数值(权重 和偏差 ),例如6个月内被卖掉的房子。这被称之为训练数据
-
通常越多越好。
Note:需要注意的是,现实世界的数据都是有限的,但是为了训练出精确的参数往往需要训练数据越多越好,当训练数据不足的时候,我们还需要进行额外处理。
-
假设我们有n个样本,记为
的每一行是一个样本,的每一行是一个输出的实数值。
1.1.4 参数学习
-
训练损失。
训练参数时,需要定义一个损失函数来衡量参数的好坏,应用前文提过的平方损失有公式:
$$\ell(\mathbf{X}, \mathbf{y},\mathbf{w},b)=\frac{1}{2n}\sum_{i=1}n(y_i-<\mathbf{x}_i,\mathbf{w}>-b)2=\frac{1}{2n}||\mathbf{y}-\mathbf{X} \mathbf{w}-b||^2$$ -
最小化损失来学习参数。
训练参数的目的就是使损失函数的值尽可能小(这意味着预估值和真实值更接近)。最后求得的参数值可表示为:
1.1.5 显示解
线性回归有显示解,即可以直接矩阵数学运算,得到参数w和b的最优解,而不是用梯度下降,牛顿法等参数优化方式一点点逼近最优解。
推导过程:
- 为了方便矩阵表示和计算,将偏差加入权重,,
-
损失函数是凸函数,最优解满足导数为0,可解出显示解:
令
有,
解得:
1.1.6 总结
- 线性回归是对 维输入的加权,外加偏差
- 使用平方损失来衡量预测值和真实值之间的误差
- 线性回归有显示解
- 线性回归可以看作单层神经网络
1.2 基础优化算法
1.2.1 梯度下降
当模型没有显示解的时候,应用梯度下降法逼近最优解。梯度下降法的具体步骤:
-
挑选一个初始值 ,
-
重复迭代参数,迭代公式为:
$-\frac{\partial l}{\partial\omega_{t-1}}$ 为函数值下降最快的方向,学习率 $\eta$ 为学习步长。
1.2.2 选择学习率
学习率 为学习步长,代表了沿负梯度方向走了多远,这是超参数(人为指定的的值,不是训练得到的)

1.2.3 小批量随机梯度下降
在实际应用中,很少直接应用梯度下降法,这是因为在整个训练集上计算梯度代价太大,一个深度神经网络模型可能需要数分钟至数小时。
为了减少运算代价,我们可以 随机采样 个样本来近似损失,损失函数为:
\frac{1}{b}\sum_{i\in I_b}l(x_i,y_i,\omega)$$ ,
其中 $b$ 是批量大小 `batch size`,也是超参数。
### 1.2.4 选择批量大小
- $b$ 也不能太大:内存消耗增加;浪费计算资源,一个极端的情况是可能会重复选取很多差不多的样本,浪费计算资源
- $b$ 也不能太小:每次计算量太小,很难以并行,不能最大限度利用 `GPU` 资源
### 1.2.5 总结
- 梯度下降通过不断**沿着负梯度方向**更新参数求解
- 小批量随机梯度下降是深度学习默认的求解算法(简单,稳定)
- **两个重要的超参数:批量大小(batch size),学习率(lr)**
## 1.3 线性回归的从零开始实现
[00:00 代码实现](https://www.bilibili.com/video/BV1PX4y1g7KC?p=3&vd_source=ed04dacb9e3f8845a9f0fa7b8130e4f1&t=0.861988)
在了解线性回归的关键思想之后,可以开始通过代码来动手实现线性回归了。
本节尝试从零开始实现整个方法,
包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。我们将只使用张量和自动求导。
在之后的章节中,会充分利用深度学习框架的优势,介绍更简洁的实现方式。
### 1.3.1 环境配置
1
2
3
4
5!pip install d2l
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19ValueError Traceback (most recent call last)
Input In [1], in <cell line: 4>()
1 #import torch
2 #print(torch.**version**)
----> 4 from d2l import torch as d2l
---> 13 from pandas._libs.interval import Interval
14 from pandas._libs.tslibs import (
15 NaT,
16 NaTType,
(...)
21 iNaT,
22 )
File pandas/_libs/interval.pyx:1, in init pandas._libs.interval()
ValueError: numpy.ndarray size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject1
pip install --force-reinstall pandas
1
2 execute the following at the download location:
sh Miniconda3-py39_23.1.0-1-MacOSX-arm64.sh -b1
2 initiate the shell
conda actiavte ~/miniconda31
~/miniconda3/bin/conda init
1
conda create --name d2l python=3.9 -y
1
conda actiavte d2l
1
pip install --pre torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/nightly/cpu
1
2
3
4import torch
print(torch.backends.mps.is_available())
True1
2
3pip install ipykernel
python -m ipykernel install --user --name ENVNAME --display-name DISP_NAME1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def synthetic_data(w, b, num_examples): #@save
"""
生成y=Xw+b+噪声
@para w 权重
@para b 偏差
@para num_examples 样本数量
@return
X 随机生成的特征数据,(num_examples, len(w))
y X对应的标签 (num_examples,1)
"""
X = torch.normal(0, 1, (num_examples, len(w)))#生成均值为0,方差为1,数据维度是(num_examples, len(w))的随机数据作为训练样本
y = torch.matmul(X, w) + b #生成X对应的预测值y
y += torch.normal(0, 0.01, y.shape)# 加入噪音,加入的是均值为0,方差为0.01,纬度和y.shape一致的噪音进行干扰
return X, y.reshape((-1, 1))#返回X,y,y为列向量1
2
3true_w = torch.tensor([2, -3.4]) #真实权重
true_b = 4.2 #真实偏差
features, labels = synthetic_data(true_w, true_b, 1000) #随机生成1000组训练数据及标签1
2
3
4'features:', features[0],'\nlabel:', labels[0]) #打印一个样本数据和对应标签 print(
features: tensor([-1.1678, -0.7740])
label: tensor([4.5072])1
2
3#可视化数据
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1); #x轴为features的第一列,y轴为标签值,正相关1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def data_iter(batch_size, features, labels):
"""
随机获取一小批样本的数据
@para batch_size 批量的大小
@para features 训练数据
@para labels 训练数据对应的标签
@return
迭代器,每次返回batch_size大小的两组数据,一个是训练样本,一个是对应的标签
"""
num_examples = len(features) #获取样本大小
indices = list(range(num_examples)) #获取样本脚标的list
# 这些样本是随机读取的,没有特定的顺序
random.shuffle(indices) #随机变换indices
for i in range(0, num_examples, batch_size): #开始循环
batch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)]) #有可能不能整除,取i + batch_size和num_examples的较小值
yield features[batch_indices], labels[batch_indices] #相当于是一个迭代器,每次返回batch_size个样本1
2
3
4
5batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y) #X为10 x 2的tensor,y为10 x 1的tensor
break1
2
3# 由于训练的时候需要更细参数,计算梯度,所以requires_grad=True
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True) #w初始化为均值为0,方差为0.001的符合正态分布的数组,纬度为2 x1
b = torch.zeros(1, requires_grad=True) #b初始化为0,纬度为1,就是一个实数1
2
3
4
5
6
7
8
9
10
11def linreg(X, w, b): #@save
"""
线性回归模型
@para X 训练数据(num_examples,len(w))
@para w 权重 (2,1)
@para b 偏差 实数
@return
模型的预估值
"""
return torch.matmul(X, w) + b1
2
3
4
5
6
7
8
9def squared_loss(y_hat, y): #@save
"""
均方损失
@para y_hat 训练数据的真实值(num,1)
@para y 训练数据的预测值
@return
均方误差,没有除以样本数目 (batch_size,1)
"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 21
2
3
4
5
6
7
8
9
10
11
12def sgd(params, lr, batch_size): #@save
"""
小批量随机梯度下降
@para params 参数
@para lr 学习率,人为指定
@para batch_size 批量大小
@return
"""
with torch.no_grad(): #不需要计算梯度
for param in params:
param -= lr * param.grad / batch_size #梯度下降法更新参数
param.grad.zero_() #手动梯度归零1
2
3
4lr = 0.003 #学习率
num_epochs = 3 #训练次数
net = linreg #网络,之前定义的线性网络
loss = squared_loss #损失函数,之前定义的平方损失函数1
2
3
4
5
6
7
8
9
10
11#开始训练
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([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}')1
2print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')1
2
3w的估计误差: tensor([ 0.7622, -1.4047], grad_fn=<SubBackward0>)
b的估计误差: tensor([1.7077], grad_fn=<RsubBackward1>)1
2
3
4
5
6
7
true_w = torch.tensor([2, -3.4]) # 真实权重
true_b = 4.2 # 真实偏差
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
# 应用liner-regression-concise里面的函数,生成训练数据
features1
2
3
4
5
6
7tensor([[-1.4684, 0.0977],
[ 1.7930, 0.9657],
[ 1.1813, 2.1203],
...,
[ 0.0698, 0.6992],
[-1.6323, 2.0169],
[-0.4198, -1.1236]])1
2
3
4
5
6
7
8
9
10
11def load_array(data_arrays, batch_size, is_train=True): #@save
"""
构造一个PyTorch数据迭代器
@para data_arrays 训练数据
@para batch_size 批量大小
@para is_train=True 是否训练,选择True会随机选择数据
@return
迭代器,每次返回batch_size大小的两组数据,一个是训练样本,一个是对应的标签
"""
dataset = data.TensorDataset(*data_arrays) #数据集
return data.DataLoader(dataset, batch_size, shuffle=is_train) 1
2
3class torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, sampler=None,
batch_sampler=None, num_workers=0, collate_fn=<function default_collate>,
pin_memory=False,drop_last=False, timeout=0, worker_init_fn=None)1
2batch_size = 10 # 批量大小
data_iter = load_array((features, labels), batch_size) # 随机返回的数据1
next(iter(data_iter))# iter构造Python迭代器,并使用next从迭代器中获取第一项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20[tensor([[-0.4879, 0.5828],
[-0.2912, -0.0160],
[-2.0002, 2.0359],
[-0.3812, 1.3547],
[ 2.4167, -0.4777],
[ 0.3231, -1.6874],
[ 1.1544, 2.3415],
[ 0.1994, -0.4488],
[ 0.4064, -0.7453],
[ 0.6125, 0.6447]]),
tensor([[ 1.2289],
[ 3.6639],
[-6.7296],
[-1.1639],
[10.6570],
[10.5763],
[-1.4631],
[ 6.1100],
[ 7.5428],
[ 3.2241]])]1
2
3
4
5# nn是神经网络的缩写
from torch import nn
# Sequential一个容器,list of layers
net = nn.Sequential(nn.Linear(2, 1)) #nn.Linear为线性层函数,输入的纬度2;输出的纬度1,1
2
3
40].weight.data.normal_(0, 0.01) #权重初始化,用normal方式 net[
0].bias.data.fill_(0) #初始化偏差为0 net[
tensor([0.])1
loss = nn.MSELoss() #均方误差函数
1
trainer = torch.optim.SGD(net.parameters(), lr=0.03) #随机梯度下降优化算法SGD,传入模型的参数,学习率lr
1
2
3
4
5
6
7
8
9num_epochs = 3 #迭代周期为3
for epoch in range(num_epochs):
for X, y in data_iter: #返回batch_size大小的数据
l = loss(net(X) ,y) #计算损失函数
trainer.zero_grad() #梯度清零
l.backward() #计算梯度
trainer.step() #更新参数
l = loss(net(features), labels) #计算所有样本的loss
print(f'epoch {epoch + 1}, loss {l:f}')1
2
3epoch 1, loss 0.000239
epoch 2, loss 0.000095
epoch 3, loss 0.0000961
2
3
4w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)1
2w的估计误差: tensor([0.0002, 0.0007])
b的估计误差: tensor([-7.1526e-06])
\begin{array}{l}
\mathbf{y}=\left[y_1, y_2, \ldots, y_n\right]^{\top} \\
y_i=\left\{\begin{array}{l}
1 \text { if } i=y \\
0 \text { otherwise }
\end{array}\right.
\end{array}
$$
-
使用均方损失训练
-
最大值为预测
-
输出匹配概率(非负,和为1)
\hat{\mathbf{y}} & =\operatorname{softmax}(\mathbf{0}) \
\hat{y}_i & =\frac{\exp \left(o_i\right)}{\sum_k \exp \left(o_k\right)}
\end{aligned}
2.1.3 Softmax和交叉熵损失
-
交叉熵用来衡量两个概率的区别
-
Softmax回归是一个多类分类模型
-
使用Softmax操作子得到每个类的预测置信度
-
使用交叉熵来衡量和预测标号的区别
2.2损失函数
- 均方损失 L2 Loss




结合L1 Loss和L2 Loss的优点
2.3 图片分类数据集
MNIST
数据集 (LeCun et al., 1998) 是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。我们将使用类似但更复杂的 Fashion-MNIST
数据集 (Xiao et al., 2017):
2.3.1 读取数据集
-
导入数据包
1
2
3
4
5
6
7
8%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l
d2l.use_svg_display()
可以通过框架中的内置函数将 Fashion-MNIST
数据集下载并读取到内存中*。
1 | # 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式, |
- Note:因笔者提前下载了该数据集,因此此处将
dawnload
参数设置为False
,如果未下载,可以设置为True
。
Fashion-MNIST
由 个类别的图像组成,
每个类别包含:
- 训练数据集(train dataset):6000张图像
- 测试数据集(test dataset):1000张图像
因此,训练集和测试集分别包含60000和10000张图像。测试数据集不会用于训练,只用于评估模型性能。
1 | len(mnist_train), len(mnist_test) |
每个输入图像的高度和宽度均为 像素。
数据集由灰度图像组成,其通道数为 。
为了简洁起见,本书将高度 像素、宽度 像素图像的形状记为 或(,)。
1 | 0][0].shape mnist_train[ |
2.3.2 可视化数据集的
Fashion-MNIST
中包含的10个类别,分别为:
- t-shirt(T恤)
- trouser(裤子)
- pullover(套衫)
- dress(连衣裙)
- coat(外套)
- sandal(凉鞋)
- shirt(衬衫)
- sneaker(运动鞋)
- bag(包)
- ankle boot(短靴)
以下函数用于在数字标签索引及其文本名称之间进行转换:
1 | def get_fashion_mnist_labels(labels): #@save |
我们现在可以创建一个函数来可视化这些样本。
1 | def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save |
以下是训练数据集中前几个样本的图像及其相应的标签。
1 | X, y = next(iter(data.DataLoader(mnist_train, batch_size=18))) |

为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建。
回顾一下,在每次迭代中,数据加载器每次都会[读取一小批量数据,大小为batch_size
]。
通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量。
1 | batch_size = 256 |
我们看一下读取训练数据所需的时间。
1 | timer = d2l.Timer() |
Results:
1 | 2.03 sec' |
2.3.4 整合所有组件
现在我们定义 load_data_fashion_mnist
函数,用于获取和读取 Fashion-MNIST
数据集。
这个函数返回训练集和验证集的数据迭代器。
此外,这个函数还接受一个可选参数 resize
,用来将图像大小调整为另一种形状。
1 | def load_data_fashion_mnist(batch_size, resize=None): #@save |
下面,我们通过指定resize
参数来测试load_data_fashion_mnist
函数的图像大小调整功能。
1 | train_iter, test_iter = load_data_fashion_mnist(32, resize=64) |
Results:
1 | torch.Size([32, 1, 64, 64]) torch.float32 torch.Size([32]) torch.int64 |
我们现在已经准备好使用Fashion-MNIST数据集,便于下面的章节调用来评估各种分类算法。
2.3.5 小结
Fashion-MNIST
是一个服装分类数据集,由 个类别的图像组成。我们将在后续章节中使用此数据集来评估各种分类算法。- 我们将高度像素,宽度像素图像的形状记为或(,)。
- 数据迭代器是获得更高性能的关键组件。依靠实现良好的数据迭代器,利用高性能计算来避免减慢训练过程。
2.4 从零实现 Softmax回归
本节我们将使用上节的 Fashion-MNIST
数据集,
并设置数据迭代器的批量大小为,来实现softmax
回归的。
1 | import torch |
2.4.1 初始化模型参数
和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。
原始数据集中的每个样本都是 的图像。此处简单展平每个图像,把它们看作长度为784的向量。在后面的章节中,我们将讨论能够利用图像空间结构的特征,但此处暂时只把每个像素位置看作一个特征。
回想一下,在 softmax
回归中,我们的输出与类别一样多。(因为我们的数据集有10个类别,所以网络输出维度为10)。因此,权重将构成一个 的矩阵,偏置将构成一个的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重 ,偏置初始化为 。
1 | num_inputs = 784 |
2.4.2 定义 softmax 操作
1 | X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) |
Results:
1 | (tensor([[5., 7., 9.]]), |
- Note: 当调用
sum
运算符时,我们可以指定保持在原始张量的轴数,而不折叠求和的维度。
这将产生一个具有形状(1, 3)
的二维张量。
回想一下,[实现softmax]由三个步骤组成:
- 对每个项求幂(使用
exp
); - 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
- 将每一行除以其规范化常数,确保结果的和为1。
softmax
的表达式:
分母或规范化常数,有时也称为配分函数(其对数称为对数-配分函数)。该名称来自统计物理学 中一个模拟粒子群分布的方程。
1 | def softmax(X): |
正如上述代码,对于任何随机输入,我们将每个元素变成一个非负数。此外,依据概率原理,每行总和为 。
1 | X = torch.normal(0, 1, (2, 5)) |
Results:
1 | (tensor([[0.2135, 0.5900, 0.0449, 0.1064, 0.0453], |
Note:虽然这在数学上看起来是正确的,但我们在代码实现中有点草率。矩阵中的非常大或非常小的元素可能造成数值上溢或下溢,但我们没有采取措施来防止这点。
2.4.3 定义模型
定义操作后,我们可以实现 softmax
回归模型。下面的代码定义了输入如何通过网络映射到输出。
Note:将数据传递到模型之前,我们使用reshape
函数将每张原始图像展平为向量。
1 | def net(X): |
2.4.4 定义损失函数
接下来,我们实现交叉熵损失函数。这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题的数量。
回顾一下,交叉熵采用真实标签的预测概率的负对数似然。
Note:这里不使用 for
循环迭代预测(这往往是低效的),而是通过一个运算符选择所有元素。
下面,我们创建一个数据样本 y_hat
,其中包含2个样本在3个类别的预测概率,以及它们对应的标签y
。
有了 y
。
我们知道在第一个样本中,第一类是正确的预测;而在第二个样本中,第三类是正确的预测。然后使用y
作为 y_hat
中概率的索引,我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。
1 | y = torch.tensor([0, 2]) |
Results:
1 | tensor([0.1000, 0.5000]) |
现在我们只需一行代码就可以实现交叉熵损失函数。
1 | def cross_entropy(y_hat, y): |
Results:
1 | tensor([2.3026, 0.6931]) |
2.4.5 分类精度
给定预测概率分布 y_hat
,当我们必须输出硬预测(hard prediction)时,我们通常选择预测概率最高的类。
当预测与标签分类 y
一致时,即是正确的。
分类精度 即正确预测数量与总预测数量之比。
虽然直接优化精度可能很困难(因为精度的计算不可导),但精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。
为了计算精度,我们执行以下操作。
-
如果
y_hat
是矩阵,那么假定第二个维度存储每个类的预测分数。使用argmax
获得每行中最大元素的索引来获得预测类别。 -
将预测类别与真实
y
元素进行比较。由于等式运算符
==
对数据类型很敏感,因此我们将y_hat
的数据类型转换为与y
的数据类型一致。结果是一个包含0(错)和1(对)的张量。 -
最后,我们求和会得到正确预测的数量。
1 | def accuracy(y_hat, y): #@save |
我们将继续使用之前定义的变量 y_hat
和 y
分别作为预测的概率分布和标签。
可以看到,第一个样本的预测类别是 (该行的最大元素为 ,索引为 ),这与实际标签 不一致。
第二个样本的预测类别是 (该行的最大元素为 ,索引为 ),这与实际标签 一致。
因此,这两个样本的分类精度率为 。
1 | accuracy(y_hat, y) / len(y) |
Results:
1 | 0.5 |
同样,对于任意数据迭代器 data_iter
可访问的数据集,我们可以评估在任意模型 net
的精度。
1 | def evaluate_accuracy(net, data_iter): #@save |
这里定义一个实用程序类 Accumulator
,用于对多个变量进行累加。在上面的evaluate_accuracy
函数中,我们在 Accumulator
实例中创建了2个变量,
分别用于存储正确预测的数量和预测的总数量。
当我们遍历数据集时,两者都将随着时间的推移而累加。
1 | class Accumulator: #@save |
由于我们使用随机权重初始化net
模型,
因此该模型的精度应接近于随机猜测。
例如在有10个类别情况下的精度为0.1。
1 | evaluate_accuracy(net, test_iter) |
Results:
1 | 0.0508 |
2.4.6 训练
在我们看过 [[dl-notes-2#2.1.3 线性回归的从零开始实现|从零开始实现线性回归]] 中的线性回归实现, softmax
回归的训练过程代码应该看起来非常眼熟。在这里,我们重构训练过程的实现以使其可重复使用。
首先,定义一个函数来训练一个迭代周期。请注意, updater
是更新模型参数的常用函数,它接受批量大小作为参数。它可以是 d2l.sgd
函数,也可以是框架的内置优化函数。
1 | def train_epoch_ch3(net, train_iter, loss, updater): #@save |
在展示训练函数的实现之前,我们[定义一个在动画中绘制数据的实用程序类]Animator
,它能够简化本书其余部分的代码。
1 | class Animator: #@save |
接下来我们实现一个训练函数,它会在 train_iter
访问到的训练数据集上训练一个模型 net
。该训练函数将会运行多个迭代周期(由 num_epochs
指定)。
在每个迭代周期结束时,利用 test_iter
访问到的测试数据集对模型进行评估。我们将利用 Animator
类来可视化训练进度。
1 | def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save |
作为一个从零开始的实现,我们使用小批量随机梯度下降来优化模型的损失函数,设置学习率为 。
1 | lr = 0.1 |
现在,我们训练模型10个迭代周期。
- Note:迭代周期(
num_epochs
)和学习率(lr
)都是可调节的超参数。通过更改它们的值,我们可以提高模型的分类精度。
1 | num_epochs = 10 |
Results:

现在训练已经完成,我们的模型已经准备好对图像进行分类预测。给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。
1 | def predict_ch3(net, test_iter, n=6): #@save |

- 借助
softmax
回归,我们可以训练多分类的模型。 - 训练
softmax
回归循环模型与训练线性回归模型非常相似:先读取数据,再定义模型和损失函数,然后使用优化算法训练模python
型。大多数常见的深度学习模型都有类似的训练过程。
2.5 Softmax 简洁实现
在 [[dl-notes-2#2.1.4 调用 API 实现|简洁实现线性回归]] 中,我们发现通过深度学习框架的高级API能够使实现线性回归变得更加容易。同样,通过深度学习框架的高级 API
也能更方便地实现 softmax
回归模型。
本节同上一节一样,继续使用 Fashion-MNIST
数据集,并保持批量大小为 256
。
1 | import torch |
2.5.1 初始化模型参数
如我们在 [[dl-notes-2#2.2 Softmax 回归|Softmax 回归]] 所述, softmax
回归的输出层是一个全连接层。
因此,为了实现模型,只需在 Sequential
中添加一个带有 个输出的全连接层。
- Note:同样,在这里
Sequential
并不是必要的,但它是实现深度模型的基础。仍然以均值 和标准差 随机初始化权重。
1 | # PyTorch不会隐式地调整输入的形状。因此, |
Note: PyTorch
不会隐式地调整输入的形状。因此,我们在线性层前定义了展平层(flatten),来调整网络输入的形状。
2.5.2 重新审视Softmax的实现
在前面 [[dl-notes-2#2.2.4 从零实现 Softmax回归|从零实现 Softmax回归]] 的例子中,我们计算了模型的输出,然后将此输出送入交叉熵损失。从数学上讲,这是一件完全合理的事情。然而,从计算角度来看,指数可能会造成数值稳定性问题。
回想一下, softmax
函数:
其中, 是预测的概率分布, 是未规范化的预测 的第 个元素。
如果 中的一些数值非常大,那么 可能大于数据类型容许的最大数字,即上溢(overflow)。
这将使分母或分子变为 inf
(无穷大),
最后得到的是0、inf
或 nan
(不是数字)的 。在这些情况下,我们无法得到一个明确定义的交叉熵值。
解决技巧:
-
在继续softmax计算之前,先从所有中减去 。这里可以看到每个按常数进行的移动不会改变
softmax
的返回值:\hat y_j & = \frac{\exp(o_j - \max(o_k))\exp(\max(o_k))}{\sum_k \exp(o_k - \max(o_k))\exp(\max(o_k))} \
& = \frac{\exp(o_j - \max(o_k))}{\sum_k \exp(o_k - \max(o_k))}.
\end{aligned}
尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。通过将 softmax
和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题。
如下面的等式所示,我们避免计算,而可以直接使用 ,因为 被抵消了。
我们希望保留传统的 softmax
函数,以用于评估模型输出的概率。但是,我们并没有在交叉熵损失函数中传递 softmax
概率,而是传递了未规范化的预测,并同时计算了 softmax
及其对数。这种方法类似于"LogSumExp技巧"技巧。
2.5.3 损失函数
1 | loss = nn.CrossEntropyLoss(reduction='none') |
2.5.4 优化算法
在这里,我们使用学习率为 的小批量随机梯度下降作为优化算法。
1 | trainer = torch.optim.SGD(net.parameters(), lr=0.1) |
2.5.5 训练
接下来我们调用上一节中定义的训练函数来训练模型。
1 | try: |
Results:
1 | module 'd2l.torch' has no attribute 'train_ch3' |

-
train_epoch_ch3
函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = d2l.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()), d2l.accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2] -
train
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16num_epochs = 10
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save
"""训练模型(定义见第3章)"""
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'], figsize=(4.5, 2.5))
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = d2l.evaluate_accuracy_gpu(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
train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
 )
2.5.6小结
- 使用深度学习框架的高级API,我们可以更简洁地实现
softmax
回归。 - 从计算的角度来看,实现
softmax
回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。
2.6 softmax回归Q&A
- Q1:softlabel训练策略以及为什么有效?
softmax用指数很难逼近1,softlabel将正例和负例分别标记为0.9和0.1使结果逼近变得可能,这是一个常用的小技巧。
- Q2:softmax回归和logistic回归?
logistic回归为二分类问题,是softmax回归的特例
- Q3:为什么使用交叉熵,而不用相对熵,互信息熵等其他基于信息量的度量?
实际上使用哪一种熵的效果区别不大,所以哪种简单就用哪种
- Q4: 为什么我们只关心正确类,而不关心不正确的类呢?
并不是不关心,而是不正确的的类标号为零,所以算式中不体现,如果使用softlabel策略,就会体现出不正确的类。
- Q5:似然函数曲线是怎么得出来的?有什么参考意义?
最小化损失函数也意味着最大化似然函数,似然函数表示统计概率和模型的拟合程度。
- Q6:在多次迭代之后欧如果测试精度出现上升后再下降是过拟合了吗?可以提前终止吗?
很有可能是过拟合,可以继续训练来观察是否持续下降
- Q7:cnn网络主要学习到的是纹理还是轮廓还是所有内容的综合?
目前认为主要学习到的是纹理信息
- Q8:softmax可解释吗?
单纯softmax是可解释的,可以在统计书籍中找到相关的解释。