本文为学习《动手学习深度学习》的一些学习,内容来源于网址:
https://d2l.ai/ Dive into Deep Learning — Dive into Deep Learning
用于学习记录
3.3线性回归的简洁实现
在本节中,我们将介绍如何通过使用深度学习框架来简洁地实现 3.2节中的线性回归模型。
生成数据集
同上一节一样,首先我们生成数据集
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
读取数据集
我们可以[调用框架中现有的API来读取数据]。 我们将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)
使用data_iter
的方式与我们在 :numref:sec_linear_scratch
中使用data_iter
函数的方式相同。为了验证是否正常工作,让我们读取并打印第一个小批量样本。 与:numref:sec_linear_scratch
不同,这里我们使用iter
构造Python迭代器,并使用next
从迭代器中获取第一项。
next(iter(data_iter))
[tensor([[ 6.5540e-01, 4.8845e-01],
[ 2.9239e+00, -1.0434e+00],
[-1.1138e+00, -2.8268e-01],
[-6.1571e-01, -1.8541e+00],
[ 4.1536e-01, -1.1004e-01],
[-3.0715e-01, -5.9745e-01],
[-3.0655e-01, 1.5793e-03],
[-1.3754e+00, -1.3812e+00],
[ 1.4831e+00, -1.3262e+00],
[ 4.0001e-01, -9.0868e-01]]),
tensor([[ 3.8507],
[13.6003],
[ 2.9386],
[ 9.2701],
[ 5.4006],
[ 5.6166],
[ 3.5723],
[ 6.1388],
[11.6530],
[ 8.0939]])]
定义模型
对于标准深度学习模型,我们可以[使用框架的预定义好的层]。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。 我们首先定义一个模型变量net
,它是一个Sequential
类的实例。 Sequential
类将多个层串联在一起。 当给定输入数据时,Sequential
实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。 在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential
。 但是由于以后几乎所有的模型都是多层的,在这里使用Sequential
会让你熟悉“标准的流水线”。
回顾 :numref:fig_single_neuron
中的单层网络架构, 这一单层被称为全连接层(fully-connected layer), 因为它的每一个输入都通过矩阵-向量乘法得到它的每个输出。
在PyTorch中,全连接层在Linear
类中定义。 值得注意的是,我们将两个参数传递到nn.Linear
中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。
nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
(初始化模型参数)
在使用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)
定义损失函数
[计算均方误差使用的是MSELoss
类,也称为平方𝐿2L2范数]。 默认情况下,它返回所有样本损失的平均值。
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}')
注:补充 pytorch的SGD函数理解及原函数
https://blog.youkuaiyun.com/xiaoxifei/article/details/87797935
from .optimizer import Optimizer, required
class SGD(Optimizer):
r"""Implements stochastic gradient descent (optionally with momentum).
Nesterov momentum is based on the formula from
`On the importance of initialization and momentum in deep learning`__.
Args:
params (iterable): iterable of parameters to optimize or dicts defining
parameter groups
lr (float): learning rate
momentum (float, optional): momentum factor (default: 0)
weight_decay (float, optional): weight decay (L2 penalty) (default: 0)
dampening (float, optional): dampening for momentum (default: 0)
nesterov (bool, optional): enables Nesterov momentum (default: False)
Example:
>>> optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
>>> optimizer.zero_grad()
>>> loss_fn(model(input), target).backward()
>>> optimizer.step()
__ http://www.cs.toronto.edu/%7Ehinton/absps/momentum.pdf
.. note::
The implementation of SGD with Momentum/Nesterov subtly differs from
Sutskever et. al. and implementations in some other frameworks.
Considering the specific case of Momentum, the update can be written as
.. math::
v = \rho * v + g \\
p = p - lr * v
where p, g, v and :math:`\rho` denote the parameters, gradient,
velocity, and momentum respectively.
This is in contrast to Sutskever et. al. and
other frameworks which employ an update of the form
.. math::
v = \rho * v + lr * g \\
p = p - v
The Nesterov version is analogously modified.
"""
def __init__(self, params, lr=required, momentum=0, dampening=0,
weight_decay=0, nesterov=False):
defaults = dict(lr=lr, momentum=momentum, dampening=dampening,
weight_decay=weight_decay, nesterov=nesterov)
if nesterov and (momentum <= 0 or dampening != 0):
raise ValueError("Nesterov momentum requires a momentum and zero dampening")
super(SGD, self).__init__(params, defaults)
def __setstate__(self, state):
super(SGD, self).__setstate__(state)
for group in self.param_groups:
group.setdefault('nesterov', False)
def step(self, closure=None):
"""Performs a single optimization step.
Arguments:
closure (callable, optional): A closure that reevaluates the model
and returns the loss.
"""
loss = None
if closure is not None:
loss = closure()
for group in self.param_groups:
weight_decay = group['weight_decay']
momentum = group['momentum']
dampening = group['dampening']
nesterov = group['nesterov']
for p in group['params']:
if p.grad is None:
continue
d_p = p.grad.data
if weight_decay != 0:
d_p.add_(weight_decay, p.data)
if momentum != 0:
param_state = self.state[p]
if 'momentum_buffer' not in param_state:
buf = param_state['momentum_buffer'] = d_p.clone()
else:
buf = param_state['momentum_buffer']
buf.mul_(momentum).add_(1 - dampening, d_p)
if nesterov:
d_p = d_p.add(momentum, buf)
else:
d_p = buf
p.data.add_(-group['lr'], d_p)
return loss
下面我们[比较生成数据集的真实参数和通过有限数据训练获得的模型参数]。 要访问参数,我们首先从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)
- 我们可以使用PyTorch的高级API更简洁地实现模型。
- 在PyTorch中,
data
模块提供了数据处理工具,nn
模块定义了大量的神经网络层和常见损失函数。 - 我们可以通过
_
结尾的方法将参数替换,从而初始化参数。
softmax回归
在 前面的学习中我们从头实现线性回归。 随后我们使用深度学习框架的高级API简洁实现线性回归。
事实上,我们也对分类问题感兴趣:不是问“多少”,而是问“哪一个”:
- 某个电子邮件是否属于垃圾邮件文件夹?
- 某个用户可能注册或不注册订阅服务?
- 某个图像描绘的是驴、狗、猫、还是鸡?
- 某人接下来最有可能看哪部电影?
通常,机器学习实践者用分类这个词来描述两个有微妙差别的问题:
- 我们只对样本的“硬性”类别感兴趣,即属于哪个类别;
- 我们希望得到“软性”类别,即得到属于每个类别的概率。 这两者的界限往往很模糊。其中的一个原因是:即使我们只关心硬类别,我们仍然使用软类别的模型。
分类问题
我们从一个图像分类问题开始。 假设每次输入是一个2×2的灰度图像。 我们可以用一个标量表示每个像素值,每个图像对应四个特征𝑥1,𝑥2,𝑥3,𝑥4。 此外,假设每个图像属于类别“猫”,“鸡”和“狗”中的一个。
接下来,我们要选择如何表示标签。 我们有两个明显的选择:最直接的想法是选择𝑦∈{1,2,3}, 其中整数分别代表{狗,猫,鸡}。 这是在计算机上存储此类信息的有效方法。 如果类别间有一些自然顺序, 比如说我们试图预测{婴儿,儿童,青少年,青年人,中年人,老年人}{婴儿,儿童,青少年,青年人,中年人,老年人}, 那么将这个问题转变为回归问题,并且保留这种格式是有意义的。
幸运的是,一般的分类问题并不与类别之间的自然顺序有关。 统计学家很早以前就发明了一种表示分类数据的简单方法:独热编码(one-hot encoding)。 独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。 在我们的例子中,标签𝑦y将是一个三维向量, 其中(1,0,0)(1,0,0)对应于“猫”、(0,1,0)(0,1,0)对应于“鸡”、(0,0,1)(0,0,1)对应于“狗”:
𝑦
∈
(
1
,
0
,
0
)
,
(
0
,
1
,
0
)
,
(
0
,
0
,
1
)
.
𝑦∈{(1,0,0),(0,1,0),(0,0,1)}.
y∈(1,0,0),(0,1,0),(0,0,1).
网络架构
为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。 为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。 在我们的例子中,由于我们有4个特征和3个可能的输出类别, 我们将需要12个标量来表示权重(带下标的𝑤w), 3个标量来表示偏置(带下标的𝑏b)。 下面我们为每个输入计算三个未规范化的预测(logit):𝑜1o1、𝑜2o2和𝑜3o3。
𝑜 1 = 𝑥 1 𝑤 11 + 𝑥 2 𝑤 12 + 𝑥 3 𝑤 13 + 𝑥 4 𝑤 14 + 𝑏 1 𝑜1=𝑥1𝑤11+𝑥2𝑤12+𝑥3𝑤13+𝑥4𝑤14+𝑏1 o1=x1w11+x2w12+x3w13+x4w14+b1
𝑜 2 = 𝑥 1 𝑤 21 + 𝑥 2 𝑤 22 + 𝑥 3 𝑤 23 + 𝑥 4 𝑤 24 + 𝑏 2 𝑜2=𝑥1𝑤21+𝑥2𝑤22+𝑥3𝑤23+𝑥4𝑤24+𝑏2 o2=x1w21+x2w22+x3w23+x4w24+b2
𝑜 3 = 𝑥 1 𝑤 31 + 𝑥 2 𝑤 32 + 𝑥 3 𝑤 33 + 𝑥 4 𝑤 34 + 𝑏 3. 𝑜3=𝑥1𝑤31+𝑥2𝑤32+𝑥3𝑤33+𝑥4𝑤34+𝑏3. o3=x1w31+x2w32+x3w33+x4w34+b3.
我们可以用神经网络图来描述这个计算过程。 与线性回归一样,softmax回归也是一个单层神经网络。 由于计算每个输出𝑜1、𝑜2和𝑜3取决于 所有输入𝑥1、𝑥2、𝑥3和𝑥4, 所以softmax回归的输出层也是全连接层。
为了更简洁地表达模型,我们仍然使用线性代数符号。 通过向量形式表达为𝐨=𝐖𝐱+𝐛, 这是一种更适合数学和编写代码的形式。 由此,我们已经将所有权重放到一个3×4矩阵中。 对于给定数据样本的特征𝐱, 我们的输出是由权重与输入特征进行矩阵-向量乘法再加上偏置𝐛得到的。
全连接层的参数开销
在深度学习中,全连接层无处不在。
顾名思义,全连接层是“完全”连接的,可能有很多可学习的参数。 具体来说,对于任何具有𝑑个输入和𝑞个输出的全连接层, 参数开销为O(dq),这个数字在实践中可能高得令人望而却步。 幸运的是,将𝑑个输入转换为𝑞个输出的成本可以减少到O(dq/n), 其中超参数𝑛可以由我们灵活指定,以在实际应用中平衡参数节约和模型有效性
softmax运算
现在我们将优化参数以最大化观测数据的概率。 为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。
我们希望模型的输出𝑦̂ 𝑗可以视为属于类𝑗的概率, 然后选择具有最大输出值的类别argmax𝑗𝑦𝑗作为我们的预测。 例如,如果𝑦̂ 1、𝑦̂ 2和𝑦̂ 3分别为0.1、0.8和0.1, 那么我们预测的类别是2,在我们的例子中代表“鸡”。
然而我们能否将未规范化的预测𝑜直接视作我们感兴趣的输出呢? 答案是否定的。 因为将线性层的输出直接视为概率时存在一些问题: 一方面,我们没有限制这些输出数字的总和为1。 另一方面,根据输入的不同,它们可以为负值。 这些违反了概率基本公理。
要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。 此外,我们需要一个训练目标,来鼓励模型精准地估计概率。 在分类器输出0.5的所有样本中,我们希望这些样本有一半实际上属于预测的类。 这个属性叫做校准(calibration)。
社会科学家邓肯·卢斯于1959年在选择模型(choice model)的理论基础上 发明的softmax函数正是这样做的:
softmax函数
将未规范化的预测变换为非负并且总和为1,
同时要求模型保持可导。
我们首先对每个未规范化的预测求幂,这样可以确保输出非负。 为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。如下式:
这里,对于所有的𝑗j总有0≤𝑦̂ 𝑗≤1。 因此,𝐲̂ 可以视为一个正确的概率分布。 softmax运算不会改变未规范化的预测𝐨之间的顺序,只会确定分配给每个类别的概率。 因此,在预测过程中,我们仍然可以用下式来选择最有可能的类别。
尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型(linear model)。
尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型(linear model)。
小批量样本的矢量化
🏷subsec_softmax_vectorization
为了提高计算效率并且充分利用GPU,我们通常会针对小批量数据执行矢量计算。 假设我们读取了一个批量的样本𝐗, 其中特征维度(输入数量)为𝑑d,批量大小为𝑛。 此外,假设我们在输出中有𝑞q个类别。 那么小批量特征为𝐗∈ℝ𝑛×𝑑, 权重为𝐖∈ℝ𝑑×𝑞, 偏置为𝐛∈ℝ1×𝑞。 softmax回归的矢量计算表达式为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ngi4Rn3B-1641561965805)(C:\毕业设计\图片.PNG)]
相对于一次处理一个样本, 小批量样本的矢量化加快了𝐗和𝐖的矩阵-向量乘法。 由于𝐗中的每一行代表一个数据样本, 那么softmax运算可以按行(rowwise)执行: 对于𝐎的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。 𝐗𝐖+𝐛的求和会使用广播, 小批量的未规范化预测𝐎和输出概率𝐘̂ 都是形状为𝑛×𝑞的矩阵。
损失函数
接下来,我们需要一个损失函数来度量预测的效果。 我们将使用最大似然估计
对数似然
softmax函数给出了一个向量𝐲̂ , 我们可以将其视为“对给定任意输入𝐱的每个类的条件概率”。 例如,𝑦̂ 1=𝑃(𝑦=猫∣𝐱)。 假设整个数据集{𝐗,𝐘}具有𝑛个样本, 其中索引𝑖的样本由特征向量𝐱(𝑖)和独热标签向量𝐲(𝑖)组成。 我们可以将估计值与实际值进行比较:
这里的损失函数 通常被称为交叉熵损失(cross-entropy loss)。 由于𝐲是一个长度为𝑞的独热编码向量, 所以除了一个项以外的所有项𝑗都消失了。 由于所有𝑦̂ 𝑗都是预测的概率,所以它们的对数永远不会大于0。 因此,如果正确地预测实际标签,即如果实际标签𝑃(𝐲∣𝐱)=1, 则损失函数不能进一步最小化。 注意,这往往是不可能的。 例如,数据集中可能存在标签噪声(比如某些样本可能被误标), 或输入特征没有足够的信息来完美地对每一个样本分类。
softmax及其导数
由于softmax和相关的损失函数很常见, 因此我们需要更好地理解它的计算方式。 将 softmax代入损失 cross_entropy`中。 利用softmax的定义,我们得到:
注:这里的推导要注意一下!!!
换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异。 从这个意义上讲,这与我们在回归中看到的非常相似, 其中梯度是观测值𝑦y和估计值𝑦̂ y^之间的差异。 这不是巧合,在任何指数族分布模型中 , 对数似然的梯度正是由此得出的。 这使梯度计算在实践中变得容易很多
交叉熵损失
我们观察到的不仅仅是一个结果,而是整个结果分布。 对于标签𝐲y,我们可以使用与以前相同的表示形式。 唯一的区别是,我们现在用一个概率向量表示,如(0.1,0.2,0.7)(0.1,0.2,0.7), 而不是仅包含二元项的向量(0,0,1)(0,0,1)。 我们使用 :eqref:eq_l_cross_entropy
来定义损失𝑙l, 它是所有标签分布的预期损失值。 此损失称为交叉熵损失(cross-entropy loss),它是分类问题最常用的损失之一。 本节我们将通过介绍信息论基础来理解交叉熵损失。
注:
cross entropy一般是用来量化两个机率分布之间的差距的
举个例子,你现在要预测一张图片是狗或猫
你的模型得到的概率是
狗 = 0.4, 猫 = 0.6
而真实的概率则是
狗 = 0.0, 猫 = 1.0
那么预测出来的概率和真实的概率,两者之间的差距有多大呢?这就是cross entropy要量化的事情了
根据上述的例子,我们可知道cross entropy为
-( 0.0 * log(0.4) + 1.0*log(0.6) ) = 0.22
0.22代表的是你的model预测出来的概率和真实的概率之间,差距有多大
信息论基础
信息论(information theory)涉及编码、解码、发送以及尽可能简洁地处理信息或数据。
熵
信息论的核心思想是量化数据中的信息内容。 在信息论中,该数值被称为分布𝑃P的熵(entropy)。可以通过以下方程得到:
信息论的基本定理之一指出,为了对从分布𝑝p中随机抽取的数据进行编码, 我们至少需要𝐻[𝑃]H[P]“纳特(nat)”对其进行编码。 “纳特”相当于比特(bit),但是对数底为𝑒e而不是2。因此,一个纳特是1log(2)≈1.441log(2)≈1.44比特。
惊异
压缩与预测有什么关系呢? 想象一下,我们有一个要压缩的数据流。 如果我们很容易预测下一个数据,那么这个数据很容易压缩。 为什么呢? 举一个极端的例子,假如数据流中的每个数据完全相同,这会是一个非常无聊的数据流。 由于它们总是相同的,所以很容易被预测。 所以,为了传递数据流的内容,我们不必传输任何信息。 因此,当数据易于预测,也就易于压缩。
但是,如果我们不能完全预测每一个事件,那么我们有时可能会感到"惊异"。 克劳德·香农决定用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-my9ENZrA-1641561965808)(C:\毕业设计\图片.PNG)]
来量化惊异(surprisal)。 在观察一个事件𝑗j,并赋予它(主观)概率𝑃(𝑗)P(j)。 当我们赋予一个事件较低的概率时,我们的惊异会更大。 在 :eqref:eq_softmax_reg_entropy
中定义的熵, 是当分配的概率真正匹配数据生成过程时的预期惊异(expected surprisal)。
重新审视交叉熵
如果把熵𝐻(𝑃)H§想象为“知道真实概率的人所经历的惊异程度”,那么什么是交叉熵? 交叉熵从𝑃到𝑄,记为𝐻(𝑃,𝑄)H(P,Q)。 你可以把交叉熵想象为“主观概率为𝑄Q的观察者在看到根据概率𝑃P生成的数据时的预期惊异”。 当𝑃=𝑄P=Q时,交叉熵达到最低。 在这种情况下,从𝑃P到𝑄Q的交叉熵是𝐻(𝑃,𝑃)=𝐻(𝑃)H(P,P)=H§。
简而言之,我们可以从两方面来考虑交叉熵分类目标: (i)最大化观测数据的似然;(ii)最小化传达标签所需的惊异。
模型预测和评估
在训练softmax回归模型后,给出任何样本特征,我们可以预测每个输出类别的概率。 通常我们使用预测概率最高的类别作为输出类别。 如果预测与实际类别(标签)一致,则预测是正确的。 在接下来的实验中,我们将使用精度(accuracy)来评估模型的性能。 精度等于正确预测数与预测总数之间的比率。
小结
- softmax运算获取一个向量并将其映射为概率。
- softmax回归适用于分类问题,它使用了softmax运算中输出类别的概率分布。
- 交叉熵是一个衡量两个概率分布之间差异的很好的度量,它测量给定模型编码数据所需的比特数。
图像分类数据集
(MNIST数据集) :cite:LeCun.Bottou.Bengio.ea.1998
就是当年玩的那个监督数据集
(是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。
我们将使用类似但更复杂的Fashion-MNIST数据集) :cite:Xiao.Rasul.Vollgraf.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()
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张图像。 测试数据集不会用于训练,只用于评估模型性能。
每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 为了简洁起见,本书将高度ℎ像素、宽度𝑤像素图像的形状记为ℎ×𝑤或(ℎ,𝑤)。
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 = 256
def get_dataloader_workers(): #@save
"""使用4个进程来读取数据"""
return 4
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers())
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包含着图像预处理的的一系列操作,可以自定义数据的预处理
softmax回归的从零开始实现
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)
读取数据集
初始化模型参数
首先 我们如何理解图像 参见前面的MLP讲解,我们把28*28的图像展平为784长度的向量,及我们的输入为784维向量,输出为10个类别 使用最简单的输入直接到映射 则需要784x10个权重 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操作
在实现softmax回归模型之前,我们简要回顾一下sum
运算符如何沿着张量中的特定维度工作。 如 :numref:subseq_lin-alg-reduction
和 :numref:subseq_lin-alg-non-reduction
所述, [给定一个矩阵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)</div>
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实现
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.2203, 0.0258, 0.2001, 0.1841, 0.3698],
[0.1713, 0.0874, 0.4748, 0.1416, 0.1249]]),
tensor([1.0000, 1.0000]))
注意,虽然这在数学上看起来是正确的,但我们在代码实现中有点草率。 矩阵中的非常大或非常小的元素可能造成数值上溢或下溢,但我们没有采取措施来防止这点。
定义模型
定义softmax操作后,我们可以[实现softmax回归模型]。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape
函数将每张原始图像展平为向量。
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
定义损失函数
接下来,我们实现 :numref:sec_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,y_hat[[0, 1], y]
(tensor([0, 2]), tensor([0.1000, 0.5000]))
注:要看懂这行代码,注意y_hat[[0, 1], [0,2]]的意思 是取出(0,0) (1,2)
交叉熵损失
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
cross_entropy(y_hat, y)
:注: 这里需要注意 交叉熵中计算y x log yhat 中 y为0,0,0,0,1,0…0类型 乘以1不变
分类精度
给定预测概率分布y_hat
,当我们必须输出硬预测(hard prediction)时, 我们通常选择预测概率最高的类。 许多应用都要求我们做出选择。如Gmail必须将电子邮件分类为“Primary(主要邮件)”、 “Social(社交邮件)”、“Updates(更新邮件)”或“Forums(论坛邮件)”。 Gmail做分类时可能在内部估计概率,但最终它必须在类中选择一个。
当预测与标签分类y
一致时,即是正确的。 分类精度即正确预测数量与总预测数量之比。 虽然直接优化精度可能很困难(因为精度的计算不可导), 但精度通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总会关注它。
为了计算精度,我们执行以下操作。 首先,如果y_hat
是矩阵,那么假定第二个维度存储每个类的预测分数。 我们使用argmax
获得每行中最大元素的索引来获得预测类别。 然后我们[将预测类别与真实y
元素进行比较]。 由于等式运算符“==
”对数据类型很敏感, 因此我们将y_hat
的数据类型转换为与y
的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。
分类精度
给定预测概率分布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())
同样,对于任意数据迭代器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]
评估
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] * 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]
训练
在这里,我们重构训练过程的实现以使其可重复使用。 首先,我们定义一个函数来训练一个迭代周期。 请注意,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.sum().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, 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
softmax回归的简洁实现
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化模型参数
如我们在 :numref:sec_softmax
所述, [softmax回归的输出层是一个全连接层]。 因此,为了实现我们的模型, 我们只需在Sequential
中添加一个带有10个输出的全连接层。 同样,在这里Sequential
并不是必要的, 但它是实现深度模型的基础。 我们仍然以均值0和标准差0.01随机初始化权重。
In [3]:
# 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);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ub76hVCt-1641561965810)(C:\毕业设计\pic.PNG)]
优化算法
在这里,我们(使用学习率为0.1的小批量随机梯度下降作为优化算法)。 这与我们在线性回归例子中的相同,这说明了优化器的普适性。
In [5]:
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
训练
接下来我们[调用] :numref:sec_softmax_scratch
中(之前) (定义的训练函数来训练模型)。
In [6]:
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
<Figure size 252x180 with 1 Axes>
和以前一样,这个算法使结果收敛到一个相当高的精度,而且这次的代码比之前更精简了。
小结
- 使用深度学习框架的高级API,我们可以更简洁地实现softmax回归。
- 从计算的角度来看,实现softmax回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。