4.线性神经网络
目录
- 线性回归
- 线性回归的基本元素
- 线性模型
- 损失函数
- 解析解
- 随机梯度下降
- 矢量化加速
- 正态分布与平方损失
- 线性回归的基本元素
- 优化方法
- 梯度下降
- 选择学习率
- 小批量随机梯度下降
- 选择批量大小
- 总结
- 线性回归的从零开始实现
- 生成数据集
- 读取数据集
- 初始化模型参数
- 定义模型
- 定义损失函数
- 定义优化算法
- 训练
- 线性回归的简介实现
- 生成数据集
- 读取数据集
- 定义模型
- 初始化模型参数
- 定义损失函数
- 定义优化函数
- 训练
- softmax回归
- 分类问题
- 网络架构
- softmax运算
- 小批量样本的矢量化
- 损失函数
- L2 Loss均方损失函数
- L1 Loss绝对值损失函数
- Huber’s Robust Loss鲁棒损失函数
- 对数似然
- softmax及其导数
- 模型的预测和评估
- 图像分类数据集
- 读取数据集
- 读取小批量
- 整合所有组件
- softmax回归的从零开始实现
- 初始化模型参数
- 定义softmax操作
- 定义模型
- 定义损失函数
- 分类精度
- 训练
- 预测
- softmax回归的简洁实现
- 初始化模型参数
- 重新审视Softmax的实现
- 优化算法
- 训练
回归(regression)是能为一个或多个自变量与因变量之间关系建模的一类方法。 在自然科学和社会科学领域,回归经常用来表示输入和输出之间的关系。
在机器学习领域中的大多数任务通常都与_预测_(prediction)有关。 当我们想预测一个数值时,就会涉及到回归问题。 常见的例子包括:预测价格(房屋、股票等)、预测住院时间(针对住院病人等)、 预测需求(零售销量等)。 但不是所有的_预测_都是回归问题。 在后面的章节中,我们将介绍分类问题。分类问题的目标是预测数据属于一组类别中的哪一个。
线性回归
线性回归的基本元素
线性回归(linear regression)可以追溯到19世纪初, 它在回归的各种标准工具中最简单而且最流行。 线性回归基于几个简单的假设: 首先,假设自变量x和因变量y之间的关系是线性的, 即y可以表示为x中元素的加权和,这里通常允许包含观测值的一些噪声; 其次,我们假设任何噪声都比较正常,如噪声遵循正态分布。
为了解释_线性回归_,我们举一个实际的例子: 我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。 为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。 这个数据集包括了房屋的销售价格、面积和房龄。
在机器学习的术语中,该数据集称为_训练数据集_(training data set) 或_训练集_(training set)。
每行数据(比如一次房屋交易相对应的数据)称为_样本_(sample), 也可以称为_数据点 _(data point)或_数据样本 _(data instance)。
我们把试图预测的目标(比如预测房屋价格)称为_标签_(label)或_目标 _(target)。
预测所依据的自变量(面积和房龄)称为_特征_(feature)或_协变量 _(covariate)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBEaIir8-1673764739910)(image/image_AN4vNwK1j1.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pVoemAYT-1673764739914)(image/image_QkLP9mOKK9.png)]
线性模型可以看作一个单层的神经网络
我们将线性回归模型描述为一个神经网络。 需要注意的是,该图只显示连接模式,即只显示每个输入如何连接到输出,隐去了权重和偏置的值。
训练数据
参数学习
显示解
线性模型
损失函数
在我们开始考虑如何用模型_拟合_(fit)数据之前,我们需要确定一个拟合程度的度量。 损失函数(loss function)能够量化目标的_实际_值与_预测_值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。 回归问题中最常用的损失函数是平方误差函数。 当样本i的预测值为 y ^ ( i ) \hat{y}^{(i)} y^(i),其相应的真实标签为 y ( i ) y^{(i)} y(i)时, 平方误差可以定义为以下公式:
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.
由于训练数据集并不受我们控制,所以经验误差只是关于模型参数的函数。 为了进一步说明,来看下面的例子。 我们为一维情况下的回归问题绘制图像。
由于平方误差函数中的二次方项, 估计值 y ^ ( i ) \hat{y}^{(i)} y^(i)和观测值 y ( i ) y^{(i)} y(i)之间较大的差异将导致更大的损失。 为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和)。
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)=n1∑i=1nl(i)(w,b)=n1∑i=1n21(w⊤x(i)+b−y(i))2.
在训练模型时,我们希望寻找一组参数 ( w ∗ , b ∗ ) \left(\mathbf{w}^{*}, b^{*}\right) (w∗,b∗), 这组参数能最小化在所有训练样本上的总损失。如下式:
w ∗ , b ∗ = argmin w , b L ( w , b ) . \mathbf{w}^{*}, b^{*}=\underset{\mathbf{w}, b}{\operatorname{argmin}} L(\mathbf{w}, b). w∗,b∗=w,bargminL(w,b).
解析解
随机梯度下降
矢量化加速
在训练我们的模型时,我们经常希望能够同时处理整个小批量的样本。 为了实现这一点,需要我们对计算进行矢量化, 从而利用线性代数库,而不是在Python中编写开销高昂的for循环。
%matplotlib inline
import math
import time
import numpy as np
import torch
from d2l import torch as d2l
为了说明矢量化为什么如此重要,我们考虑对向量相加的两种方法。
我们实例化两个全为1的10000维向量。 在一种方法中,我们将使用Python的for循环遍历向量; 在另一种方法中,我们将依赖对+
的调用。
n = 10000
a = torch.ones([n])
b = torch.ones([n])
我们将频繁地进行运行时间的基准测试,所以我们定义一个计时器:
class Timer: #@save
"""记录多次运行时间"""
def __init__(self):
self.times = []
self.start()
def start(self):
"""启动计时器"""
self.tik = time.time()
def stop(self):
"""停止计时器并将时间记录在列表中"""
self.times.append(time.time() - self.tik)
return self.times[-1]
def avg(self):
"""返回平均时间"""
return sum(self.times) / len(self.times)
def sum(self):
"""返回时间总和"""
return sum(self.times)
def cumsum(self):
"""返回累计时间"""
return np.array(self.times).cumsum().tolist()
现在我们可以对工作负载进行基准测试。
首先,我们使用for循环,每次执行一位的加法。
c = torch.zeros(n)
timer = Timer()
for i in range(n):
c[i] = a[i] + b[i]
f'{timer.stop():.5f} sec'
#----------输出结果-----------
'0.12068 sec'
或者,我们使用重载的+
运算符来计算按元素的和。
timer.start()
d = a + b
f'{timer.stop():.5f} sec'
#----------输出结果-----------
'0.00000 sec'
结果很明显,第二种方法比第一种方法快得多。 矢量化代码通常会带来数量级的加速。 另外,我们将更多的数学运算放到库中,而无须自己编写那么多的计算,从而减少了出错的可能性。
正态分布与平方损失
接下来,我们通过对噪声分布的假设来解读平方损失目标函数。
正态分布和线性回归之间的关系很密切。 正态分布(normal distribution),也称为_高斯分布_(Gaussian distribution), 最早由德国数学家高斯(Gauss)应用于天文学研究。 简单的说,若随机变量x具有均值μ和方差σ2(标准差σ),其正态分布概率密度函数如下:
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)
下面我们定义一个Python函数来计算正态分布。
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)
我们现在可视化正态分布。
# 再次使用numpy进行可视化
x = np.arange(-7, 7, 0.01)
# 均值和标准差对
params = [(0, 1), (0, 2), (3, 1)]
d2l.plot(x, [normal(x, mu, sigma) for mu, sigma in params], xlabel='x',
ylabel='p(x)', figsize=(4.5, 2.5),
legend=[f'mean {mu}, std {sigma}' for mu, sigma in params])
优化方法
梯度下降
选择学习率
学习率不能太大也不能太小,人为选择
小批量随机梯度下降
选择批量大小
不能太小:每次计算量太小,不适合并行来最大利用计算资源
不能太大:内存消耗增加,浪费计算。例如所有的样本都是相同的
总结
- 梯度下降通过不断沿着反梯度方向更新参数求解
- 小批量随机梯度下降是深度学习默认的求解算法
- 两个重要的超参数是批量大小和学习率
线性回归的从零开始实现
在了解线性回归的关键思想之后,我们可以开始通过代码来动手实现线性回归了。 在这一节中,我们将从零开始实现整个方法, 包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。 虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保我们真正知道自己在做什么。 同时,了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。 在这一节中,我们将只使用张量和自动求导。 在之后的章节中,我们会充分利用深度学习框架的优势,介绍更简洁的实现方式。
%matplotlib inline
import random
import torch
from d2l import torch as d2l
生成数据集
为了简单起见,我们将根据带有噪声的线性模型构造一个人造数据集。 我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。 我们将使用低维数据,这样可以很容易地将其可视化。
在下面的代码中,我们生成一个包含1000个样本的数据集, 每个样本包含从标准正态分布中采样的2个特征。 我们的合成数据集是一个矩阵 X ∈ R 1000 × 2 \mathbf{X}\in \mathbb{R}^{1000 \times 2} X∈R1000×2。
def synthetic_data(w, b, num_examples): #@save
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
这是一个名为 synthetic_data()
的函数,用于生成用于训练线性回归模型的虚拟数据。
它有三个参数:
w
:线性模型的权重b
:线性模型的偏移量num_examples
:要生成的样本数
它的工作流程如下:
- 使用
torch.normal()
函数生成均值为 0,标准差为 1 的随机数组X
。数组的形状为(num_examples
,len(w)
)。
📌形状为(
num_examples
,len(w)
)的数组是一个二维数组。它的第一维大小为num_examples
,表示数组包含的样本数。第二维大小为len(w)
,表示数组包含的特征数。
- 使用线性模型的权重
w
和偏移量b
计算y
。具体来说,y=Xw+b
。 - 使用
torch.normal()
函数为每个样本添加噪声。噪声的均值为 0,标准差为 0.01。 - 返回生成的输入数组
X
和标签数组y
。
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
注意,features
中的每一行都包含一个二维数据样本, labels
中的每一行都包含一维标签值(一个标量)。
首先创建了两个张量 true_w
和 true_b
。
true_w
是一个长度为 2 的张量,包含两个数字 2 和 -3.4。
true_b
是一个数字 4.2。
然后,它调用了前面定义的 synthetic_data()
函数,生成了虚拟数据。这个函数接受三个参数:线性模型的权重 true_w
,偏移量 true_b
和样本数 1000
。它返回两个数组:输入数组 features
和标签数组 labels
。
features
数组是一个形状为(1000,2)的张量,包含 1000 个样本,每个样本有 2 个特征。
labels
数组是一个形状为(1000,1)的张量,包含 1000 个样本的标签
print('features:', features[0],'\nlabel:', labels[0])
#----------输出结果-----------
features: tensor([0.2525, 0.7209])
label: tensor([2.2487])
通过生成第二个特征features[:, 1]
和labels
的散点图, 可以直观观察到两者之间的线性关系。
features
数组的第一个样本和 labels
数组的第一个样本是对应的,即第一个样本的特征和标签是一组。
d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BKhjxcuh-1673764739920)(image/image_MiudYXJst-.png)]
读取数据集
回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。
在下面的代码中,我们定义一个data_iter
函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size
的小批量。 每个小批量包含一组特征和标签。
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]
这是一个名为 data_iter()
的函数,用于生成输入数据的迭代器。
它有三个参数:
batch_size
:批量大小。features
:输入数据的特征数组。labels
:输入数据的标签数组。
它的工作流程如下:
- 计算输入数据的样本数
num_examples
。 - 创建样本索引的列表
indices
。 - 打乱样本的顺序,使得它们的顺序是随机的。
- 使用循环,逐个读取批量的样本。
- 使用
yield
语句返回当前批量的特征数组和标签数组。
这段代码中的 indices[i: min(i + batch_size, num_examples)]
是一个切片操作,它用于从索引列表 indices
中选取一个连续的子序列。
i: min(i + batch_size, num_examples)
表示从索引 i
开始,选取连续的索引。这个子序列的长度为 batch_size
,但是如果 i + batch_size
大于 num_examples
,那么这个子序列就不会包含最后几个索引。例如,如果 num_examples=10
,batch_size=3
和 i=5
,那么这个切片操作的结果就是 [5, 6, 7]。
通常,我们利用GPU并行运算的优势,处理合理大小的“小批量”。 每个样本都可以并行地进行模型计算,且每个样本损失函数的梯度也可以被并行计算。 GPU可以在处理几百个样本时,所花费的时间不比处理一个样本时多太多。
我们直观感受一下小批量运算:读取第一个小批量数据样本并打印。 每个批量的特征维度显示批量大小和输入特征数。 同样的,批量的标签形状与batch_size
相等。
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break
#----------输出结果-----------
tensor([[ 0.0221, 0.2320],
[ 0.6794, -0.5034],
[ 0.1363, -0.9847],
[ 0.4438, -0.1550],
[-0.8514, 0.1900],
[ 0.0214, 0.9961],
[-1.1829, 0.7807],
[-1.0912, 1.1282],
[ 0.3664, -1.3300],
[-0.5001, 0.6672]])
tensor([[ 3.4437],
[ 7.2608],
[ 7.8031],
[ 5.6222],
[ 1.8395],
[ 0.8499],
[-0.8188],
[-1.8158],
[ 9.4456],
[ 0.9353]])
当我们运行迭代时,我们会连续地获得不同的小批量,直至遍历完整个数据集。 上面实现的迭代对教学来说很好,但它的执行效率很低,可能会在实际问题上陷入麻烦。 例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多, 它可以处理存储在文件中的数据和数据流提供的数据。
初始化模型参数
在我们开始用小批量随机梯度下降优化我们的模型参数之前, 我们需要先有一些参数。 在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
这段代码中使用了 PyTorch 库中的 normal()
函数和 zeros()
函数分别创建了一个服从正态分布的张量和一个全 0 张量。
normal()
函数的参数有:
0
:均值。0.01
:标准差。size=(2,1)
:张量的形状。这里的形状是 (2,1),表示这个张量有 2 行和 1 列。requires_grad=True
:是否要跟踪这个张量的梯度。如果设为True
,那么这个张量在反向传播中会自动计算梯度。
zeros()
函数的参数有:
1
:张量的形状。这里的形状是 (1,),表示这个张量有 1 个元素。requires_grad=True
:是否要跟踪这个
在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损失的方向更新每个参数。
定义模型
接下来,我们必须定义模型,将模型的输入和参数同模型的输出关联起来。
def linreg(X, w, b): #@save
"""线性回归模型"""
return torch.matmul(X, w) + b
这是一个名为 linreg()
的函数,它实现了一个线性回归模型。
它有三个参数:
X
:输入数据的特征数组。w
:模型的权重参数。b
:模型的偏移参数。
它的工作流程如下:
- 使用 PyTorch 库中的
matmul()
函数计算矩阵乘法。 - 将矩阵乘法的结果加上偏移参数
b
。 - 返回结果。
这个函数的输入是输入数据的特征数组和模型的权重和偏移参数,输出是线性回归模型的预测结果。
定义损失函数
因为需要计算损失函数的梯度,所以我们应该先定义损失函数
def squared_loss(y_hat, y): #@save
"""均方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
这是一个名为 squared_loss()
的函数,它实现了均方损失的计算。
它有两个参数:
y_hat
:模型的预测结果。y
:真实的标签。
它的工作流程如下:
- 将预测结果与真实标签的差平方。
- 将平方的结果除以 2。
- 返回结果。
这个函数的输入是模型的预测结果和真实的标签,输出是均方损失的值。
定义优化算法
在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr
决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size
) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
def sgd(params, lr, batch_size): #@save
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
这是一个名为 sgd()
的函数,它实现了小批量随机梯度下降 (SGD) 算法。
它有三个参数:
params
:模型的参数列表。lr
:学习率。batch_size
:批量大小。
它的工作流程如下:
- 使用 PyTorch 库中的
no_grad()
函数创建一个上下文管理器。 - 使用循环,逐个处理模型的参数。
- 使用梯度下降的更新规则更新参数的值。
- 使用
zero_()
函数清空参数的梯度。
这个函数的作用是:对于给定的模型参数,使用 SGD 算法进行一次梯度下降。
训练
现在我们已经准备好了模型训练所有需要的要素,可以实现主要的训练过程部分了。 理解这段代码至关重要,因为从事深度学习后, 相同的训练过程几乎一遍又一遍地出现。 在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。 最后,我们调用优化算法sgd
来更新模型参数。
概括一下,我们将执行以下循环:
- 初始化参数
- 重复以下训练,直到完成
- 计算梯度 g ← ∂ ( w , b ) 1 ∣ B ∣ ∑ i ∈ B l ( x ( i ) , y ( i ) , w , b ) \mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b) g←∂(w,b)∣B∣1∑i∈Bl(x(i),y(i),w,b)
- 更新参数 ( w , b ) ← ( w , b ) − η g (\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g} (w,b)←(w,b)−ηg
在每个_迭代周期_(epoch)中,我们使用data_iter
函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。 这里的迭代周期个数num_epochs
和学习率lr
都是超参数,分别设为3和0.03。
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}')
#------------输出结果------------
epoch 1, loss 0.041482
epoch 2, loss 0.000166
epoch 3, loss 0.000049
这是一段训练模型的代码。它包含了两个循环:
- 第一个循环按照周期数重复迭代数据。
- 第二个循环按照批量大小迭代数据。
每个周期包含多个批次,在每个批次中,它会执行以下操作:
- 计算当前批次的损失值。
- 计算当前批次的梯度。
- 使用 SGD 算法更新模型的参数。
在每个周期结束时,它会计算整个训练集的损失值,并将其打印出来。
整个训练过程包括多个周期,每个周期包含多个批次,每个批次包含多个样本。
因为我们使用的是自己合成的数据集,所以我们知道真正的参数是什么。 因此,我们可以通过比较真实参数和通过训练学到的参数来评估训练的成功程度。 事实上,真实参数和通过训练学到的参数确实非常接近。
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
#------------输出结果------------
w的估计误差: tensor([ 0.0003, -0.0002], grad_fn=<SubBackward0>)
b的估计误差: tensor([0.0010], grad_fn=<RsubBackward1>)
线性回归的简介实现
生成数据集
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)
def synthetic_data(w, b, num_examples): #@save
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
读取数据集
我们可以调用框架中现有的API来读取数据。 我们将features
和labels
作为API的参数传递,并通过数据迭代器指定batch_size
。 此外,布尔值is_train
表示是否希望数据迭代器对象在每个迭代周期内打乱数据。
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)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
next(iter(data_iter))
# ---------------输出结果------------
[tensor([[ 0.6718, 0.5967],
[-1.2405, -0.6843],
[-1.3672, 0.0093],
[-0.1689, 0.5642],
[-0.1460, -1.2817],
[ 0.8564, 0.5281],
[ 1.2138, 0.6259],
[-0.7343, -0.1812],
[ 1.0611, 0.7354],
[ 0.0861, -0.0404]]),
tensor([[3.5074],
[4.0387],
[1.4269],
[1.9447],
[8.2659],
[4.1071],
[4.4991],
[3.3432],
[3.8256],
[4.5022]])]
这段代码用于创建一个 PyTorch 数据迭代器。
它的工作流程如下:
- 将输入数据转换为 PyTorch 的
TensorDataset
格式。 - 使用
DataLoader
函数创建一个数据迭代器。 - 使用
iter()
函数获取数据迭代器的迭代器。 - 使用
next()
函数获取数据迭代器的下一个批次数据。
在这段代码中,输入数据是 features
和 labels
两个张量,批量大小是 batch_size
。
这个数据迭代器每次迭代时会返回一个批次的输入数据和标签,用于训练模型。
dataset = data.TensorDataset(*data_arrays)
其中这句代码创建了一个 TensorDataset
类的实例。这个类是 PyTorch 中的数据集类,用于将数据保存在张量中。
在这句代码中,*data_arrays
是 Python 中的打散参数 (unpacking arguments) 特性。它将 data_arrays
这个包含多个张量的元组,打散成多个单独的张量作为参数传入 TensorDataset
类的构造函数。
例如,如果 data_arrays
是 (tensor_1, tensor_2, tensor_3)
这样的元组,那么 *data_arrays
将被解释为 tensor_1, tensor_2, tensor_3
,这样就可以直接传入 TensorDataset
类的构造函数了。
定义模型
对于标准深度学习模型,我们可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。 我们首先定义一个模型变量net
,它是一个Sequential
类的实例。 Sequential
类将多个层串联在一起。 当给定输入数据时,Sequential
实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。 在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential
。 但是由于以后几乎所有的模型都是多层的,在这里使用Sequential
会让你熟悉“标准的流水线”。
这一单层被称为全连接层(fully-connected layer), 因为它的每一个输入都通过矩阵-向量乘法得到它的每个输出。
在PyTorch中,全连接层在Linear
类中定义。 值得注意的是,我们将两个参数传递到nn.Linear
中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
这段代码创建了一个线性回归模型。线性回归模型是一种简单的神经网络模型,它只包含一个全连接层 (fully-connected layer),用于对输入数据进行线性变换。
具体来说,这段代码创建了一个 Sequential
类的实例,该实例是一个多层感知机 (multilayer perceptron, MLP)。它包含一个输入层和一个输出层,输入层的大小是 2,输出层的大小是 1。
这个模型的输入数据是 2 维的,输出是 1 维的。它可以用于对 2 维输入数据进行线性回归。
初始化模型参数
在使用net
之前,我们需要初始化模型参数。
如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样, 偏置参数将初始化为零。
正如我们在构造nn.Linear
时指定输入和输出尺寸一样, 现在我们能直接访问参数以设定它们的初始值。 我们通过net[0]
选择网络中的第一个图层, 然后使用weight.data
和bias.data
方法访问参数。 我们还可以使用替换方法normal_
和fill_
来重写参数值。
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
# ---------------输出结果------------
tensor([0.])
这段代码对线性回归模型的参数进行初始化。
在这段代码中,net[0]
表示线性回归模型的第一层,也就是唯一的一层。这一层包含两个参数:权重 (weight) 和偏差 (bias)。
weight.data.normal_(0, 0.01)
用于将权重初始化为均值为 0,标准差为 0.01 的随机数。
bias.data.fill_(0)
用于将偏差初始化为 0。
通常来说,我们需要对模型的参数进行初始化,以便模型在训练时能够更快收敛。对参数进行随机初始化可以帮助避免梯度消失或梯度爆炸的问题。
定义损失函数
计算均方误差使用的是MSELoss
类,也称为平方L2范数。 默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()
定义优化函数
小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim
模块中实现了该算法的许多变种。 当我们实例化一个SGD
实例时,我们要指定优化的参数 (可通过net.parameters()
从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr
值,这里设置为0.03。
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
训练
通过深度学习框架的高级API来实现我们的模型只需要相对较少的代码。 我们不必单独分配参数、不必定义我们的损失函数,也不必手动实现小批量随机梯度下降。 当我们需要更复杂的模型时,高级API的优势将大大增加。 当我们有了所有的基本组件,训练过程代码与我们从零开始实现时所做的非常相似。
回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(train_data
), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:
- 通过调用
net(X)
生成预测并计算损失l
(前向传播)。 - 通过进行反向传播来计算梯度。
- 通过调用优化器来更新模型参数。
为了更好的衡量训练效果,我们计算每个迭代周期后的损失,并打印它来监控训练过程。
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y)
trainer.zero_grad()
l.backward()
trainer.step()
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')
# ---------------输出结果------------
epoch 1, loss 0.000271
epoch 2, loss 0.000098
epoch 3, loss 0.000098
这段代码实现了线性回归模型的训练过程。
训练过程分为多个 epoch,每个 epoch 中包含多个批次 (batch)。在每个批次中,首先使用线性回归模型对输入数据进行预测,然后计算预测结果与真实标签之间的损失。
然后使用反向传播算法(backpropagation)计算每个参数的梯度,并更新参数。最后使用更新后的参数再次预测,计算并输出当前 epoch 的损失。
下面我们比较生成数据集的真实参数和通过有限数据训练获得的模型参数。 要访问参数,我们首先从net
访问所需的层,然后读取该层的权重和偏置。 正如在从零开始实现中一样,我们估计得到的参数与生成数据的真实参数非常接近。
w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)
# ---------------输出结果------------
w的估计误差: tensor([-0.0002, -0.0008])
b的估计误差: tensor([0.0002])
这段代码用于输出训练后线性回归模型的参数,以及这些参数与真实参数之间的误差。
net[0].weight.data
表示线性回归模型的权重,net[0].bias.data
表示线性回归模型的偏差。
true_w
和 true_b
是真实参数,用于生成训练数据。
输出的结果表明,训练后的线性回归模型的参数与真实参数之间的误差很小。
softmax回归
回归可以用于预测_多少_的问题。 比如预测房屋被售出价格,或者棒球队可能获得的胜场数,又或者患者住院的天数。
事实上,我们也对_分类_问题感兴趣:不是问“多少”,而是问“哪一个”:
- 某个电子邮件是否属于垃圾邮件文件夹?
- 某个用户可能_注册_或_不注册_订阅服务?
- 某个图像描绘的是驴、狗、猫、还是鸡?
- 某人接下来最有可能看哪部电影?
通常,机器学习实践者用_分类_这个词来描述两个有微妙差别的问题:
1. 我们只对样本的“硬性”类别感兴趣,即属于哪个类别;
2. 我们希望得到“软性”类别,即得到属于每个类别的概率。 这两者的界限往往很模糊。其中的一个原因是:即使我们只关心硬类别,我们仍然使用软类别的模型。
分类问题
网络架构
为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。 为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。
在我们的例子中,由于我们有4个特征和3个可能的输出类别, 我们将需要12个标量来表示权重(带下标的 w w w), 3个标量来表示偏置(带下标的 b b b)。 下面我们为每个输入计算三个未规范化的预测(logit): o 1 o_1 o1、 o 2 o_2 o2和 o 3 o_3 o3。
softmax运算
现在我们将优化参数以最大化观测数据的概率。 为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。
要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。 此外,我们需要一个训练的目标函数,来激励模型精准地估计概率。 例如, 在分类器输出0.5的所有样本中,我们希望这些样本是刚好有一半实际上属于预测的类别。 这个属性叫做校准(calibration)。
社会科学家邓肯·卢斯于1959年在选择模型(choice model)的理论基础上 发明的softmax函数正是这样做的: softmax函数能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持 可导的性质。 为了完成这一目标,我们首先对每个未规范化的预测求幂,这样可以确保输出非负。 为了确保最终输出的概率值总和为1,我们再让每个求幂后的结果除以它们的总和。如下式:
y ^ = softmax ( o ) 其中 y ^ j = exp ( o j ) ∑ k exp ( o k ) \hat{\mathbf{y}}=\operatorname{softmax}(\mathbf{o}) \quad 其中 \quad \hat{y}_{j}=\frac{\exp \left(o_{j}\right)}{\sum_{k} \exp \left(o_{k}\right)} y^=softmax(o)其中y^j=∑kexp(ok)exp(oj)
小批量样本的矢量化
损失函数
L2 Loss均方损失函数
L1 Loss绝对值损失函数
Huber’s Robust Loss鲁棒损失函数
对数似然
softmax及其导数
模型的预测和评估
在训练softmax回归模型后,给出任何样本特征,我们可以预测每个输出类别的概率。 通常我们使用预测概率最高的类别作为输出类别。 如果预测与实际类别(标签)一致,则预测是正确的。 在接下来的实验中,我们将使用精度(accuracy)来评估模型的性能。 精度等于正确预测数与预测总数之间的比率。
图像分类数据集
MNIST数据集 (LeCun et al., 1998) 是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。 我们将使用类似但更复杂的Fashion-MNIST数据集 (Xiao et al., 2017)。
%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数据集下载并读取到内存中。
# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
# 并除以255使得所有像素的数值均在0~1之间
trans = transforms.ToTensor() # 预处理:把图片转换为pytorch的tensor
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)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FHUrY3r2-1673764739924)(image/image_PmKwQ2RcZP.png)]
这个代码分别使用 torchvision.datasets.FashionMNIST
函数加载了训练数据和测试数据。其中,参数 root
指定了数据所在的文件夹,参数 train
指定是加载训练数据还是测试数据,参数 transform
指定了对数据进行的变换,在这里使用了 transforms.ToTensor
函数将图像数据从 PIL
类型变换成32位浮点数格式,并除以255使得所有像素的数值均在0~1之间。参数 download
指定是否自动下载数据。
📌Fashion-MNIST由10个类别的图像组成, 每个类别由训练数据集(train dataset)中的6000张图像 和测试数据集(test dataset)中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。 测试数据集不会用于训练,只用于评估模型性能。
len(mnist_train), len(mnist_test)
输出
(60000, 10000)
每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 为了简洁起见,本书将高度h像素、宽度w像素图像的形状记为 h × w h \times w h×w或 ( h , w ) (h,w) (h,w)。
# 查看第一张图片的shape
mnist_train[0][0].shape
输出
torch.Size([1, 28, 28])
Fashion-MNIST中包含的10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。 以下函数用于在数字标签索引及其文本名称之间进行转换**。**
def get_fashion_mnist_labels(labels): #@save
"""返回Fashion-MNIST数据集的文本标签"""
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
这段代码定义了一个函数 get_fashion_mnist_labels
,用于将 Fashion-MNIST 数据集的数字标签转换为文本标签。具体来说,该函数接受一个数字标签的列表,并返回一个与输入列表长度相同的文本标签列表。 在函数内部,定义了一个文本标签列表text_labels
,包含了 Fashion-MNIST 数据集中的 10 个类别,即 t-shirt、trouser、pullover、dress、coat、sandal、shirt、sneaker、bag 和 ankle boot。然后,使用列表推导式将输入的数字标签列表中的每个元素转换为文本标签,并返回转换后的文本标签列表。
我们现在可以创建一个函数来可视化这些样本。
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save
"""绘制图像列表"""
figsize = (num_cols * scale, num_rows * scale) # 图像的长和宽
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
# 图片张量
ax.imshow(img.numpy())
else:
# PIL图片
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes
这个函数的作用是将一个列表中的图像绘制到多个子图中,num_rows 和 num_cols 参数指定了将图像绘制成几行几列的矩阵。imags 参数是图像列表,titles 参数是图像标题列表,scale 参数是每个子图的高度和宽度与默认值的比值。在循环中,对于每个子图,首先判断图像是否是一个张量,如果是的话使用 imshow 函数绘制张量的内容,否则直接绘制 PIL 图像的内容。最后,如果传入了标题列表,就设置子图的标题。
d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
函数返回一个包含画板和轴的二元组。这个函数会创建一个新的画板,并将其分成 num_rows
行和 num_cols
列的网格。然后,它会在每个网格中创建一个轴。这个函数返回的第一个值是画板本身,第二个值是一个轴数组,它是一个二维数组,第 i 行第 j 列对应网格中的第 i 行第 j 列的轴。
axes = axes.flatten()
函数将二维数组展平成一维数组,即把所有轴都放在一个列表中。
在循环中,对于每个子图和图像对,它会检查图像是否是张量。如果是,则使用imshow函数绘制张量中的图像。否则,使用imshow绘制PIL图像。然后,它会隐藏子图的坐标轴,并如果有标题,则将标题添加到子图中。最后,函数返回子图矩阵。
以下是训练数据集中前几个样本的图像及其相应的标签。
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W9rqVBX1-1673764739924)(image/image__PlVdGW80H.png)]
这段代码的作用是从 Fashion-MNIST 训练集中取出一个批次的数据,并使用 show_images 函数将这批数据中的图像可视化出来。具体来说,首先使用 data.DataLoader 函数从 mnist_train 中取出一个批次的数据。然后使用 X.reshape 将每张图像的张量重塑为二维张量,并传入 show_images 函数进行可视化。可视化结果中,每张图像都有一个对应的标签,使用 get_fashion_mnist_labels 函数将标签的数字索引转换为文本名称。
读取小批量
为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建。 回顾一下,在每次迭代中,数据加载器每次都会读取一小批量数据,大小为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())
此代码创建了一个PyTorch的DataLoader,用于在训练期间从FashionMNIST数据集中加载小批量数据。DataLoader提供了一个更方便的方法来遍历数据集中的所有样本,并在训练时提供批量数据。参数batch_size定义了每个批量的大小,参数shuffle表示是否在每个epoch之间对数据进行洗牌,num_workers定义了使用几个进程来加载数据。
我们看一下读取训练数据所需的时间。
timer = d2l.Timer()
for X, y in train_iter:
continue
f'{timer.stop():.2f} sec'
输出
'2.18 sec'
这个代码创建了一个数据加载器,用于读取Fashion-MNIST训练数据集中的每个批次。训练数据集中的每个样本都将打包成一个批次。这个代码还使用了一个定时器来测量数据加载器读取一个批次的时间。
整合所有组件
现在我们定义load_data_fashion_mnist
函数,用于获取和读取Fashion-MNIST数据集。 这个函数返回训练集和验证集的数据迭代器。 此外,这个函数还接受一个可选参数resize
,用来将图像大小调整为另一种形状。
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()))
这段代码用来下载并加载Fashion-MNIST数据集。它通过使用torchvision的FashionMNIST数据集类实现,并将数据集的训练集和测试集读取到内存中。首先,使用transforms.ToTensor()将数据从PIL类型转换为张量,并将像素值除以255使得值域在0~1之间。如果传入了resize参数,则使用transforms.Resize()将图像的大小调整为指定大小。最后,使用transforms.Compose()将所有转换组成一个序列,然后传给FashionMNIST类的transform参数。
最后,返回由两个DataLoader实例组成的元组,分别对应训练集和测试集。对于训练集,shuffle参数设置为True,表示将数据打乱。在测试集上,shuffle参数设置为False,表示保持数据顺序。num_workers参数指定使用4个进程来读取数据,可以加快数据加载速度。
下面,我们通过指定resize
参数来测试load_data_fashion_mnist
函数的图像大小调整功能。
train_iter, test_iter = load_data_fashion_mnist(32, resize=64)
for X, y in train_iter:
print(X.shape, X.dtype, y.shape, y.dtype)
break
输出
torch.Size([32, 1, 64, 64]) torch.float32 torch.Size([32]) torch.int64
输出为(32, 1, 64, 64) torch.float32 (32,) torch.int64,表示训练集中包含32个样本,每个样本为1个通道、64x64像素的张量,标签为32个整数。
我们现在已经准备好使用Fashion-MNIST数据集,便于下面的章节调用来评估各种分类算法。
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的向量。 在后面的章节中,我们将讨论能够利用图像空间结构的特征, 但现在我们暂时只把每个像素位置看作一个特征。
回想一下,在softmax回归中,我们的输出与类别一样多。** 因为我们的数据集有10个类别,所以网络输出维度为10。** 因此,权重将构成一个784×10的矩阵, 偏置将构成一个1×10的行向量。 与线性回归一样,我们将使用正态分布初始化我们的权重W
,偏置初始化为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)
这段代码中,num_inputs
和num_outputs
是用来指定模型输入和输出的维度的。具体来说,num_inputs
指的是输入数据的特征数量,而num_outputs
指的是模型的输出类别数量。在这里,num_inputs
被赋值为784,num_outputs
被赋值为10。
然后,代码中的W和b是模型的权重和偏移量。它们都是PyTorch张量,并且都有requires_grad参数被设置为True,表示这些张量需要被追踪,以便在反向传播时计算梯度。W张量是一个形状为(num_inputs, num_outputs)的矩阵,b张量是一个长度为num_outputs的向量。W和b张量的初始值都是随机初始化的,使用均值为0、标准差为0.01的正态分布随机生成的。
定义softmax操作
在实现softmax回归模型之前,我们简要回顾一下sum
运算符如何沿着张量中的特定维度工作。
给定一个矩阵X
,我们可以对所有元素求和(默认情况下)。 也可以只求同一个轴上的元素,即同一列(轴0)或同一行(轴1)。 如果X
是一个形状为(2, 3)
的张量,我们对列进行求和, 则结果将是一个具有形状(3,)
的向量。 当调用sum
运算符时,我们可以指定保持在原始张量的轴数,而不折叠求和的维度。 这将产生一个具有形状(1, 3)
的二维张量。
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)
输出
(tensor([[5., 7., 9.]]),
tensor([[ 6.],
[15.]]))
回想一下,[实现softmax]由三个步骤组成:
- 对每个项求幂(使用
exp
); - 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
- 将每一行除以其规范化常数,确保结果的和为1。
softmax ( X ) i j = exp ( X i j ) ∑ k exp ( X i k ) \operatorname{softmax}(\mathbf{X})_{i j}=\frac{\exp \left(\mathbf{X}_{i j}\right)}{\sum_{k} \exp \left(\mathbf{X}_{i k}\right)} softmax(X)ij=∑kexp(Xik)exp(Xij)
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制
正如上述代码,对于任何随机输入,我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1。
# 创建随机的均值为0,方差为1,2行5列的矩阵x
X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
在这个代码中,我们定义了一个名为softmax
的函数,这个函数接受一个名为X
的矩阵,并将它转换为概率分布。它通过对矩阵X
中的每个元素求幂,确保所有值都是正数,然后将指数化的值除以它们的和来归一化。这在构建概率分布或用于分类任务的输出(可以解释为概率)时非常有用。
例如,在多类分类的情况下,可以将模型的输出通过softmax
函数传递,以获得类别的概率分布,其中预测的类别是概率最高的类别。
在给定的代码中,对矩阵X
求幂,然后将结果矩阵按其行元素的和(即沿着列)进行除法。sum
方法的keepdim
参数指定结果矩阵应与输入矩阵具有相同的维数,其中沿指定轴的元素的和在结果矩阵中是单个元素。这是为了确保可以逐元素应用除法操作。
最后,将打印出结果矩阵X_prob
和它的行和。由于softmax
函数的输出是概率分布,因此行和应该等于1。
输出
(tensor([[0.0699, 0.0579, 0.0616, 0.1511, 0.6596],
[0.0676, 0.1175, 0.4517, 0.2931, 0.0700]]),
tensor([1.0000, 1.0000]))
注意,虽然这在数学上看起来是正确的,但我们在代码实现中有点草率。 矩阵中的非常大或非常小的元素可能造成数值上溢或下溢,但我们没有采取措施来防止这点。
定义模型
定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape
函数将每张原始图像展平为向量。
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
这个代码定义了一个名为net
的函数,该函数接受一个名为X
的张量作为输入,并返回由softmax
函数计算得到的概率分布。
首先,将张量X
的形状重新调整为具有两个维度的形状,其中第一维大小是-1,第二维大小为另一个张量W
的第一维大小。这里的reshape
方法可以用来重新调整张量的形状。其中,使用-1作为第一维的大小意味着这一维的大小将根据输入张量的总元素数和新形状的其他维度的大小自动计算。
接下来,使用矩阵乘法将调整后的张量X
与另一个张量W
相乘,然后将结果与另一个张量b
相加。然后,将结果传递给softmax
函数,并将计算的概率分布作为函数的输出返回。
定义损失函数
接下来,我们实现交叉熵损失函数。 这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题的数量。
回顾一下,交叉熵采用真实标签的预测概率的负对数似然。 这里我们不使用Python的for循环迭代预测(这往往是低效的), 而是通过一个运算符选择所有元素。
下面,我们创建一个数据样本y_hat
,其中包含2个样本在3个类别的预测概率, 以及它们对应的标签y
。 有了y
,我们知道在第一个样本中,第一类是正确的预测; 而在第二个样本中,第三类是正确的预测。 然后使用y
作为y_hat
中概率的索引, 我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。
y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]
这段代码会输出一个包含两个元素的张量,这两个元素分别对应输入张量y_hat
的第一行第一列和第二行第三列的元素。
首先,我们有两个张量y
和y_hat
。y
是一个包含两个整数的张量,表示两行的索引,而y_hat
是一个2x3的张量,其中每行表示一个概率分布。
然后,我们使用索引操作符**[...]
来访问张量y_hat
**的每行。在这里,第一个索引[0, 1]指定了要选择的行,而第二个索引y指定了要选择的列。这会返回一个包含第一行第一列和第二行第三列元素的张量。
因此,输出为tensor([0.1, 0.5])
。
输出
tensor([0.1000, 0.5000])
现在我们只需一行代码就可以实现交叉熵损失函数。
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
cross_entropy(y_hat, y)
这个代码定义了一个名为cross_entropy
的函数,该函数计算给定的概率分布和标签的交叉熵。
交叉熵是用于度量两个概率分布之间的差异的常见度量。在分类任务中,通常使用真实标签的概率分布和预测标签的概率分布之间的交叉熵作为损失函数。交叉熵越小,两个概率分布之间的差异就越小,这意味着预测标签的概率分布更接近真实标签的概率分布。
我们对返回的张量求负对数,并将结果作为函数的输出返回。
最后,我们调用函数并传入概率分布和标签的张量作为参数,并打印出结果。输出应该是一个包含两个元素的张量,表示概率分布和标签之间的交叉熵。
输出
tensor([2.3026, 0.6931])
分类精度
给定预测概率分布y_hat
,当我们必须输出硬预测(hard prediction)时, 我们通常选择预测概率最高的类。 许多应用都要求我们做出选择。如Gmail必须将电子邮件分类为“Primary(主要邮件)”、 “Social(社交邮件)”“Updates(更新邮件)”或“Forums(论坛邮件)”。 Gmail做分类时可能在内部估计概率,但最终它必须在类中选择一个。
当预测与标签分类y
一致时,即是正确的。 分类精度即正确预测数量与总预测数量之比。 虽然直接优化精度可能很困难(因为精度的计算不可导), 但精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。
为了计算精度,我们执行以下操作。 首先,如果y_hat
是矩阵,那么假定第二个维度存储每个类的预测分数。 我们使用argmax
获得每行中最大元素的索引来获得预测类别。 然后我们将预测类别与真实y
元素进行比较。 由于等式运算符“==
”对数据类型很敏感, 因此我们将y_hat
的数据类型转换为与y
的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。
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())
这个函数用于计算分类任务的准确率。
输入参数:
- y_hat:模型预测的输出。
- y:真实的输出。
输出:
- float:预测正确的数量占总样本数的比例,即准确率。
流程:
- 判断 y_hat 的维度是否大于 1,如果是,将 y_hat 的最后一维按照最大值的索引取值。这是因为,如果 y_hat 为 one-hot 形式的输出,需要将它转换为单个数字编号的形式。
- 比较 y_hat 和 y 相等的数量,并将结果转换为浮点数返回。
我们将继续使用之前定义的变量y_hat
和y
分别作为预测的概率分布和标签。 可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。 第二个样本的预测类别是2(该行的最大元素为0.5,索引为2),这与实际标签2一致。 因此,这两个样本的分类精度率为0.5。
accuracy(y_hat, y) / len(y)
输出
0.5
同样,对于任意数据迭代器data_iter
可访问的数据集, 我们可以评估在任意模型net
的精度。
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]
这段代码定义了一个函数 evaluate_accuracy
,用于计算给定的神经网络在给定的数据迭代器上的精度。
首先,判断给定的神经网络是否是 torch.nn.Module
的实例。如果是,就将其设置为评估模式。
接下来,创建一个累加器 metric
,它有两个元素。然后使用 torch.no_grad()
上下文管理器禁用自动求导。 在这个上下文中,对数据迭代器中的每个输入数据和标签进行循环。每次循环中,调用函数 accuracy 计算输入数据的准确率,并将其添加到累加器 metric 中。最后,计算并返回累加器中正确预测数与预测总数之比。
函数的输入包括神经网络 net 和数据迭代器 data_iter。输出是神经网络在数据迭代器上的精度。
这里定义一个实用程序类Accumulator
,用于对多个变量进行累加。 在上面的evaluate_accuracy
函数中, 我们在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]
这是一个可以在多个变量上累加的辅助类。
该类包含一个列表,其中存储了当前的累加值。它有三个方法:
- add: 将给定的值分别与列表中对应位置的值相加,并将结果存回列表。
- reset: 将列表中所有值设为 0。
- getitem: 返回列表中指定位置的值。
例如,可以使用这个类来跟踪多个统计信息(如精度、召回率、F1 值)。
由于我们使用随机权重初始化net
模型, 因此该模型的精度应接近于随机猜测。 例如在有10个类别情况下的精度为0.1。
evaluate_accuracy(net, test_iter)
输出
0.1045
训练
在这里,我们重构训练过程的实现以使其可重复使用。 首先,我们定义一个函数来训练一个迭代周期。 请注意,updater
是更新模型参数的常用函数,它接受批量大小作为参数。 它可以是d2l.sgd
函数,也可以是框架的内置优化函数。
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]) # 表示输入数据的 batch size
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
这段代码定义了一个函数 train_epoch_ch3
,用于训练模型一个迭代周期。
首先,判断给定的神经网络是否是 torch.nn.Module
的实例。如果是,将其设置为训练模式。
然后,创建一个累加器 metric
,它有三个元素。对数据迭代器中的每个输入数据和标签进行循环。每次循环中,使用神经网络对输入数据进行预测,并计算预测值与标签之间的损失。
然后,判断给定的优化器是否是 PyTorch 内置的优化器。如果是,就使用 PyTorch 内置的优化器来更新神经网络的参数。否则,使用定制的优化器来更新参数。
最后,将计算的损失值、准确率和样本数加到累加器 metric
中。
函数的输入包括神经网络 net
、数据迭代器 train_iter
、损失函数 loss
和优化器 updater
。输出是训练损失和训练准确率。
在展示训练函数的实现之前,我们定义一个在动画中绘制数据的实用程序类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)
接下来我们实现一个训练函数, 它会在train_iter
访问到的训练数据集上训练一个模型net
。 该训练函数将会运行多个迭代周期(由num_epochs
指定)。 在每个迭代周期结束时,利用test_iter
访问到的测试数据集对模型进行评估。 我们将利用Animator
类来可视化训练进度。
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
这段代码定义了一个函数 train_ch3
,用于训练模型。输入参数包括神经网络 net
、训练数据迭代器 train_iter
、测试数据迭代器 test_iter
、损失函数 loss
、迭代周期数 num_epochs
和优化器 updater
。
首先,创建一个 Animator
对象 animator
。这个对象用于在训练过程中实时绘制训练损失和训练准确率以及测试准确率的图像。
然后,使用循环迭代 num_epochs
次,每次调用函数 train_epoch_ch3
训练一个迭代周期,并计算测试准确率。每次迭代完成后,将训练损失和训练准确率以及测试准确率的值传递给 Animator
对象的 add
方法,以便在图像中添加数据。
最后,函数 train_ch3
使用三个 assert
语句检查训练损失、训练准确率和测试准确率是否满足预期的要求。这些要求是:训练损失小于 0.5,训练准确率大于 0.7 且小于等于 1,测试准确率大于 0.7 且小于等于 1。如果这些要求满足,则函数会正常结束。如果有任意一个要求不满足,则会抛出 AssertionError
异常,并输出不符合要求的指标的值。
作为一个从零开始的实现,我们使用小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。
lr = 0.1
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
现在,我们训练模型10个迭代周期。 请注意,迭代周期(num_epochs
)和学习率(lr
)都是可调节的超参数。 通过更改它们的值,我们可以提高模型的分类精度。
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r5dE4cWV-1673764739925)(image/image_9_wplYYaxo.png)]
预测
现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。
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)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VMv5pdEB-1673764739925)(image/image_el_KNzg1fl.png)]
softmax回归的简洁实现
线性回归变得更加容易。 同样,通过深度学习框架的高级API也能更方便地实现softmax回归模型。 本节如在 3.6节中一样, 继续使用Fashion-MNIST数据集,并保持批量大小为256。
from mxnet import gluon, init, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化模型参数
softmax回归的输出层是一个全连接层。 因此,为了实现我们的模型, 我们只需在Sequential
中添加一个带有10个输出的全连接层。 同样,在这里Sequential
并不是必要的, 但它是实现深度模型的基础。 我们仍然以均值0和标准差0.01随机初始化权重。
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
这段代码中,首先定义了一个神经网络 net
。这个神经网络是一个序列(Sequential
),包含两个层:展平层(Flatten
)和线性层(Linear
)。
展平层用于将输入数据的形状从(batch_size, channel, height, width
)转换为(batch_size, channel_height_width
)。这样,线性层才能接受这些输入数据。
接下来定义了一个函数 init_weights
,用于初始化模型参数。函数 init_weights
使用 PyTorch 的内置函数 nn.init.normal_
将线性层的权重初始化为正态分布的随机数。
最后,使用 net
的 apply
方法,将函数 init_weights
应用到每一层的参数上。这样, **net
**的每一层的参数都会被初始化。
重新审视Softmax的实现
loss = nn.CrossEntropyLoss(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)
和以前一样,这个算法使结果收敛到一个相当高的精度,而且这次的代码比之前更精简了。