Softmax回归代码实现#

本篇将详细的使用代码来实现

导入数据集#

我们这里的任务是对10个类别的“时装”图像进行分类,使用FashionMNIST数据集(zalandoresearch/fashion-mnist )。上图给出了FashionMNIST中数据的若干样例图,其中每个小图对应一个样本。
FashionMNIST数据集中包含已经预先划分好的训练集和测试集,其中训练集共60,000张图像,测试集共10,000张图像。每张图像均为单通道黑白图像,大小为32*32pixel,分属10个类别。
我们设置数据迭代器的批量大小为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)

上面注释的为使用D2L封装好的代码,但是我看的一头雾水完全不知道原理是什么,所以下面的为拆开沐神封装好的代码,具体理解下。(PS:数据包不能下载到指定位置强迫症真的很难受)

首先导入必要的包

import os
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms,datasets
import matplotlib.pyplot as plt

配置训练环境和超参数

# 配置GPU,这里有两种方式
## 方案一:使用os.environ
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
# 方案二:使用“device”,后续对要使用GPU的变量用.to(device)即可
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")

数据读入和加载
这里同时展示两种方式:

  • 下载并使用PyTorch提供的内置数据集

  • 从网站下载以csv格式存储的数据,读入并转成预期的格式
    第一种数据读入方式只适用于常见的数据集,如MNIST,CIFAR10等,PyTorch官方提供了数据下载。这种方式往往适用于快速测试方法(比如测试下某个idea在MNIST数据集上是否有效)
    第二种数据读入方式需要自己构建Dataset,这对于PyTorch应用于自己的工作中十分重要

同时,还需要对数据进行必要的变换,比如说需要将图片统一为一致的大小,以便后续能够输入网络训练;需要将数据格式转为Tensor类,等等。

这些变换可以很方便地借助torchvision包来完成,这是PyTorch官方用于图像处理的工具库,上面提到的使用内置数据集的方式也要用到。PyTorch的一大方便之处就在于它是一整套“生态”,有着官方和第三方各个领域的支持。这些内容我们会在后续课程中详细介绍。

# 首先设置数据变换
# transforms.Compose是一个用于组合多个数据变换的类
# Compose()类会将transforms列表里面的transform操作进行遍历,然后将数据依次传递给每个transform操作,最后返回处理后的数据
image_size = 28
data_transform = transforms.Compose([
    #transforms.ToPILImage(),   # 将torch.Tensor或numpy.ndarray类型图像转为PIL.Image类型图像。这段里面可以移除transforms.ToPILImage(),因为 FashionMNIST 数据集已经是 PIL.Image 类型
    transforms.Resize(image_size),#按给定尺寸对图像进行缩放
    transforms.ToTensor() #将PIL.Image或numpy.ndarray类型图像转为torch.Tensor类型图像
])

使用torchvision自带数据集,下载可能需要一段时间

# train表示是否是训练集,download表示是否需要下载,transform表示是否需要进行数据变换
train_data = datasets.FashionMNIST(root='../raw/data/', train=True, download=True, transform=data_transform)
test_data = datasets.FashionMNIST(root='../raw/data/', train=False, download=True, transform=data_transform)

在构建训练和测试数据集完成后,需要定义DataLoader类,以便在训练和测试时加载数据

batch_size = 256
num_workers = 0  #mac 不知道为什么变为4也报错   # 对于Windows用户,这里应设置为0,否则会出现多线程错误
# DataLoader是一个用于生成batch数据的迭代器,可以设置batch_size、shuffle、num_workers等参数
#batch_size是指每个批次中包含的样本数量。shuffle=True表示在每个epoch开始时,将训练数据集打乱顺序,以增加模型的泛化能力。num_workers是指用于数据加载的线程数量,可以加快数据加载的速度。drop_last=True表示如果训练数据集的样本数量不能被batch_size整除,最后一个不完整的批次将被丢弃。
train_iter = DataLoader(train_data, batch_size=batch_size, shuffle=True, num_workers=num_workers, drop_last=True)
test_iter = DataLoader(test_data, batch_size=batch_size, shuffle=False, num_workers=num_workers)

