3.5 softmax回归的从零开始实现

通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。通过代码从零实现softmax回归。

参考资料:李沐《动手学深度学习-Pytorch版》ch3 线性神经网络
开源地址:动手学深度学习
链接至上一节:3.4 softmax回归
此篇仅仅学习记录,更详细的内容可参考开源的书和代码以及b站上李沐老师的视频动手学深度学习在线课程。

文章目录

  • 1. 图像分类数据集-Fashion-MNIST数据集
    • 1.0 导入Python包
    • 1.1 读取数据集
    • 1.2 读取小批量
    • 1.3 整合所有组件
  • 2. softmax回归的从零开始实现
    • 2.1 初始化模型参数
    • 2.2 定义softmax操作
    • 2.3 定义模型
    • 2.4 定义损失函数
    • 2.5 分类精度
    • 2.6 训练
    • 2.7 预测
  • 3.引用
  • 4. softmax回归的简洁实现

1. 图像分类数据集-Fashion-MNIST数据集

1.0 导入Python包

%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l
from IPython import display
import torchvision.transforms as transforms
from torch import nn

d2l.use_svg_display()

1.1 读取数据集

MNIST数据集 :cite:LeCun.Bottou.Bengio.ea.1998 是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。将使用类似但更复杂的Fashion-MNIST数据集 :cite:Xiao.Rasul.Vollgraf.2017

通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中

# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数形式
# 并除以255使得所有像素的数值均为0-1
trans = transforms.ToTensor()
# 通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。
mnist_train = torchvision.datasets.FashionMNIST(root="C:\\Users\\HP\\Desktop\\data1", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(root="C:\\Users\\HP\\Desktop\\data2", train=False, transform=trans, download=True)
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-images-idx3-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-images-idx3-ubyte.gz to C:\Users\HP\Desktop\data1\FashionMNIST\raw\train-images-idx3-ubyte.gz
100%|█████████████████████████████████████████████████████████████████| 26421880/26421880 [00:07<00:00, 3750721.85it/s]
Extracting C:\Users\HP\Desktop\data1\FashionMNIST\raw\train-images-idx3-ubyte.gz to C:\Users\HP\Desktop\data1\FashionMNIST\raw

Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-labels-idx1-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-labels-idx1-ubyte.gz to C:\Users\HP\Desktop\data1\FashionMNIST\raw\train-labels-idx1-ubyte.gz
100%|████████████████████████████████████████████████████████████████████████| 29515/29515 [00:00<00:00, 125064.16it/s]
Extracting C:\Users\HP\Desktop\data1\FashionMNIST\raw\train-labels-idx1-ubyte.gz to C:\Users\HP\Desktop\data1\FashionMNIST\raw

Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz to C:\Users\HP\Desktop\data1\FashionMNIST\raw\t10k-images-idx3-ubyte.gz
100%|███████████████████████████████████████████████████████████████████| 4422102/4422102 [00:02<00:00, 1899527.32it/s]
Extracting C:\Users\HP\Desktop\data1\FashionMNIST\raw\t10k-images-idx3-ubyte.gz to C:\Users\HP\Desktop\data1\FashionMNIST\raw

Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz to C:\Users\HP\Desktop\data1\FashionMNIST\raw\t10k-labels-idx1-ubyte.gz
100%|██████████████████████████████████████████████████████████████████████████████████████| 5148/5148 [00:00

Fashion-MNIST数据集由10个类别的图像组成,每个类别由训练数据集(train dataset)中的6000张图像和测试数据集(test dataset)中的1000张图像组成。因此,训练集和测试集分别包含60000和10000张图像。测试数据集不会用于训练,只用于评估模型性能。

每个输入图像的高度和宽度均为28像素。数据集由灰度图像组成,其通道数为1。

len(mnist_train), len(mnist_test)
(60000, 10000)

每个输入图像的高度和宽度均为28像素。数据集由灰度图像组成,其通道数为1。
为了简洁起见,将高度 h h h像素、宽度 w w w像素图像的形状记为 h × w h \times w h×w或( h h h, w w 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(短靴)。

get_fashion_mnist_labels函数用于在数字标签索引及其文本名称之间进行转换

# 定义函数get_fashion_mnist_labels用于在数字标签索引及其文本名称之间进行转换。
# 函数输入参数labels是一个整数列表,每个整数表示一个数据样本的标签。函数将每个标签转换为相应的文本标签,并返回一个文本标签列表。
def get_fashion_mnist_labels(labels):
    """返回Fashion-MNIST数据集的文本标签"""
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
                  'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
    # 函数使用int(i)将输入标签列表中的每个标签转换为整数,并使用这个整数从text_labels列表中获取相应的文本标签。
    # 最后,函数返回一个文本标签列表。
    return [text_labels[int(i)] for i in labels]

show_images函数用于可视化样本

# 创建函数show_images来可视化样本
# 输入参数:
# imgs:一个包含多个图像的列表。每个图像可以是一个PIL图像对象或一个PyTorch张量。
# num_rows和num_cols:要在子图中显示的行数和列数。
# titles(可选):一个包含每个图像标题的列表。该列表应该和imgs列表具有相同的长度。如果未指定,则不显示标题。
# scale(可选):每个子图的缩放比例。默认值为1.5。
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
    """绘制图像列表"""
    # 首先计算了子图的总数,并根据num_rows和num_cols参数创建了一个子图网格。
    figsize = (num_cols * scale, num_rows * scale)
    # 使用了Matplotlib库的subplots()函数来创建一个包含多个子图的网格。
    # 使用了d2l.plt模块中的subplots()函数来创建一个num_rows行、num_cols列的网格,每个子图的大小为figsize指定的大小。
    # _表示我们对该函数的返回值不感兴趣,而axes变量是返回的子图对象的数组。
    _, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
    # 在Matplotlib中,subplots()函数返回的对象是一个二维数组,其中每个元素都是一个子图对象。
    # 使用flatten()方法,它将一个多维数组展平为一个一维数组。
    axes = axes.flatten()
    # 使用zip()函数将每个图像和相应的子图对象配对,并使用enumerate()函数获取每个子图的索引。
    for i,(ax, img) in enumerate(zip(axes, imgs)):
        
        # 对于每个子图,函数首先检查图像是否是一个PyTorch张量,
        # 并使用img.numpy()将其转换为一个NumPy数组,然后使用ax.imshow()方法将图像绘制到子图中。
        if torch.is_tensor(img):
            # 图像张量
            ax.imshow(img.numpy())
        else:
            # PIL图像
            ax.imshow(img)
        # 函数隐藏了子图的x轴和y轴标签
        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))
array([,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ,
       ], dtype=object)

3.5 softmax回归的从零开始实现_第1张图片

1.2 读取小批量

为了读取训练集和测试集时更容易,使用内置的数据迭代器,而不是从零开始创建。在每次迭代中,数据加载器每次都会读取一小批量数据,大小batch_size。通过内置数据迭代器,可以随机打乱了所有样本,从而无偏见地读取小批量。

batch_size = 256

def get_dataloader_workers():
    """使用4个进程来读取数据"""
    return 4

# 使用PyTorch的DataLoader类来创建一个数据迭代器train_iter,该迭代器可以用于遍历MNIST训练集中的所有图像和标签。
# DataLoader类的输入参数包括要加载的数据集、批大小、是否对数据进行洗牌、以及可选的数据加载器工作进程数量等。
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,
                            num_workers=get_dataloader_workers())

查看读取训练数据所需的时间。

timer = d2l.Timer()
for X, y in train_iter:
    continue

f'{timer.stop():.2f} sec'
'14.26 sec'

1.3 整合所有组件

现在定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。这个函数返回训练集和验证集的数据迭代器。此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一种形状。

