《动手学深度学习(Pytorch版)》Task03:线性神经网络
- 线性回归
- 基本元素
- 线性模型
- 损失函数
- 随机梯度下降
- 正态分布与平方损失
- 线性回归的从零开始实现
- 读取数据集
- 初始化模型参数
- 定义模型
- 定义损失函数
- 定义优化算法
- 训练
- 线性回归的简洁实现
- 读取数据集
- 定义模型
- 初始化模型参数
- 定义损失函数
- 定义优化算法
- 训练
- softmax回归
- 分类问题
- 网络架构
- softmax运算
- 损失函数
- 熵
- 信息量
- 交叉熵
- 模型预测和评估
- 图像分类数据集
- 读取小批量
- softmax回归的从零开始实现
- 读取数据
- 初始化模型参数
- 定义softmax操作
- 定义模型
- 定义损失函数
- 分类精度
- 训练
- 一个迭代周期的训练模型
- 整体训练模型
- 设置损失函数
- 预测
- 训练过程
- softmax回归的简洁实现
- 初始化模型参数
- 定义损失函数
- 优化算法
- 训练
线性回归
回归(regression):为一个或多个自变量与因变量之间关系建模的一类方法,表示输入和输出之间的关系。
预测(prediction)/推断(inference):给定特征估计目标的过程
经典回归问题:预测价格(房屋、股票等)、预测住院时间(针对住院病人等)、 预测需求(零售销量等)。
基本元素
以房屋预测为例:
任务:根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。
特征(feature)/协变量(covariate):预测所依据的自变量(面积和房龄)
训练数据集(training data set)/训练集(training set):真实的房屋销售价格、面积和房龄等数据集
样本(sample)/数据点(data point)/数据样本(data instance):每行数据(比如一次房屋交易相对应的数据)
线性模型
线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和
p r i c e = w a r e a ⋅ a r e a + w a g e ⋅ a g e + b \mathrm{price} = w_{\mathrm{area}} \cdot \mathrm{area} + w_{\mathrm{age}} \cdot \mathrm{age} + b price=warea⋅area+wage⋅age+b
权重(weight):决定每个特征对预测值的影响,如: w a r e a w_{\mathrm{area}} warea和 w a g e w_{\mathrm{age}} wage
偏置(bias)/偏移量(offset)/截距(intercept):当所有特征都取值为0时,预测值应该为多少,如: b b b
仿射变换(affine transformation):通过加权和对特征进行线性变换(linear transformation), 并通过偏置项来进行平移(translation)
预测结果:通常使用
y
^
\hat{y}
y^表示
y
y
y的估计值
y
^
=
w
1
x
1
+
.
.
.
+
w
d
x
d
+
b
\hat{y} = w_1 x_1 + ... + w_d x_d + b
y^=w1x1+...+wdxd+b
用点积形式表示:
y
^
=
w
⊤
x
+
b
\hat{y} = \mathbf{w}^\top \mathbf{x} + b
y^=w⊤x+b
用矩阵-向量乘法表示:
y
^
=
X
w
+
b
{\hat{\mathbf{y}}} = \mathbf{X} \mathbf{w} + b
y^=Xw+b
线性模型可以看作单层神经网络,权重是一层,输出是一层
损失函数
损失函数(loss function):量化目标的实际值与预测值之间的差距。
回归问题中最常用的损失函数是平方误差函数。公式定义为:
l
(
i
)
(
w
,
b
)
=
1
2
(
y
^
(
i
)
−
y
(
i
)
)
2
l^{(i)}(\mathbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y^{(i)}\right)^2
l(i)(w,b)=21(y^(i)−y(i))2
计算在训练集𝑛个样本上的损失均值(也等价于求和)。
L
(
w
,
b
)
=
1
n
∑
i
=
1
n
l
(
i
)
(
w
,
b
)
=
1
n
∑
i
=
1
n
1
2
(
w
⊤
x
(
i
)
+
b
−
y
(
i
)
)
2
L(\mathbf{w}, b) =\frac{1}{n}\sum_{i=1}^n l^{(i)}(\mathbf{w}, b) =\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2
L(w,b)=n1i=1∑nl(i)(w,b)=n1i=1∑n21(w⊤x(i)+b−y(i))2
最小化损失
w
∗
,
b
∗
=
argmin
w
,
b
L
(
w
,
b
)
\mathbf{w}^*, b^* = \operatorname*{argmin}_{\mathbf{w}, b}\ L(\mathbf{w}, b)
w∗,b∗=w,bargmin L(w,b)
随机梯度下降
梯度下降(gradient descent):最常用的损失函数求优化模型方法
小批量随机梯度下降(minibatch stochastic gradient descent):在每次需要计算更新的时候随机抽取一小批样本。
批量大小(batch size): b b b每个小批量中的样本数
学习率(learning rate): η \eta η ,每次走多长,步长的超参数
超参数(hyperparameter):可以调整但在训练过程中不更新的参数
调参(hyperparameter tuning):选择超参数的过程
泛化(generalization):找到一组参数,这组参数能够在我们从未见过的数据上实现较低的损失
正态分布与平方损失
正态分布(normal distribution)/高斯分布(Gaussian distribution):
若随机变量 x x x具有均值 μ \mu μ和方差 σ 2 \sigma^2 σ2(标准差 σ \sigma σ),其正态分布概率密度函数如下:
p ( x ) = 1 2 π σ 2 exp ( − 1 2 σ 2 ( x − μ ) 2 ) p(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (x - \mu)^2\right) p(x)=2πσ21exp(−2σ21(x−μ)2)
def normal(x, mu, sigma):
p = 1 / math.sqrt(2 * math.pi * sigma**2)
return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)
- 改变均值会产生沿𝑥轴的偏移
- 增加方差将会分散分布、降低其峰值
**均方误差损失函数(简称均方损失)(MSE):**对于一组数据,模型预测的值与真实值之间的差异的平方和的平均值被定义为均方误差。
极大似然估计量(MLE):对于给定的数据集和一个概率分布,MLE寻找使得观测到的数据概率(即“似然”)最大化的参数值。
在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。
最小化均方误差(MSE):
- 在统计学和机器学习中,均方误差是衡量模型预测准确性的一种常用指标。对于一组数据,模型预测的值与真实值之间的差异的平方和的平均值被定义为均方误差。
- 如果我们有一个线性模型 $ y = X\beta + \epsilon$ ,其中 ( y ) 是观测到的响应变量, X X X 是设计矩阵(包含了所有的预测变量), b e t a beta beta是模型参数, e p s i l o n epsilon epsilon是误差项。
- 均方误差可以表示为
MSE = 1 n ∑ i = 1 n ( y i − X i β ) 2 \text{MSE} = \frac{1}{n}\sum_{i=1}^{n} (y_i - X_i\beta)^2 MSE=n1i=1∑n(yi−Xiβ)2
,其中 ( n ) 是样本数,( $y_i KaTeX parse error: Can't use function '\)' in math mode at position 1: \̲)̲ 是第 \( i \) 个观测… X_i$ ) 是第 ( i ) 个观测值的预测变量,( β \beta β ) 是模型参数。- 最小化MSE即寻找最佳的模型参数 ( $\hat{\beta} $),使得 ( MSE \text{MSE} MSE ) 最小。
极大似然估计(MLE):
- 极大似然估计是一种基于概率模型的参数估计方法。对于给定的数据集和一个概率分布,MLE寻找使得观测到的数据概率(即“似然”)最大化的参数值。
- 假设观测误差 ( ϵ \epsilon ϵ ) 遵循一个以零为均值的高斯(或正态)分布,那么每一个观测值 ( y i y_i yi ) 也会遵循一个以 ( X i β X_i\beta Xiβ ) 为均值的高斯分布。
- 高斯分布的似然函数可以写作
L ( β ) = ∏ i = 1 n 1 2 π σ exp ( − ( y i − X i β ) 2 2 σ 2 ) L(\beta) = \prod_{i=1}^{n} \frac{1}{\sqrt{2\pi}\sigma} \exp \left( -\frac{(y_i - X_i\beta)^2}{2\sigma^2} \right) L(β)=i=1∏n2πσ1exp(−2σ2(yi−Xiβ)2)
,其中 ( σ \sigma σ) 是高斯噪声的标准差。- 极大似然估计的目标是找到参数 ( β \beta β ),以最大化似然函数 ( $L(\beta) $)。
等价性:
- 当我们取似然函数 ( L ( β ) L(\beta) L(β) ) 的对数(称为对数似然函数),并对其进行最大化,我们实际上是在最小化 ( y i − X i β ) 2 (y_i - X_i\beta)^2 (yi−Xiβ)2 的负和,因为对数函数是单调递增的,而负号是因为对数似然通常有一个负的指数项。
- 对数似然函数的最大化与最小化均方误差在数学上是等价的,因为这两个优化问题都会导致同样的最优参数 β ^ \hat{\beta} β^ 。
因此,在高斯噪声的假设下,通过最小化均方误差来求解线性模型的参数等价于对模型参数进行极大似然估计,即两种方法都会得到相同的参数估计结果。这表明在某些条件下,频率学派的估计方法(如最小二乘法)和贝叶斯学派的估计方法(如极大似然估计)是可以互相转换的
线性回归的从零开始实现
数据流水线、模型、损失函数和小批量随机梯度下降优化器
读取数据集
训练模型:
- 对数据集进行遍历
- 每次抽取一小批量样本
- 使用它们来更新我们的模型
data_iter
函数: 生成大小为batch_size
的小批量
# input: 批量大小、特征矩阵、标签向量
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# 打乱下标顺序,这些样本是随机读取的,没有特定的顺序
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]
初始化模型参数
通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。
采用梯度下降法更新参数,并使用pytorch 自动微分
定义模型
将模型的输入和参数同模型的输出关联起来。
# 输入特征X、模型权重w、偏置b
def linreg(X, w, b): #@save
"""线性回归模型"""
return torch.matmul(X, w) + b
torch.matmul
:tensor的乘法
定义损失函数
定义平方损失函数作为损失函数
# input: 预测值、真实值
def squared_loss(y_hat, y): #@save
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
定义优化算法
小批量随机梯度下降:
- 在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。
- 朝着减少损失的方向更新我们的参数。
# input: 参数集合、学习速率和批量大小
def sgd(params, lr, batch_size): #@save
"""小批量随机梯度下降"""
with torch.no_grad(): # 暂时设置选定的代码块中操作不会跟踪梯度
for param in params:
param -= lr * param.grad / batch_size
# pytorch不会设为0,需要手动
param.grad.zero_()
训练
训练过程: 正向传播->计算损失->反向传播->参数更新
- 在每次迭代(epoch)中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测(正向传播)。
- 计算完损失后,我们开始反向传播[计算每个参数(权重和偏差)的偏导数(梯度)],存储每个参数的梯度。
- 调用优化算法
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([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}')
真实参数和通过训练学到的参数确实非常接近
深度网络中存在许多参数组合能够实现高度精确的预测。
习题参考
线性回归的简洁实现
通过使用深度学习框架来简洁地实现线性回归模型
读取数据集
调用API读取数据
data_arrays
:包括特征矩阵和对应的标签数组
batch_size
:定义每个批次包含的样本数
is_train
:是否希望数据迭代器对象在每个迭代周期内打乱数据
# input:
def load_array(data_arrays, batch_size, is_train=True): #@save
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)
data.TensorDataset
是一个 PyTorch 类,用于封装数据和目标张量。通过提供的data_arrays
(一个包含特征和标签的元组或列表),它创建了一个数据集,其中每个元素是一个特征和标签对。*data_arrays
使用星号(*)操作符,这意味着如果data_arrays
是一个元组或列表,则它会被解包成多个独立的参数传递给TensorDataset
。data.DataLoader
是一个迭代器,用于加载 PyTorch 数据集。它提供了一个简便的方式来自动批处理数据,并且根据需要进行数据的洗牌和多线程加载。shuffle=is_train
根据is_train
的值决定是否在每个 epoch 开始时打乱数据。这是训练时常用的做法,因为它有助于模型泛化,避免因数据顺序引入的偏差。
使用iter
构造Python迭代器
batch_size = 10
data_iter = load_array((features, labels), batch_size)
使用next
从迭代器中获取第一项
next(iter(data_iter))
定义模型
使用框架的预定义好的层:只关注哪些层构造模型,而不关注实现细节。
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
- 定义一个模型变量
net
,它是一个Sequential
类的实例。 Sequential
类是一个容器,将多个层串联在一起,形成一个神经网络模型。nn.Linear(2, 1)
是一个线性层,它将输入的维度从 2 维降到 1 维。- 第一个参数
2
是输入特征的维度,表示输入数据是一个二维向量(或者说包含两个特征)。 - 第二个参数
1
是输出特征的维度,表示线性层将输入的特征映射到一个一维向量上。
- 第一个参数
初始化模型参数
使用预定义方法来初始化参数
通过net[0]
选择网络中的第一个图层
使用weight.data
和bias.data
方法访问参数
使用替换方法normal_
和fill_
来重写参数值
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
net[0].weight.data.normal_(0, 0.01)
每个权重参数从均值为0、标准差为0.01的正态分布中随机采样net[0].bias.data.fill_(0)
偏置参数将初始化为零。
定义损失函数
计算均方误差使用的是MSELoss
类,也称为平方
L
2
L_2
L2范数
loss = nn.MSELoss()
定义优化算法
小批量随机梯度下降算法
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
torch.optim
是 PyTorch 中的优化器模块,提供了各种优化算法的实现。SGD
是随机梯度下降(Stochastic Gradient Descent)的缩写,是一种常用的优化算法之一,用于更新神经网络的参数。net.parameters()
是一个生成器,用于指定获取神经网络模型net
中的所有可学习参数。lr=0.03
是学习率(learning rate)的设置,表示每次更新参数时的步长大小。
训练
num_epochs = 3 # 定义迭代周期,训练轮数为 3 轮。
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y) # 调用net(X)生成预测,并且计算预测值与真实标签之间的损失(前向传播)
trainer.zero_grad() #清零之前保存在优化器中的梯度,以便进行新一轮的梯度计算。
l.backward() # 反向传播计算梯度
trainer.step() # 调用优化器,根据梯度更新模型的参数
l = loss(net(features), labels)# 计算整个训练集上模型的损失值
print(f'epoch {epoch + 1}, loss {l:f}')
真实参数和训练获得的模型参数非常接近
Q:如果将小批量的总损失替换为小批量损失的平均值,需要如何更改学习率?
A:将学习率缩小为之前的1/n
Q:用Huber损失代替原损失
# huber损失对应Pytorch的SmoothL1损失
loss = nn.SmoothL1Loss(beta=0.5)
Q:如何访问线性回归的梯度?
net[0].weight.grad, net[0].bias.grad
softmax回归
分类问题:
- 硬分类:属于哪个类别
- 软分类:属于每个类别的概率
分类问题
图像分类问题:
假设每次输入是一个 2 × 2 2\times2 2×2的灰度图像。
我们可以用一个标量表示每个像素值,每个图像对应四个特征 x 1 , x 2 , x 3 , x 4 x_1, x_2, x_3, x_4 x1,x2,x3,x4。
此外,假设每个图像属于类别“猫”“鸡”和“狗”中的一个。
独热编码(one-hot encoding):一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。
利用one-hot encoding,设置标签
y
y
y是一个三维向量,
其中
(
1
,
0
,
0
)
(1, 0, 0)
(1,0,0)对应于“猫”、
(
0
,
1
,
0
)
(0, 1, 0)
(0,1,0)对应于“鸡”、
(
0
,
0
,
1
)
(0, 0, 1)
(0,0,1)对应于“狗”:
y ∈ { ( 1 , 0 , 0 ) , ( 0 , 1 , 0 ) , ( 0 , 0 , 1 ) } . y \in \{(1, 0, 0), (0, 1, 0), (0, 0, 1)\}. y∈{(1,0,0),(0,1,0),(0,0,1)}.
网络架构
建立一个有多个输出的模型,每个类别对应一个输出。
我们有4个特征和3个可能的输出类别
向量形式表达为 o = W x + b \mathbf{o} = \mathbf{W} \mathbf{x} + \mathbf{b} o=Wx+b
softmax运算
校准(calibration):训练一个目标函数,来激励模型精准地估计概率。
softmax函数:能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持可导的性质。
y
^
=
s
o
f
t
m
a
x
(
o
)
其中
y
^
j
=
exp
(
o
j
)
∑
k
exp
(
o
k
)
\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}
y^=softmax(o)其中y^j=∑kexp(ok)exp(oj)
softmax回归是一个线性模型(linear model):softmax回归的输出仍然由输入特征的仿射变换决定。
最大值为预测结果:
y
^
=
argmax
i
o
i
\hat y = \operatorname*{argmax}_i o_i
y^=iargmaxoi
概率
y
y
y和
y
^
\hat{y}
y^的区别作为损失
softmax回归的矢量计算表达式:
O
=
X
W
+
b
,
Y
^
=
s
o
f
t
m
a
x
(
O
)
.
\begin{aligned} \mathbf{O} &= \mathbf{X} \mathbf{W} + \mathbf{b}, \\ \hat{\mathbf{Y}} & = \mathrm{softmax}(\mathbf{O}). \end{aligned}
OY^=XW+b,=softmax(O).
损失函数
交叉熵损失(cross-entropy loss):所有标签分布的预期损失值。
熵
信息论的核心思想是量化数据中的信息内容。
这个量化的数据称作熵
H
[
P
]
=
∑
j
−
P
(
j
)
log
P
(
j
)
H[P] = \sum_j - P(j) \log P(j)
H[P]=j∑−P(j)logP(j)
信息量
如果我们很容易预测下一个数据,那么这个数据就很容易压缩。
如果我们不能完全预测每一个事件,会感到惊异在观察一个事件
j
j
j时,并赋予它(主观)概率
P
(
j
)
P(j)
P(j),事件的
P
(
j
)
P(j)
P(j)较低时,越惊异,该事件的信息量也就更大。
log
1
P
(
j
)
=
−
log
P
(
j
)
\log \frac{1}{P(j)} = -\log P(j)
logP(j)1=−logP(j)
熵是当分配的概率真正匹配数据生成过程时的信息量的期望。
交叉熵
交叉熵可以抽象理解为:主观概率为𝑄的观察者在看到根据概率𝑃生成的数据时的预期惊异
交叉熵从
P
P
P到
Q
Q
Q,记为
H
(
P
,
Q
)
H(P, Q)
H(P,Q)
H
(
P
,
Q
)
=
∑
i
−
P
(
i
)
log
Q
(
i
)
H(P, Q)= \sum_i - P(i) \log Q(i)
H(P,Q)=i∑−P(i)logQ(i)
构建损失函数:
l
(
y
,
y
^
)
=
−
∑
j
=
1
q
y
j
log
y
^
j
l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j
l(y,y^)=−j=1∑qyjlogy^j
不关心非正确的预测值,只关心正确的预测值的置信度有多大,对真实类别的值求log然后求负数
其梯度是真实概率和预测概率的区别:
∂
o
j
l
(
y
,
y
^
)
=
s
o
f
t
m
a
x
(
o
)
j
−
y
j
\partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) = \mathrm{softmax}(\mathbf{o})_j - y_j
∂ojl(y,y^)=softmax(o)j−yj
模型预测和评估
精度(accuracy):正确预测数/预测总数
图像分类数据集
Fashion-MNIST是一个服装分类数据集,由10个类别的图像组成,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。
每个类别由训练数据集(train dataset)中的6000张图像 和测试数据集(test dataset)中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。
每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。简洁表示为高度 h h h像素,宽度 w w w像素,记为 h × w h \times w h×w或( h h h, w w w)。
下载数据集
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()))
transforms.ToTensor()
通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,将图像数据转换为 Tensor 类型,并且进行归一化处理,以便于输入到神经网络中进行训练或推理。
报错:RuntimeError: Error downloading train-images-idx3-ubyte.gz
原因:没有安装最新版本的torchvision
pip install torchvision --upgrade
安装完就可以正常下载了
读取小批量
- 使用内置的数据迭代器读取数据
- 每次都会读取一小批量数据,大小为
batch_size
- 随机打乱所有样本
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())
softmax回归的从零开始实现
读取数据
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化模型参数
原始数据集中的每个样本都是28×28的图像,将他们展平,得到输入为784维的向量
10个类别输出维度为10
权重:784×10的矩阵
偏置:1×10的行向量
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操作
X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdim=True), X.sum(1, keepdim=True)
keepdim=True
保持原始张量的轴数
实现softmax步骤:
- 对每个项求幂(使用
exp
); - 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
- 将每一行除以其规范化常数,确保结果的和为1。
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制
PS:
在一个二维张量中,
axis=0
表示沿着行的方向进行操作,而axis=1
表示沿着列的方向进行操作。所以,指定
axis=0
表示对每一列进行求和,而指定axis=1
表示对每一行进行求和。
定义模型
softmax回归模型
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
torch.matmul(X.reshape((-1, W.shape[0])), W)
操作将图像展平为向量
定义损失函数
交叉熵损失函数
交叉熵采用真实标签的预测概率的负对数似然。
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
分类精度
分类精度:正确预测数量与总预测数量之比。
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())
精确率:
accuracy(y_hat, y) / len(y)
评估在指定数据集上任意模型net
的精度
# input: 神经网络模型、数据迭代器
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module): #检查 net 是否为 PyTorch 的神经网络模型
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 存储正确预测数、预测总数
with torch.no_grad(): #设置不需要梯度计算的上下文环境
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel()) #计算每个批次的准确率,并将正确预测数和预测总数累加到 metric 中。
return metric[0] / metric[1] #整个数据集上的精度,即所有正确预测的样本数除以总样本数。
定义实用程序类Accumulator
,用于对多个变量进行累加。
Accumulator
实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n): #初始化累加器
self.data = [0.0] * n
def add(self, *args): #向累加器中添加新的数据
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self): #重置累加器的状态
self.data = [0.0] * len(self.data)
def __getitem__(self, idx): #获取累加器中指定索引位置的数据
return self.data[idx]
训练
定义画图函数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)
一个迭代周期的训练模型
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module): #检查 net 是否为 PyTorch 的神经网络模型
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): # 检查 updater 对象是否是一个有效的优化器对象
# 使用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]
isinstance()
函数用于检查一个对象是否是指定类的实例。updater
是更新模型参数的常用函数,它接受批量大小作为参数。 它可以是d2l.sgd
函数,也可以是框架的内置优化函数。
整体训练模型
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
- 在Python中,
assert
语句用作一种调试辅助工具,它会检查指定的条件表达式。如果条件表达式结果为True
,程序会继续执行;如果条件表达式结果为False
,则会引发一个AssertionError
异常。assert
语句通常用于确保程序在某个特定状态下运行,或者验证某些假设条件是否为真。 - 使用
assert
语句进行条件断言,确保训练损失小于0.5,训练准确度和测试准确度都应该大于0.7且不超过1。这些断言是为了验证训练过程是否达到预期的质量标准。
设置损失函数
lr = 0.1
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
训练十个周期
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
预测
对图像进行分类预测
#在测试集预测少量数据
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])
predict_ch3(net, test_iter)
get_fashion_mnist_labels
函数(通常定义在辅助库d2l
中)将标签索引转换为人类可读的标签名。net(X)
计算每个图像的预测结果.argmax(axis=1)
找出每个预测结果中概率最高的类别的索引d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
:这个函数调用显示前n
个图像。图像数据X[0:n]
被重塑成(n, 28, 28)
的格式,因为原始Fashion-MNIST
图像是28x28像素的灰度图。参数1
和n
指定每行显示1个图像,总共显示n
个图像。titles[0:n]
为每个图像提供一个标题。
输出:实际标签(文本输出的第一行)模型预测(文本输出的第二行)
训练过程
- 读取数据
- 定义模型和损失函数
- 使用优化算法训练模型
softmax回归的简洁实现
初始化模型参数
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) #将输入数据展平,定义输入784维、输出10维的线性层
def init_weights(m): # 如果是线性层
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01) #使用正态分布初始化线性层 m 的权重
net.apply(init_weights);
nn.Flatten()
: 这是一个用于将输入数据展平的层。在这个例子中,输入数据的形状通常是(batch_size, 1, 28, 28)
,表示一个批次中包含的样本数、通道数(灰度图像为 1)、图像的高度和宽度。nn.init.normal_
是 PyTorch 提供的一个函数,用于对张量进行正态分布初始化。接受两个参数:m.weight
表示线性层的权重std=0.01
表示正态分布的标准差为 0.01。
定义损失函数
loss = nn.CrossEntropyLoss(reduction='none')
reduction='none'
:指定损失的计算方式,表示不进行降维操作
优化算法
使用学习率为0.1的小批量随机梯度下降作为优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
Q:为什么测试精度会在一段时间后降低?
A:模型过拟合,可以加入正则化如dropout