读入后,我们可以做一些数据可视化操作,主要是验证我们读入的数据是否正确

这段代码使用 matplotlib 库中的 imshow 函数来显示图像。以下是每个部分的详细说明:

  1. plt.imshow:

    • imshowmatplotlib.pyplot 模块中的一个函数,用于显示图像。

  2. image_batch[0][0]:

    • image_batch 是从数据加载器中提取的一批图像张量(tensor),形状通常是 (batch_size, channels, height, width)

    • image_batch[0] 选择了这批图像中的第一张图像。它的形状通常是 (channels, height, width)

    • image_batch[0][0] 选择了这张图像的第一个通道(对于灰度图像来说,只有一个通道)。所以结果是一个二维张量,形状是 (height, width)

  3. cmap="gray":

    • cmap 参数指定了图像的颜色映射(colormap)。"gray" 表示使用灰度颜色映射,这通常用于显示灰度图像(即单通道图像)。

# 获取一个数据批次
image_batch, label_batch = next(iter(train_iter))
print(image_batch.shape, label_batch.shape)

# 显示批次中的第一张图像
plt.imshow(image_batch[0][0], cmap="gray")
plt.show()
torch.Size([256, 1, 28, 28]) torch.Size([256])
../_images/020896240e93cf96ce1a03deca2789e857c0250485af5c1f645070e92f423504.png

定义类神经网路模型#

Fashion-MNIST的影像是28(width)×28(height)=784(pixels)。原始数据集中的每个样本都是\(28×28\)的图像,输出图像类别有 10 个。本节将展平每个图像,把它们看作长度为 784 的向量。网络只有一层,输入的是 784 维,输出 10 维,也就是说:

  • 样本矩阵\(X\)\(6000×784\)的矩阵

  • 参数矩阵\(W\)\(784×10\)的矩阵

  • 偏置\(b\)\(1×10\)的矩阵

  • 输出\(Y\)\(6000×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)
print(W)
print(b)
tensor([[ 0.0058,  0.0149,  0.0017,  ...,  0.0134, -0.0031, -0.0022],
        [ 0.0041,  0.0023, -0.0163,  ...,  0.0059, -0.0010,  0.0082],
        [ 0.0150, -0.0166, -0.0157,  ..., -0.0013,  0.0012,  0.0246],
        ...,
        [ 0.0029,  0.0039, -0.0034,  ..., -0.0030,  0.0005, -0.0085],
        [-0.0108, -0.0026,  0.0183,  ..., -0.0112,  0.0001, -0.0023],
        [ 0.0059,  0.0161,  0.0095,  ...,  0.0137,  0.0063,  0.0076]],
       requires_grad=True)
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], requires_grad=True)

softmax回归为何要使用正态分布初始化的权重W,并把偏置初始化为0

在softmax回归中,我们需要对输入特征进行加权求和,并将结果通过softmax函数转换为概率分布。权重W和偏置b是模型的参数,它们的初始化对模型的性能和收敛速度都有影响。

权重W的初始化通常采用正态分布(也称为高斯分布),原因如下:

  1. 对称性破坏:如果权重初始化为相同的值(如0),则每个特征的梯度更新也会相同,这可能导致模型对称性。通过使用正态分布初始化,可以打破对称性,使得每个权重的初始值不同。

  2. 避免梯度消失或爆炸:在深层神经网络中,如果权重初始化过大,可能导致梯度爆炸;如果权重初始化过小,可能导致梯度消失。正态分布初始化可以在一定程度上控制权重的初始范围,帮助避免梯度问题。

  3. 适应数据分布:正态分布是自然界中许多现象的分布模式,它可以更好地适应数据的分布特征。通过使用正态分布初始化权重,可以使模型更好地适应输入数据的分布。

而偏置b的初始化为0是因为偏置项的作用是调整模型的输出,使其与真实标签更接近。在softmax回归中,偏置项的初始化为0可以看作是对模型输出的一个初始偏置,因为softmax函数的特性会使得输出概率总和为1。因此,将偏置初始化为0是一个合理的选择。