def load_data_fashion_mnist(batch_size, resize=None):  #@save
    """下载Fashion-MNIST数据集,然后将其加载到内存中"""
    trans = [transforms.ToTensor()]
    
    # 首先检查resize参数是否为真。
    # 如果是,则将一个Resize预处理操作插入到预处理操作列表中。Resize操作用于将图像缩放到指定大小。
    # 接下来,我们使用transforms.Compose函数将所有预处理操作组合成一个transforms.Compose对象。
    # Compose对象可以将多个预处理操作串联起来,并将它们应用于输入数据。
    if resize:
        trans.insert(0, transforms.Resize(resize))
    trans = transforms.Compose(trans)
    
    
    mnist_train = torchvision.datasets.FashionMNIST(
        root="C:\\Users\\HP\\Desktop\\data1", train=True, transform=trans, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(
        root="C:\\Users\\HP\\Desktop\\data2", 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

2. softmax回归的从零开始实现

使用Fashion-MNIST数据集,并设置数据迭代器的批量大小为256。

batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)

2.1 初始化模型参数

同线性回归一样,这里的每个样本都将用固定长度的向量表示。原始数据集中的每个样本都是 28 × 28 28 \times 28 28×28的图像。将展平每个图像,把它们看作长度为784的向量。

在softmax回归中,输出与类别一样多。因为数据集有10个类别,所以网络输出维度为10。因此,权重将构成一个 784 × 10 784 \times 10 784×10的矩阵,偏置将构成一个 1 × 10 1 \times 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)

2.2 定义softmax操作

实现softmax由三个步骤组成:

  1. 对每个项求幂(使用exp);
  2. 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
  3. 将每一行除以其规范化常数,确保结果的和为1。

s o f t m a x ( X ) i j = exp ⁡ ( X i j ) ∑ k exp ⁡ ( X i k ) . \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}. softmax(X)ij=kexp(Xik)exp(Xij).

def softmax(X):
    X_exp = torch.exp(X)
    # 给定一个矩阵`X`,可以对所有元素求和(默认情况下)。也可以只求同一个轴上的元素,即同一列(轴0)或同一行(轴1)。
    # 当调用`sum`运算符时,可以指定保持在原始张量的轴数,而不折叠求和的维度。
    partition = X_exp.sum(1, keepdim=True)
    return X_exp / partition # 这里应用了广播机制

通过softmax函数,对于任何随机输入,可以将每个元素变成一个非负数。此外,依据概率原理,每行总和为1。

X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
(tensor([[0.0502, 0.3213, 0.2161, 0.1118, 0.3005],
         [0.0515, 0.6224, 0.0304, 0.1231, 0.1726]]),
 tensor([1., 1.]))

2.3 定义模型

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

# 定义了一个简单的全连接神经网络,用于对MNIST图像进行分类。
# 该网络使用了一个权重矩阵W和一个偏置向量b,其中W的形状为784x10,b的形状为(1, 10)。
# 这意味着W包含了784个输入特征和10个输出特征,b包含了10个偏置项,每个输出特征对应一个偏置项。

def net(X):
    # 首先使用reshape()函数将输入张量变形为形状为(batch_size, 784)的二维张量,以便我们可以将其与权重矩阵相乘。
    # matmul()函数用于计算矩阵乘积,它将输入张量与权重矩阵相乘,并将偏置向量加到结果中。
    # 最后,softmax()函数用于计算每个输出特征的概率分布,它将网络输出的每个元素转换为一个非负数,并将它们归一化为和为1的概率分布。
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

2.4 定义损失函数

使用交叉熵损失函数。交叉熵采用真实标签的预测概率的负对数似然。
l ( y , y ^ ) = − ∑ j = 1 q y j log ⁡ y ^ j . l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j. l(y,y^)=j=1qyjlogy^j.
这里不使用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]])
y_hat[[0, 1], y]
tensor([0, 2])
tensor([0.1000, 0.5000])

定义交叉熵损失函数cross_entropy

