1. 引入Fashion-MNIST数据集
并设置数据迭代器的批量大小为256
import torch
from IPython import display
from d2l import torch as d2l
batch_size = 256
# 每次随机读256张图片,返回训练集和测试集的迭代器
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
2. 初始化参数模型
每个样本都将用固定长度的向量表示。 原始数据集中的每个样本都是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)
3. 定义softmax操作
回想一下,实现softmax由三个步骤组成:
- 对每个项求幂(使用exp);
- 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
- 将每一行除以其规范化常数,确保结果的和为1。
# 在实现softmax时,当X是矩阵时,是对每一行做softmax
def softmax(X):
X_exp = torch.exp(X) # 对矩阵中每一个元素做指数运算
# keepdim=True 表示仍然使得X是一个矩阵,保持维度
# 按照维度为1来进行求和sum(axis=1),就是按列压缩,也就是把每一行求和
partition = X_exp.sum(1,keepdim=True)
# 这里用了广播机制,矩阵中每个元素/对应行元素之和
return X_exp / partition
用如下这个例子,可以知道softmax是怎样工作的:
可以看出,经过softmax之后,矩阵的维度不变,仍然是2 * 5,并且每一行元素相加为1.
4. 实现softmax回归模型
def net(X):
# reshape(-1,W.shape[0]):-1表示自动计算,实际上算出来就等于批量大小
# 列数=W.shape[0],也就是权重矩阵的行数784,也是784个特征
# 之前定义了batch_size=256,因此最后X会被reshape成256 * 784 的矩阵
# 再对 矩阵X和矩阵W进行乘法,最后通过广播机制加上偏移
# 最后,放入softmax中,得到所有元素值非0,且每一行的和为1的输出
return softmax(torch.matmul(X.reshape(-1,W.shape[0]),W)+b)
5. 定义交叉熵损失函数
先补充一下如何根据标号把对应的预测值拿出来。
交叉熵采用真实标签的预测概率的负对数似然。我们创建一个数据样本y_hat,其中包含2个样本在3个类别的预测概率, 以及它们对应的标签y。 有了y,我们知道在第一个样本中,第一类是正确的预测; 而在第二个样本中,第三类是正确的预测。 然后使用y作为y_hat中概率的索引, 我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。
实现交叉熵损失函数
交叉熵 = -log(预测的类别的概率)
6. 分类精度
分类精度即正确预测数量与总预测数量之比。
为了计算精度,我们执行以下操作:
- 如果y_hat是矩阵(即有多个列,也就是有多个分类类别),那么假定第二个维度存储每个类的预测分数。
- 我们使用argmax获得每行中最大元素的索引来获得预测类别。
- 然后我们将预测类别与真实y元素进行比较。
- 由于等式运算符“==”对数据类型很敏感, 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含**0(错)和1(对)**的张量。
- 最后,我们求和会得到正确预测的数量。
def accuracy(y_hat,y):
'''计算预测的正确率'''
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
# 判断张量y_hat是否大于一维,以及张量的第二个维度是否大于1
y_hat = y_hat.argmax(axis=1) # 对每一行求argmax,选出索引最大的列
# 从而得到每一个样本的最大值的下标,存到y_hat中,也就是得到预测的类别
# 例如,在上面的例子中,第一个样本中的最大概率的下标是2,也就是预测是第二类,
# 然而真实的类别是第0类,这样就表示预测错误。
# 将y_hat的数据类型转换为与y的数据类型一致
cmp = y_hat.type(y.dtype) == y # 变成一个bool的tensor
# 预测对了就等于1,预测错了就等于0
# 再把cmp转成和y一样的形状,求和,再转成一个浮点数,将其转化为标量
# 仍然看上面的例子,可以看出,第一个样本预测错了,因此是0
# 第一个预测对了,因此是1,两个相加得到结果为1
return float(cmp.type(y.dtype).sum())
#cmp.type(y.dtype)实现了bool到y的类型tensor的转化
# 也就是把True和False转化成1和0
# 得到的sum=1,除以len(y),也就是类别个数,1/2,得到0.5
# 0.5也就是预测正确的概率
accuracy(y_hat,y) / len(y) # 返回的float类型的数是一个标量,len(y)也是一个标量
自己的理解:
同样,对于任意数据迭代器data_iter可访问的数据集, 我们可以评估在任意模型net的精度.
def evaluate_accuracy(net,data_iter):
'''计算在指定数据集上模型的精度'''
# isinstance和type都是比较类型是否一致#
# 只是isinstance算继承类,type不算继承类
if isinstance(net,torch.nn.Module):
# 如果是用torch.nn 实现一个模型的话,把它转成一个评估模式,不用计算梯度
# 评估模式:指输入后得出结果用来评估模型的正确率,不做反向传播
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 实用程序类Accumulator,用于对多个变量进行累加
# Accumulator实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量
for X,y in data_iter:
# accuracy(net(X),y)预测正确的样本数,y.numel()表示样本总数
metric.add(accuracy(net(X),y),y.numel())
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]
- 关于[0.0] * n:假设n=2,则 [0.0] * n 表示[0.0 , 0.0],得到的是数组
- 关于zip(self.data, args),可以知道 第一项是【0.0 ,0.0】,第二项是(1,2)是元组,假设 obj1 = [0.0 ,0.0] ,obj2=(1,2),则 [a+float(b) for a,b in zip] 就会得到一个数组[0.0+float(1),0.0+float(2)],也就是最终得到数组[1.0,2.0] ,因此,调用add函数最终会得到一个数组。
ps:如果是 for i in zip(),得到是两个元组(0.0,1) 和 (0.0,2)
最后通过下面这个函数,看随机出来的模型和测试集的迭代器得到的精度是0.1645,因为类别数是10,所以随机的话,应该是10%的正确率,二者相差不大,基本上可以认为是随机的:
7. softmax回归的训练
首先,我们定义一个函数来训练一个迭代周期。 请注意,updater是更新模型参数的常用函数,它接受批量大小作为参数。 它可以是d2l.sgd函数,也可以是框架的内置优化函数。
def train_epoch_ch3(net,train_iter,loss,updater):
if isinstance(net,torch.nn.Module):
# 将模型设置为训练模式,需要计算梯度
net.train()
metric.Accumulator(3) # 长度为3的迭代器,来累加需要的信息
for X,y in train_iter:
y_hat = net(X) # 通过softmax得到预测值
l = loss(y_hat,y) # 计算损失函数
# 如果updater是torch.optim.Optimizer(python内置的优化器)
if isinstance(updater,torch.optim.Optimizer): # 使用PyTorch内置的优化器和损失函数
updater.zero_grad() # 梯度清零
l.backward() # 反向传播
updater.step() # 对参数进行一次更新
metric.add( # float(l) * len(y) 就是总的训练损失累加
# 因为内置的损失函数会自动对loss取均值,所以得到的损失要乘以len(y)
float(l) * len(y),accuracy(y_hat,y),
y.size().numel()) # y.size().numel()相当于y.numel,求tensor包含的元素个数
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]
定义一个在动画中绘制数据的实用程序类,代码略。
接下来我们实现一个训练函数, 它会在train_iter访问到的训练数据集上训练一个模型net。 该训练函数将会运行多个迭代周期(由num_epochs指定)。 在每个迭代周期结束时,利用test_iter访问到的测试数据集对模型进行评估。 我们将利用Animator类来可视化训练进度。
def train_ch3(net,train_iter,test_iter,loss,num_epochs,updater):
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): # 扫num_epochs遍数据
# 对整个数据扫一次,返回训练损失和训练精度
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
# 在net()这个函数中,W和b两个参数都被指定了requires_grad=True
# 同时注意W和b都是全局变量,因此在此处调用net()时,会自动调用W和b
# 计算在测试数据集上模型的精度
test_acc = evaluate_accuracy(net, test_iter)
# 在animator中显示,训练误差,训练精度以及测试的精度
animator.add(epoch + 1, train_metrics + (test_acc,))
# 训练损失和训练精度
train_loss, train_acc = train_metrics
小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1:
lr = 0.1
def updater(batch_size):
return d2l.sgd([W,b],lr,batch_size)
训练模型10个迭代周期:
8. 预测
现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。