需要注意的是,权重和偏置的初始化方法并不是唯一的,根据具体的问题和模型结构,可能会有其他的初始化策略。这里提到的正态分布和0初始化是一种常见的选择。

定义softmax操作#

给定一个矩阵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]])

#torch.sum(input, list: dim, bool: keepdim=False, dtype=None) → Tensor 
#input:输入一个tensor
#dim:要求和的维度,可以是一个列表
#keepdim:是否保持求和维度个维度,如果需要保持应当keepdim=True 

print(X,"\n",X.shape,"\n--------------------")
print(X.sum(0, keepdim=True),"\n",X.sum(0, keepdim=True).shape,"\n--------------------")
print(X.sum(1, keepdim=True),"\n",X.sum(1, keepdim=True).shape,"\n--------------------")
print(X.sum(1),"\n",X.sum(1).shape)
tensor([[1., 2., 3.],
        [4., 5., 6.]]) 
 torch.Size([2, 3]) 
--------------------
tensor([[5., 7., 9.]]) 
 torch.Size([1, 3]) 
--------------------
tensor([[ 6.],
        [15.]]) 
 torch.Size([2, 1]) 
--------------------
tensor([ 6., 15.]) 
 torch.Size([2])

回想一下,实现softmax由三个步骤组成:

  1. 对每个项求幂(使用exp);

  2. 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;

  3. 将每一行除以其规范化常数,确保结果的和为1。

在查看代码之前,我们回顾一下这个表达式:

\[ \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}. \]

分母或规范化常数,有时也称为配分函数(其对数称为对数-配分函数)。 该名称来自统计物理学中一个模拟粒子群分布的方程。

def softmax(X):
    X_exp = torch.exp(X)
    #print(X_exp)
    partition = X_exp.sum(1, keepdim=True) #如果有keepdim那么就是列的维度
    #print(partition)
    softmax_dota=X_exp / partition
    return softmax_dota # 这里应用了广播机制

正如上述代码,对于任何随机输入,我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1。

整个函数是对输出\(Y\)做概率转换,输出\(Y\)是6000×10的矩阵,要沿着横轴方向求和,所以是 axis=1

官方文档:https://pytorch.org/docs/1.2.0/torch.html#torch.exp

torch.exp的用法样例可以参考下面内容,常数\(e\)等于2.704

def softmax_test(X):
    print(f"X等于:{X}")
    X_exp = torch.exp(X)
    print(f"X_exp等于:{X_exp}")
    partition = X_exp.sum(1, keepdim=True) #如果有keepdim那么就是列的维度
    print(f"partition等于:{partition}")
    return X_exp / partition  # 这里应用了广播机制
ccccc=torch.exp(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]))
print(f"ccccc等于:{ccccc}")
X = torch.normal(0, 1, (2, 5))
X_prob = softmax_test(X)
X_prob, X_prob.sum(1)
ccccc等于:tensor([[  2.7183,   7.3891,  20.0855],
        [ 54.5982, 148.4132, 403.4288]])
X等于:tensor([[ 0.0078,  1.5513,  0.4311, -0.5112, -0.8403],
        [-1.2555,  0.2248, -0.5926,  0.2370,  0.8091]])
X_exp等于:tensor([[1.0078, 4.7174, 1.5389, 0.5998, 0.4316],
        [0.2849, 1.2520, 0.5529, 1.2675, 2.2458]])
partition等于:tensor([[8.2955],
        [5.6032]])
(tensor([[0.1215, 0.5687, 0.1855, 0.0723, 0.0520],
         [0.0509, 0.2235, 0.0987, 0.2262, 0.4008]]),
 tensor([1., 1.]))

注意,虽然这在数学上看起来是正确的,但我们在代码实现中有点草率。 矩阵中的非常大或非常小的元素可能造成数值上溢或下溢,但我们没有采取措施来防止这点。

定义模型#

定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape函数将每张原始图像展平为向量。