def cross_entropy(y_hat, y):
    # range(len(y_hat))生成一个包含从0到len(y_hat)-1的整数序列的张量,该序列用于选择每个批次中正确的预测概率
    # y_hat[range(len(y_hat)), y]使用y张量中的每个标签作为索引来选择y_hat张量中对应的预测概率。
    return -torch.log(y_hat[range(len(y_hat)), y])

cross_entropy(y_hat, y)
tensor([2.3026, 0.6931])

2.5 分类精度

给定预测概率分布y_hat,当必须输出硬预测(hard prediction)时,通常选择预测概率最高的类。当预测与标签分类y一致时,即是正确的。分类精度即正确预测数量与总预测数量之比。 虽然直接优化精度可能很困难(因为精度的计算不可导),但精度通常是我们最关心的性能衡量标准,在训练分类器时几乎总会关注它。

为了计算精度,执行以下操作。首先,如果y_hat是矩阵,那么假定第二个维度存储每个类的预测分数。使用argmax获得每行中最大元素的索引来获得预测类别。然后将预测类别与真实y元素进行比较。由于等式运算符“==”对数据类型很敏感,因此将y_hat的数据类型转换为与y的数据类型一致。结果是一个包含0(错)和1(对)的张量。最后,求和会得到正确预测的数量。

def accuracy(y_hat, y):
    """计算预测正确的数量"""
    # 首先,如果`y_hat`是矩阵,那么假定第二个维度存储每个类的预测分数
    if len(y_hat.shape)>1 and y_hat.shape[1] > 1:
        # 使用argmax获得每行中最大元素的索引来获得预测类别
        y_hat = y_hat.argmax(axis=1)
    # 将预测类别与真实y元素进行比较。由于等式运算符“==”对数据类型很敏感,因此将y_hat的数据类型转换为与y的数据类型一致
    # 结果是一个包含0(错)和1(对)的张量
    cmp = y_hat.type(y.dtype) == y
    # 求和得到正确预测的数量
    return float(cmp.type(y.dtype).sum())

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

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

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

# 定义了一个函数evaluate_accuracy,用于计算模型在指定数据集上的准确率
# 输入参数包括模型net和数据集data_iter
def evaluate_accuracy(net, data_iter):  
    """计算在指定数据集上模型的精度"""
    # 如果模型是一个PyTorch的nn.Module对象,则通过调用net.eval()方法将模型设置为评估模式。
    # 这会禁用Dropout和Batch Normalization等训练时使用的技巧,以确保模型在评估时的输出结果是确定性的。
    if isinstance(net, torch.nn.Module):
        net.eval()  # 将模型设置为评估模式
    # 创建一个名为metric的累加器,该累加器有两个条目,用于跟踪正确的预测数和预测总数。这个累加器是自定义的Accumulator类的实例。
    metric = Accumulator(2)  # 正确预测数、预测总数
    
    # 使用PyTorch的torch.no_grad()上下文管理器,禁用梯度计算,以加快计算速度。
    with torch.no_grad():
        
        # 遍历数据集中的所有数据,对于每个数据样本,计算模型的输出,并计算预测标签的准确率。
        # 将正确预测的数量和总预测数量添加到metric累加器中。
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    
    # 最后,返回正确预测的数量除以总预测数量的比例,这就是模型在指定数据集上的准确率。
    return metric[0] / metric[1]

定义一个实用程序类Accumulator,用于对多个变量进行累加。在上面evaluate_accuracy函数中,在 Accumulator实例中创建了2个变量,分别用于存储正确预测的数量和预测的总数量。当遍历数据集时,两者都将随着时间的推移而累加。

# 该实例对于求和数据集中所有样本的某个指标的值非常有用,如准确率、损失等。
# 在每次遍历数据集时,可以使用Accumulator类的实例进行累加,并在遍历完所有样本后,将结果除以数据集中的总样本数得到指标的平均值。

