而在机器学习领域, 我们通常使用的是高维数据集, 建模时采用线性代数表示法会比较方便。当我们的输入 包含 ddd 个特征时,我们将预测结果 y^\hat{y}y^ (通常使用 “尖角”符号表示 yyy 的估计值)表示为:
y^=w1x1+…+wdxd+b.\hat{y}=w_1 x_1+\ldots+w_d x_d+b . y^=w1x1+…+wdxd+b.
将所有特征放到向量 x∈Rd\mathbf{x} \in \mathbb{R}^dx∈Rd 中,并将所有权重放到向量w∈Rd\mathbf{w} \in \mathbb{R}^dw∈Rd中,我们可以用点积形式来简洁地表达模型 :
y^=w⊤x+b.\hat{y}=\mathbf{w}^{\top} \mathbf{x}+b . y^=w⊤x+b.
向量x\mathbf{x}x 对应于单个数据样本的特征。用符号表示的矩阵X∈Rn×d\mathbf{X} \in \mathbb{R}^{n \times d}X∈Rn×d可以很方便地引用我们整个数 据集的ddd个样本。其中, X\mathbf{X}X 的每一行是一个样本, 每一列是一种特征。
对于特征集合x\mathbf{x}x, 预测值y^∈Rn\hat{\mathbf{y}} \in \mathbb{R}^ny^∈Rn可以通过矩阵-向量乘法表示为 :
y^=Xw+b\hat{\mathbf{y}}=\mathbf{X} \mathbf{w}+b y^=Xw+b
在开始寻找最好的模型参数(model parameters)w和b之前,我们还需要两个东西:(1)⼀种模型质量的度量⽅式;(2)⼀种能够更新模型以提⾼模型预测质量的⽅法。
在我们开始考虑如何⽤模型拟合(fit)数据之前,我们需要确定⼀个拟合程度的度量。损失函数(loss function)能够量化⽬标的实际值与预测值之间的差距。
通常我们会选择⾮负数作为损失,且数值越⼩表⽰损失越⼩,完美预测时的损失为 0 。回归问题中最常用的损失函数是平方误差函数。当样本 iii 的预测值为y^(i)\hat{y}^{(i)}y^(i), 其相应的真 实标签为y(i)y^{(i)}y(i)时,平方误差可以定义为以下公式:
l(i)(w,b)=12(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.
为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和)。
L(w,b)=1n∑i=1nl(i)(w,b)=1n∑i=1n12(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∗)\left(\mathbf{w}^*, b^*\right)(w∗,b∗), 这组参数能最小化在所有训练样本上的总损失。如下式 :
w∗,b∗=argminw,bL(w,b).\mathbf{w}^*, b^*=\underset{\mathbf{w}, b}{\operatorname{argmin}} L(\mathbf{w}, b) . w∗,b∗=w,bargminL(w,b).
线性回归的解可以⽤⼀个公式简单地表达出来,这类解叫作解析解(analytical solution)。
将损失关于w的导数设为0,得到解析解:w∗=(X⊤X)−1X⊤y\mathbf{w}^*=\left(\mathbf{X}^{\top} \mathbf{X}\right)^{-1} \mathbf{X}^{\top} \mathbf{y}w∗=(X⊤X)−1X⊤y
即使在我们⽆法得到解析解的情况下,我们仍然可以有效地训练模型。我们⽤到⼀种名为**梯度下降(gradient descent)**的⽅法,这种⽅法⼏乎可以优化所有深度学习模型。它通过不断地在损失函数递减的⽅向上更新参数来降低误差。
梯度下降最简单的⽤法是计算损失函数(数据集中所有样本的损失均值)关于模型参数的导数(在这⾥也可以称为梯度)。但实际中的执⾏可能会⾮常慢:因为在每⼀次更新参数之前,我们必须遍历整个数据集。因此,我们通常会在每次需要计算更新的时候随机抽取⼀⼩批样本,这种变体叫做⼩批量随机梯度下降(minibatch stochastic gradient descent)。
在每次迭代中,我们⾸先随机抽样⼀个⼩批量B,它是由固定数量的训练样本组成的。然后,我们计算⼩批量的平均损失关于模型参数的导数(也可以称为梯度)。最后,我们将梯度乘以⼀个预先确定的正数η,并从当前参数的值中减掉。
我们⽤下⾯的数学公式来表⽰这⼀更新过程(∂表⽰偏导数):
(w,b)←(w,b)−η∣B∣∑i∈B∂(w,b)l(i)(w,b)(\mathbf{w}, b) \leftarrow(\mathbf{w}, b)-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{(\mathbf{w}, b)} l^{(i)}(\mathbf{w}, b) (w,b)←(w,b)−∣B∣ηi∈B∑∂(w,b)l(i)(w,b)
总结⼀下,算法的步骤如下:
(1)初始化模型参数的值,如随机初始化;
(2)从数据集中随机抽取⼩批量样本且在负梯度的⽅向上更新参数,并不断迭代这⼀步骤。对于平⽅损失和仿射变换,我们可以明确地写成如下形式:
w←w−η∣B∣∑i∈B∂wl(i)(w,b)=w−η∣B∣∑i∈Bx(i)(w⊤x(i)+b−y(i)),b←b−η∣B∣∑i∈B∂bl(i)(w,b)=b−η∣B∣∑i∈B(w⊤x(i)+b−y(i)).\begin{aligned}& \mathbf{w} \leftarrow \mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b)=\mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right), \\& b \leftarrow b-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b)=b-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right) . \\&\end{aligned}w←w−∣B∣ηi∈B∑∂wl(i)(w,b)=w−∣B∣ηi∈B∑x(i)(w⊤x(i)+b−y(i)),b←b−∣B∣ηi∈B∑∂bl(i)(w,b)=b−∣B∣ηi∈B∑(w⊤x(i)+b−y(i)).
B|表⽰每个⼩批量中的样本数,这也称为批量⼤⼩(batch size)。η表⽰学习率(learning rate)。批量⼤⼩和学习率的值通常是⼿动预先指定,⽽不是通过模型训练得到的。这些可以调整但不在训练过程中更新的参数称为超参数(hyperparameter)。调参(hyperparameter tuning)是选择超参数的过程。超参数通常是我们根据训练迭代结果来调整的,⽽训练迭代结果是在独⽴的验证数据集(validation dataset)上评估得到的。
事实上,更难做到的是找到⼀组参数,这组参数能够在我们从未⻅过的数据上实现较低的损失,这⼀挑战被称为泛化(generalization)。
%matplotlib inline
import random
import torch
from d2l import torch as d2l
我们使用线性模型参数w=[2,−3.4]⊤、b=4.2\mathbf{w}=[2,-3.4]^{\top} 、 b=4.2w=[2,−3.4]⊤、b=4.2和噪声项ϵ\epsilonϵ生成数据集及其标签:
y=Xw+b+ϵ.\mathbf{y}=\mathbf{X} \mathbf{w}+b+\epsilon . y=Xw+b+ϵ.
你可以将ϵ\epsilonϵ 视为模型预测和标签时的潜在观测误差。在这里我们认为标准假设成立, 即 ϵ\epsilonϵ 服从均值为 0 的正态分布。为了简化问题,我们将标准差设为0.010.010.01。下面的代码生成合成数据集。
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) + by += torch.normal(0, 0.01, y.shape)return X, y.reshape((-1, 1))true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
注意,features
中的每一行都包含一个二维数据样本, labels
中的每一行都包含一维标签值(一个标量)。
print('features:', features[0],'\nlabel:', labels[0])
features: tensor([-0.1413, 0.9253])
label: tensor([0.7524])
训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。
在下面的代码中,我们定义一个data_iter
函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size
的小批量。 每个小批量包含一组特征和标签。
def data_iter(batch_size, features, labels):num_examples = len(features) # python 矩阵中的len() 方法用于获取矩阵的行indices = list(range(num_examples)) # range函数返回一个range对象实例。实例包含了计数的起始位置、终点位置和步长等信息。# list()函数是Python的内置函数。它可以将任何可迭代数据转换为列表类型,并返回转换后的列表。当参数为空时,list函数可以创建一个空列表。# 这些样本是随机读取的,没有特定的顺序random.shuffle(indices)# shuffle()方法将序列的所有元素随机排列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]
通常,我们利用GPU并行运算的优势,处理合理大小的“小批量”。 每个样本都可以并行地进行模型计算,且每个样本损失函数的梯度也可以被并行计算。
我们直观感受一下小批量运算:读取第一个小批量数据样本并打印。 每个批量的特征维度显示批量大小和输入特征数。 同样的,批量的标签形状与batch_size
相等。
batch_size = 10for X, y in data_iter(batch_size, features, labels):print(X, '\n', y)break
tensor([[-0.0929, 0.3136],[-0.4081, 0.5990],[ 1.2006, -0.8625],[ 2.8351, 1.2113],[ 0.4811, 1.6206],[-1.5946, 0.7590],[-0.7296, 2.0734],[ 1.4357, -0.4068],[-1.1405, -0.0359],[ 0.6749, 0.9677]])tensor([[ 2.9562],[ 1.3347],[ 9.5308],[ 5.7467],[-0.3549],[-1.5650],[-4.3218],[ 8.4510],[ 2.0353],[ 2.2612]])
在我们开始用小批量随机梯度下降优化我们的模型参数之前, 我们需要先有一些参数。 在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损失的方向更新每个参数。 因为手动计算梯度很枯燥而且容易出错,所以没有人会手动计算梯度。 我们使用自动微分来计算梯度。
接下来,我们必须定义模型,将模型的输入和参数同模型的输出关联起来。 回想一下,要计算线性模型的输出, 我们只需计算输入特征X和模型权重w的矩阵-向量乘法后加上偏置b。
def linreg(X, w, b): #@save"""线性回归模型"""return torch.matmul(X, w) + b
因为需要计算损失函数的梯度,所以我们应该先定义损失函数。 这里我们使用平方损失函数。 在实现中,我们需要将真实值y
的形状转换为和预测值y_hat
的形状相同。
def squared_loss(y_hat, y): #@save"""均方损失"""return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
这里我们介绍小批量随机梯度下降。
在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr
决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size
) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
def sgd(params, lr, batch_size): #@save"""小批量随机梯度下降"""with torch.no_grad(): # 屏蔽梯度计算for param in params:param -= lr * param.grad / batch_sizeparam.grad.zero_()
with torch.no_grad()和backward()
在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。 最后,我们调用优化算法sgd
来更新模型参数。
概括一下,我们将执行以下循环:
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_lossfor 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.026352
epoch 2, loss 0.000093
epoch 3, loss 0.000054
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2ltrue_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
我们可以调用框架中现有的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)
pytorch中Dataset,TensorDataset和DataLoader用法_鬼道2022的博客-CSDN博客_data.tensordataset
我们使用iter
构造Python迭代器,并使用next
从迭代器中获取第一项。
next(iter(data_iter))
[tensor([[ 0.7882, -0.7068],[ 0.5081, 0.2577],[-0.5769, 0.1545],[-0.3271, -0.6080],[-0.2716, -1.4628],[-1.1530, -1.4643],[ 0.1635, -0.2018],[-0.0753, -1.1161],[ 3.4251, 0.1953],[ 0.3589, -0.9478]]),tensor([[ 8.1742],[ 4.3357],[ 2.5157],[ 5.6106],[ 8.6395],[ 6.8726],[ 5.2155],[ 7.8377],[10.3918],[ 8.1590]])]
对于标准深度学习模型,我们可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。 我们首先定义一个模型变量net
,它是一个Sequential
类的实例。 Sequential
类将多个层串联在一起。 当给定输入数据时,Sequential
实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。 在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential
。 但是由于以后几乎所有的模型都是多层的,在这里使用Sequential
会让你熟悉“标准的流水线”。
在PyTorch中,全连接层在Linear
类中定义。 值得注意的是,我们将两个参数传递到nn.Linear
中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。
# nn是神经网络的缩写
from torch import nnnet = nn.Sequential(nn.Linear(2, 1))
在使用net
之前,我们需要初始化模型参数。 如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样, 偏置参数将初始化为零。
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
tensor([0.])
PyTorch权重初始化的几种方法_鹊踏枝-码农的博客-CSDN博客_pytorch weight.data.normal_
深度学习基础知识(一)— 权重初始化_Teeyohuang的博客-CSDN博客_weight.data.normal_
计算均方误差使用的是MSELoss
类,也称为平方L2L_2L2范数。 默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()
小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim
模块中实现了该算法的许多变种。 当我们实例化一个SGD
实例时,我们要指定优化的参数 (可通过net.parameters()
从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr
值,这里设置为0.03。
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(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.000157
epoch 2, loss 0.000094
epoch 3, loss 0.000094
统计学家很早以前就发明了一种表示分类数据的简单方法:独热编码(one-hot encoding)。 独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。
与线性回归一样,softmax回归也是一个单层神经网络。 由于计算每个输出o1,o2,o3,o4o_1,o_2,o_3,o_4o1,o2,o3,o4取决于 所有输入x1,x2,x3,x4x_1,x_2,x_3,x_4x1,x2,x3,x4, 所以softmax回归的输出层也是全连接层。
softmax函数能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持 可导的性质。 为了完成这一目标,我们首先对每个未规范化的预测求幂,这样可以确保输出非负。 为了确保最终输出的概率值总和为1,我们再让每个求幂后的结果除以它们的总和。如下式:
y^=softmax(o)其中 y^j=exp(oj)∑kexp(ok)\hat{\mathbf{y}}=\operatorname{softmax}(\mathbf{o}) \quad \text { 其中 } \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)
这里, 对于所有的jjj总有0≤y^j≤10 \leq \hat{y}_j \leq 10≤y^j≤1 。 因此, y^\hat{\mathbf{y}}y^可以视为一个正确的概率分布。 softmax运算不会改变末规范化的预测o\mathbf{o}o之间的大小次序, 只会确定分配给每个类别的概 率。因此, 在预测过程中, 我们仍然可以用下式来选择最有可能的类别。
argmaxjy^j=argmaxjoj.\underset{j}{\operatorname{argmax}} \hat{y}_j=\underset{j}{\operatorname{argmax}} o_j . jargmaxy^j=jargmaxoj.
softmax函数给出了一个向量y^\hat{\mathbf{y}}y^, 我们可以将其视为 “对给定任意输入x\mathbf{x}x 的每个类的条件概率”。可以将估计值与实际值进行比较:
假设整个数据集X,Y{X,Y}X,Y具有nnn个样本,其中索引i的样本由特征向量x(i)x^{(i)}x(i)和独热标签向量y(i)y^{(i)}y(i)组成
P(Y∣X)=∏i=1nP(y(i)∣x(i)).P(\mathbf{Y} \mid \mathbf{X})=\prod{i=1}^n P\left(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}\right) . P(Y∣X)=∏i=1nP(y(i)∣x(i)).
根据最大似然估计, 我们最大化P(Y∣X)P(\mathbf{Y} \mid \mathbf{X})P(Y∣X), 相当于最小化负对数似然:
−logP(Y∣X)=∑i=1n−logP(y(i)∣x(i))=∑i=1nl(y(i),y^(i)),-\log P(\mathbf{Y} \mid \mathbf{X})=\sum_{i=1}^n-\log P\left(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}\right)=\sum_{i=1}^n l\left(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)}\right), −logP(Y∣X)=i=1∑n−logP(y(i)∣x(i))=i=1∑nl(y(i),y^(i)),
其中,对于任何标签 y\mathbf{y}y 和模型预测 y^\hat{\mathbf{y}}y^ ,损失函数为:
l(y,y^)=−∑j=1qyjlogy^j.l(\mathbf{y}, \hat{\mathbf{y}})=-\sum_{j=1}^q y_j \log \hat{y}_j . l(y,y^)=−j=1∑qyjlogy^j.
上式中的损失函数通常被称为交叉嫡损失 (cross-entropy loss)。
我们可以通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。
# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
# 并除以255使得所有像素的数值均在0到1之间
trans = transforms.ToTensor()
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)
Fashion-MNIST由10个类别的图像组成, 每个类别由训练数据集(train dataset)中的6000张图像 和测试数据集(test dataset)中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。 测试数据集不会用于训练,只用于评估模型性能。
len(mnist_train), len(mnist_test)
# (60000, 10000)
每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 为了简洁起见,本书将高度h像素、宽度w像素图像的形状记为h×wh\times wh×w或者(h,w)(h,w)(h,w)。
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]
我们现在可以创建一个函数来可视化这些样本。
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
以下是训练数据集中前几个样本的图像及其相应的标签。
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));
在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size
。 通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量。
batch_size = 256def get_dataloader_workers(): #@save"""使用4个进程来读取数据"""return 4train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_workers())
现在我们定义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()))
下面,我们通过指定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
本节我们将使用Fashion-MNIST数据集, 并设置数据迭代器的批量大小为256。
import torch
from IPython import display
from d2l import torch as d2lbatch_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 = 10W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
在实现softmax回归模型之前,我们简要回顾一下sum
运算符如何沿着张量中的特定维度工作。
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
);def softmax(X):X_exp = torch.exp(X)partition = X_exp.sum(1, keepdim=True)return X_exp / partition # 这里应用了广播机制
正如你所看到的,对于任何随机输入,我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1。
X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
(tensor([[0.0456, 0.1734, 0.0443, 0.2028, 0.5339],[0.0648, 0.0213, 0.0681, 0.6248, 0.2211]]),tensor([1., 1.]))
定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape
函数将每张原始图像展平为向量。
def net(X):return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
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]
我们只需一行代码就可以实现交叉熵损失函数。
def cross_entropy(y_hat, y):return - torch.log(y_hat[range(len(y_hat)), y])cross_entropy(y_hat, y)
tensor([2.3026, 0.6931])
为了计算精度,我们执行以下操作。 首先,如果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) == yreturn float(cmp.type(y.dtype).sum())
我们将继续使用之前定义的变量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]
这里定义一个实用程序类Accumulator
,用于对多个变量进行累加。 在上面的evaluate_accuracy
函数中, 我们在Accumulator
实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量。 当我们遍历数据集时,两者都将随着时间的推移而累加。
class Accumulator: #@save"""在n个变量上累加"""def __init__(self, n):self.data = [0.0] * ndef 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]
由于我们使用随机权重初始化net
模型, 因此该模型的精度应接近于随机猜测。 例如在有10个类别情况下的精度为0.1。
evaluate_accuracy(net, test_iter)
# 0.1012
首先,我们定义一个函数来训练一个迭代周期。 请注意,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])metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())# 返回训练损失和训练精度return metric[0] / metric[2], metric[1] / metric[2]
在展示训练函数的实现之前,我们定义一个在动画中绘制数据的实用程序类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, fmtsdef add(self, x, y):# 向图表中添加多个数据点if not hasattr(y, "__len__"):y = [y]n = len(y)if not hasattr(x, "__len__"):x = [x] * nif 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
类来可视化训练进度。
小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。
lr = 0.1def 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)
现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。
def predict_ch3(net, test_iter, n=6): #@save"""预测标签(定义见第3章)"""for X, y in test_iter:breaktrues = 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)
继续使用Fashion-MNIST数据集,并保持批量大小为256。
import torch
from torch import nn
from d2l import torch as d2lbatch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# 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);
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)