def net(X):
    #return softmax(torch.`(X.reshape((-1, W.shape[0])), W) + b)
    #正态分布初始化的权重W,偏置初始化为0
    tmp_=X.reshape((-1, W.shape[0]))  #将每张原始图像展平为向量。第一维是batch_size,第二维是特征数。Tensor:torch.Size([256,1,28,28])变成Tensor:torch.Size([256, 784])
    #W的维度是784*10,tmp_的维度是256*784(每张原始图像展平为向量),然后进行矩阵乘法,所以matmul_的维度是256*10
    #因为是二维相乘所以(n×m)×(m×p)=(n×p)。要是以为的话就是点积等于torch.dot。
    #如果跟高维度参考这个文章https://www.cnblogs.com/HOMEofLowell/p/15963140.html
    #然后矩阵的乘法参考这个https://www.ascotbe.com/2023/12/05/LinearAlgebra_0x03/#%E7%9F%A9%E9%98%B5%E7%9A%84%E4%B9%98%E6%B3%95
    matmul_=torch.matmul(tmp_, W)  

    sof=softmax( matmul_+ b) 
    #print(sof[0]) #这里打印第一张图片的预测结果,所有类别的概率,概率最大的类别即为预测类别,这里是10个类别,相加为1
    return sof

定义损失函数#

接下来,我们实现交叉熵损失函数。 这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题的数量。

交叉熵损失函数:https://blog.csdn.net/b1055077005/article/details/100152102

底是10的对数叫:常用对数 \(\log_{10}(x)\),有时写为\(\log(x)\)

用torch实现#

回顾一下,交叉熵采用真实标签的预测概率的负对数似然。 这里我们不使用Python的for循环迭代预测(这往往是低效的), 而是通过一个运算符选择所有元素。 下面,我们创建一个数据样本y_hat,其中包含2个样本在3个类别的预测概率, 以及它们对应的标签y。 有了y,我们知道在第一个样本中,第一类是正确的预测; 而在第二个样本中,第三类是正确的预测。 然后使用y作为y_hat中概率的索引, 我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。

y = torch.tensor([0, 2])
print(y)
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
print(y_hat)
#y_hat是一个2*3的数组。y_hat[[0,1],y]中的[0,1]指的是第一行和第二行的索引,后面的y等价于[0,2]。那么可以这么理解y_hat[0,0]和y_hat[1,2]。
print(f"对于第0样本y_hat,拿出y[0](值为0)对应的那个元素,对于第1个样本y_hat,拿出y[1](值为2)对应那个元素----------{y_hat[[0, 1], y]}")
print(f"等同于:y_hat[[0, 1], [0,2]]----------{y_hat[[0, 1], [0,2]]}")
print(f"y_hat[[0, 1], [2,1]]---------{y_hat[[0, 1], [2,1]]}")
tensor([0, 2])
tensor([[0.1000, 0.3000, 0.6000],
        [0.3000, 0.2000, 0.5000]])
对于第0样本y_hat,拿出y[0](值为0)对应的那个元素,对于第1个样本y_hat,拿出y[1](值为2)对应那个元素----------tensor([0.1000, 0.5000])
等同于:y_hat[[0, 1], [0,2]]----------tensor([0.1000, 0.5000])
y_hat[[0, 1], [2,1]]---------tensor([0.6000, 0.2000])

现在我们只需一行代码就可以实现交叉熵损失函数。

def cross_entropy(y_hat, y):
    return - torch.log(y_hat[range(len(y_hat)), y])

def tmp_cross_entropy(y_hat, y):
    print(f"len(y_hat):{len(y_hat)}")
    print(f"range(len(y_hat)):{range(len(y_hat))}")
    tmp_=y_hat[range(len(y_hat)), y]
    print(f"tmp_:{tmp_}")
    print(f"-torch.log(torch.tensor(0.1)):{-torch.log(torch.tensor(0.1))}")
    print(f"-torch.log(torch.tensor(0.5)):{-torch.log(torch.tensor(0.5))}")
    return - torch.log(tmp_)