class Accumulator: 
    """在n个变量上累加"""
    
    # _init__方法接受一个整数n作为输入,创建一个长度为n的列表self.data,并将其中所有的元素初始化为0.0
    def __init__(self, n):
        self.data = [0.0] * n
        
    # add方法使用Python的可变参数列表*args接受任意数量的参数。这些参数可以是整数、浮点数或其他可转换为浮点数的类型
    # zip(self.data, args)将self.data和args中相同索引的元素打包成一个元组,返回一个迭代器。
    # for a, b in zip(self.data, args)循环遍历self.data和args中相同索引的元素,分别赋值给变量a和b。
    # a + float(b)将a和b转换为浮点数后相加,返回一个浮点数。
    # [a + float(b) for a, b in zip(self.data, args)]将所有相加后的浮点数放入一个列表中。   
    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]
    
    # reset方法将self.data中的所有元素重置为0.0
    def reset(self):
        self.data = [0.0] * len(self.data)
        
    # __getitem__方法接受一个整数idx作为输入,并返回self.data中索引为idx的元素
    def __getitem__(self, idx):
        return self.data[idx]

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

evaluate_accuracy(net, test_iter)
0.0961

2.6 训练

首先,定义一个函数来训练一个迭代周期。这里的updater是更新模型参数的常用函数,它接受批量大小作为参数。它可以是d2l.sgd函数,也可以是框架的内置优化函数。

def train_epoch_ch3(net, train_iter, loss, updater): 
    # 函数的输入参数包括模型net、训练数据集train_iter、损失函数loss和更新器updater
    """训练模型一个迭代周期"""
    
    # 如果模型是一个PyTorch的nn.Module对象,则通过调用net.train()方法将模型设置为训练模式
    # 这会启用Dropout和Batch Normalization等训练时使用的技巧
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
        # 创建一个名为metric的累加器,该累加器有三个条目,用于跟踪训练损失总和、训练准确度总和和样本数
    
    # 训练损失总和、训练准确度总和、样本数
    metric = Accumulator(3)
    # 遍历数据集中的所有数据,对于每个数据样本,计算模型的输出,并计算训练损失和训练准确度
    # 将训练损失、训练准确度和样本数量添加到metric累加器中。
    for X, y in train_iter:
        # 计算梯度并更新参数
        y_hat = net(X)
        l = loss(y_hat, y)
        # 如果更新器是一个PyTorch的torch.optim.Optimizer对象,则使用PyTorch的优化器和损失函数来计算梯度并更新参数
        if isinstance(updater, torch.optim.Optimizer):
            # 使用PyTorch内置的优化器和损失函数
            updater.zero_grad()  # 调用updater.zero_grad()方法将梯度清零
            l.mean().backward()  # 调用l.mean().backward()方法计算损失对参数的梯度
            updater.step()  # 调用updater.step()方法使用梯度更新参数
        # 如果更新器不是PyTorch的优化器对象,则使用定制的优化器和损失函数来计算梯度并更新参数
        else:
            # 使用定制的优化器和损失函数
            l.sum().backward()  # 调用l.sum().backward()方法计算损失对参数的梯度
            updater(X.shape[0])  # 调用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:
    """在动画中绘制数据"""
    
    # __init__方法接受多个可选参数,包括x轴标签、y轴标签、图例、x轴范围、y轴范围、x轴刻度、y轴刻度、线条样式、子图行数和列数、以及图像大小
    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()   # 调用d2l.use_svg_display()方法将绘图设为SVG格式
        self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)   # 调用d2l.plt.subplots()方法创建一个子图
        
        # 如果子图的行列数为1,则将self.axes转换为一个长度为1的列表
        if nrows * ncols == 1:
            self.axes = [self.axes, ]
        
        # 使用lambda函数捕获参数
        # self.config_axes是一个函数,用于配置子图的属性,包括x轴标签、y轴标签、x轴范围、y轴范围、x轴刻度、y轴刻度和图例
        self.config_axes = lambda: d2l.set_axes(
            self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        
        # self.X和self.Y分别是x轴和y轴的数据列表,初始值为None
        # self.fmts是一个包含多个字符串的元组,用于指定绘制线条的样式
        self.X, self.Y, self.fmts = None, None, fmts
    
    # add方法接受两个参数x和y,分别表示x轴和y轴的数据
    def add(self, x, y):
        # 向图表中添加多个数据点
        
        # 如果y不是一个列表,则将其转换为一个仅包含一个元素的列表
        if not hasattr(y, "__len__"):
            y = [y]
        n = len(y)
        # 如果x不是一个列表,则将其复制n次,其中n是y的长度
        if not hasattr(x, "__len__"):
            x = [x] * n
        # 如果self.X为None,则将其初始化为一个包含n个空列表的列表
        if not self.X:
            self.X = [[] for _ in range(n)]
        # 如果self.Y为None,则将其初始化为一个包含n个空列表的列表
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        # 将每个x和y添加到相应的列表中
        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()
        
        # 使用self.fmts中指定的样式绘制每个列表中的数据
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        
        # 调用self.config_axes()方法配置子图的属性
        self.config_axes()
        
        # 使用display.display()和display.clear_output(wait=True)方法将子图显示在Jupyter Notebook中
        display.display(self.fig)
        display.clear_output(wait=True)

接下来实现一个训练函数,在train_iter访问到的训练数据集上训练一个模型net。该训练函数将会运行多个迭代周期(由num_epochs指定)。在每个迭代周期结束时,利用test_iter访问到的测试数据集对模型进行评估。将利用Animator类来可视化训练进度。

# 函数的输入参数包括模型net、训练数据集train_iter、测试数据集test_iter、损失函数loss、训练周期数num_epochs和更新器updater。
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): 
    """训练模型"""
    
    # 创建一个Animator类的实例,用于动态可视化训练和测试结果。
    # 设置x轴标签为“epoch”,x轴范围为[1, num_epochs],y轴范围为[0.3, 0.9],图例包括“train loss”、“train acc”和“test acc”。
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
                        legend=['train loss', 'train acc', 'test acc'])
    
    # 在每个epoch中,使用train_epoch_ch3函数训练模型,并记录训练损失和训练准确度。
    # 使用evaluate_accuracy函数评估模型在测试集上的性能。
    # 将训练损失、训练准确度和测试准确度添加到Animator实例中,以动态展示训练和测试结果的变化过程。
    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
    
    # 在训练结束后,检查训练损失和训练准确度是否满足要求
    # (训练损失小于0.5,训练准确度在0.7到1之间),并检查测试准确度是否满足要求(在0.7到1之间)。
    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
# 使用之前定义的小批量随机梯度下降来优化模型的损失函数,设置学习率为0.01
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)

3.5 softmax回归的从零开始实现_第2张图片

2.7 预测

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

def predict_ch3(net, test_iter, n=6):  #@save
    """预测标签(定义见第3章)"""
    
    # 使用test_iter迭代器获取测试数据集的第一个批次数据,并将其转换为NDArray类型。
    for X, y in test_iter:
        break
    
    # 使用get_fashion_mnist_labels函数获取真实标签trues,并使用模型net对数据进行预测。
    trues = d2l.get_fashion_mnist_labels(y)
    # 将预测结果转换为标签,并使用get_fashion_mnist_labels函数获取预测标签preds。
    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)

3.5 softmax回归的从零开始实现_第3张图片

3.引用

引用原书:

@book{zhang2019dive,
    title={Dive into Deep Learning},
    author={Aston Zhang and Zachary C. Lipton and Mu Li and Alexander J. Smola},
    note={\url{http://www.d2l.ai}},
    year={2020}
}

4. softmax回归的简洁实现

链接至下一节:3.6 softmax回归的简洁实现

你可能感兴趣的:(动手学深度学习Pytorch版,回归,数据挖掘,人工智能,python,算法,深度学习,神经网络)