tmp_cross_entropy(y_hat, y)
len(y_hat):2
range(len(y_hat)):range(0, 2)
tmp_:tensor([0.1000, 0.5000])
-torch.log(torch.tensor(0.1)):2.3025851249694824
-torch.log(torch.tensor(0.5)):0.6931471824645996
tensor([2.3026, 0.6931])

分类精度#

给定预测概率分布y_hat,当我们必须输出硬预测(hard prediction)时, 我们通常选择预测概率最高的类。 许多应用都要求我们做出选择。如Gmail必须将电子邮件分类为“Primary(主要邮件)”、 “Social(社交邮件)”“Updates(更新邮件)”或“Forums(论坛邮件)”。 Gmail做分类时可能在内部估计概率,但最终它必须在类中选择一个。

def accuracy(y_hat, y):  #@save
    """计算预测正确的数量"""
    #传入的是一个y_hat的矩阵,每一行是一个样本的预测结果,每一列是一个类别的预测概率
    #y是一个标签列表,每个元素是一个样本的标签
    #如果y_hat.shape的列数大于1,说明y_hat是一个矩阵,然后y_hat.argmax(axis=1)返回每一行的最大值的索引,即预测的类别。
    #然后和y进行比较,如果相等,返回1,否则返回0,然后求和,即为预测正确的数量
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(dim=1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())


#将预测类别和真实y元素进行比较,因为做的是分类问题。
'''
y:tensor([0, 2])
y_hat:tensor([[0.1000, 0.3000, 0.6000],
        [0.3000, 0.2000, 0.5000]])
'''
def accuracy_test(y_hat,y):                           #给定预测值y_hat和真实值y,计算分类正确的类别数.
    """计算预测正确的数量"""
    if len(y_hat.shape)>1 and y_hat.shape[1]>1:#如果y_hat是一个二维矩阵的话,它的shape>1,它的列数也>1.
        print("什么都没做的时候:")
        print(f"y_hat:{y_hat}")
        print(f"y:{y}")
        print(f"y_hat.shape:{y_hat.shape}")
        print(f"y.shape:{y.shape}")
        print(f"y_hat.type:{y_hat.type}")
        print(f"y_hat.dtype:{y_hat.dtype}")
        print(f"y.type:{y.type}")
        print(f"y.dtype:{y.dtype}")
        y_hat=y_hat.argmax(axis=1) #对每一行求argmax-每一行中元素最大的那个下标存到y_hat里面。
        print("对每一行求argmax-每一行中元素最大的那个下标存到y_hat里面:")
        print(f"y_hat:{y_hat}")
        print(f"y:{y}")
        print(f"y_hat.shape:{y_hat.shape}")
        print(f"y.shape:{y.shape}")
        print(f"y_hat.type:{y_hat.type}")
        print(f"y_hat.dtype:{y_hat.dtype}")
        print(f"y.type:{y.type}")
        print(f"y.dtype:{y.dtype}")
    cmp=y_hat.type(y.dtype)==y  #y_hat和y的数据类型可能不一样,把y_hat转成y的数据类型,然后对比相同下标的数据是否相同,变成一个bool的tensor。
    print("y_hat和y的数据类型可能不一样,把y_hat转成y的数据类型,变成一个bool的tensor。")
    print(f"y_hat.type:{y_hat.type}")
    print(f"y_hat.dtype:{y_hat.dtype}")
    print(f"y.type:{y.type}")
    print(f"y.dtype:{y.dtype}")
    print(f"cmp:{cmp}")
    print("cmp转换成y的数据类型,然后求和")
    cmp.type(y.dtype)
    print(f"y.type:{y.type}")
    print(f"y.dtype:{y.dtype}")
    print(f"cmp.type:{cmp.type}")
    print(f"cmp.dtype:{cmp.dtype}")
    _t=cmp.sum()
    print("True被解释为1,False被解释为0,所以后面相加为1")
    print(f"cmp:{_t}")
    print("转换位浮点型")
    print(f"float(cmp.type(y.dtype).sum()):{float(cmp.type(y.dtype).sum())}")
    return float(cmp.type(y.dtype).sum())         #转成跟y一样的形状,求和。

我们将继续使用之前定义的变量y_hat和y分别作为预测的概率分布和标签。 可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。 第二个样本的预测类别是2(该行的最大元素为0.5,索引为2),这与实际标签2一致。 因此,这两个样本的分类精度率为0.5。

#accuracy(y_hat, y) / len(y)

accuracy_test(y_hat, y) / len(y)
什么都没做的时候:
y_hat:tensor([[0.1000, 0.3000, 0.6000],
        [0.3000, 0.2000, 0.5000]])
y:tensor([0, 2])
y_hat.shape:torch.Size([2, 3])
y.shape:torch.Size([2])
y_hat.type:<built-in method type of Tensor object at 0x7feb706a0b30>
y_hat.dtype:torch.float32
y.type:<built-in method type of Tensor object at 0x7feb706a0a40>
y.dtype:torch.int64
对每一行求argmax-每一行中元素最大的那个下标存到y_hat里面:
y_hat:tensor([2, 2])
y:tensor([0, 2])
y_hat.shape:torch.Size([2])
y.shape:torch.Size([2])
y_hat.type:<built-in method type of Tensor object at 0x7feb70683e20>
y_hat.dtype:torch.int64
y.type:<built-in method type of Tensor object at 0x7feb706a0a40>
y.dtype:torch.int64
y_hat和y的数据类型可能不一样,把y_hat转成y的数据类型,变成一个bool的tensor。
y_hat.type:<built-in method type of Tensor object at 0x7feb70683e20>
y_hat.dtype:torch.int64
y.type:<built-in method type of Tensor object at 0x7feb706a0a40>
y.dtype:torch.int64
cmp:tensor([False,  True])
cmp转换成y的数据类型,然后求和
y.type:<built-in method type of Tensor object at 0x7feb706a0a40>
y.dtype:torch.int64
cmp.type:<built-in method type of Tensor object at 0x7feb706a0450>
cmp.dtype:torch.bool
True被解释为1,False被解释为0,所以后面相加为1
cmp:1
转换位浮点型
float(cmp.type(y.dtype).sum()):1.0
0.5

同样,对于任意数据迭代器data_iter可访问的数据集, 我们可以评估在任意模型net的精度。

#评估准确性
def evaluate_accuracy(net, data_iter): 
    """计算在指定数据集上模型的精度"""
    if isinstance(net, torch.nn.Module): #检查一个对象是否属于指定的类型或类
        net.eval()  # 将模型设置为评估模式,softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)是一个张量,eval()方法将计算该张量的值并返回结果。
    metric = Accumulator(2)  # 正确预测数、预测总数
    with torch.no_grad():
        for X, y in data_iter:   # 不断叠加计算精度
            # y.numel()返回y中元素的个数
            # y为当前取一次数据的标签列表,一次取256个标签,y.numel()为列表的个数
            # X为图片数据,结构为Tensor:torch.Size([256, 1, 28, 28])
            #print(y[4]) #其中一条数据的标签
            #print(X[4]) #其中一条数据
            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 add(self, *args): #就是对每次成功预测的个数和总数进行分别累加
        result = []
        for a, b in zip(self.data, args):
            result.append(a + float(b))
        self.data = result

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]
    '''__getitem__的用法
    class MyList:
        def __init__(self):
            self.data = [1, 2, 3, 4, 5]

        def __getitem__(self, index):
            return self.data[index]
    my_list = MyList()
    print(my_list[2])  # 输出:3'''

由于我们使用随机权重初始化net模型, 因此该模型的精度应接近于随机猜测。 例如在有10个类别情况下的精度为0.1。

evaluate_accuracy(net, test_iter)
0.0871

训练#

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, 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

作为一个从零开始的实现,我们使用 3.2节中定义的 小批量随机梯度下降来优化模型的损失函数,设置学习率为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)
../_images/6d60c10d7253ba8c0646c7b4aa0a42008653f229344f754c2754d2d613975370.svg

预测#

现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。

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)
../_images/201b2bbf09f356b8d48bfe17e56d4e52cb024ac5b5898635763f06f1a4663cd4.svg