使用 PyTorch 学习生成式人工智能——使用 PyTorch 进行深度学习

42 阅读30分钟

本章内容包括

  • PyTorch 张量及其基本操作
  • 为 PyTorch 深度学习准备数据
  • 使用 PyTorch 构建和训练深度神经网络
  • 利用深度学习进行二分类和多分类任务
  • 创建验证集以决定训练的终止时机

在本书中,我们将使用深度神经网络生成多种内容,包括文本、图像、形状、音乐等。我假设你已经具备机器学习(ML),特别是人工神经网络的基础知识。本章将帮助你回顾一些关键概念,如损失函数、激活函数、优化器和学习率,这些都是开发和训练深度神经网络的核心。如果你对这些内容理解有不足,强烈建议先补充学习再继续本书项目。附录B对人工神经网络的架构和训练等基础技能和概念进行了总结。

:市面上有很多优秀的机器学习书籍可供选择,例如《Hands-on Machine Learning with Scikit-Learn, Keras, and TensorFlow》(2019,O’Reilly)和《Machine Learning, Animated》(2023,CRC Press),两者均使用TensorFlow构建神经网络。如果你偏好PyTorch,则推荐《Deep Learning with PyTorch》(2020,Manning Publications)。

生成式AI模型经常面临二分类或多分类的任务。例如,在生成对抗网络(GAN)中,判别器承担二分类任务,其目的是区分生成器生成的假样本与训练集中的真实样本。类似地,在文本生成模型中,无论是循环神经网络还是Transformer,核心任务都是从大量可能性中预测下一个字符或词语,本质上也是多分类任务。

本章将教你如何使用PyTorch构建深度神经网络,完成二分类和多分类任务,帮助你熟练掌握深度学习及分类任务。

具体来说,你将进行一个端到端的PyTorch深度学习项目,任务是对灰度服装图片进行分类,类别包括外套、包包、运动鞋、衬衫等。目的是让你掌握构建能执行二分类和多分类任务的深度神经网络,为后续章节中使用PyTorch深度神经网络构建各种生成模型做好准备。

训练生成式AI模型时,我们会使用多种数据格式,如原始文本、音频文件、图像像素和数值数组。PyTorch构建的深度神经网络无法直接接受这些数据格式作为输入,因此需要先将它们转换成神经网络能理解和接受的格式。具体而言,你将把各种原始数据转换成PyTorch张量(PyTorch用于表示和操作数据的基本数据结构),再将其输入生成式AI模型。因此,本章还将介绍数据类型基础、如何创建各种PyTorch张量以及它们在深度学习中的应用。

掌握分类任务在现实生活中有广泛应用。分类技术在医疗诊断中至关重要,例如判断患者是否患有某种疾病(如基于医学影像或检测结果判断是否患癌)。它们在许多商业场景中也非常关键,如股票推荐、信用卡欺诈检测等。分类任务同样是我们日常使用的许多系统和服务的基础,比如垃圾邮件检测和人脸识别。

2.1 PyTorch中的数据类型

本书将使用来自各种来源和格式的数据集,深度学习的第一步是将输入转换为数字数组。
本节将介绍PyTorch如何将不同格式的数据转换成代数结构——张量(tensor)。张量可以视为多维数字数组,类似于NumPy数组,但有几个关键区别,最重要的是支持GPU加速训练。根据最终用途,张量有不同类型,你将学习如何创建不同类型的张量以及何时使用它们。本节将以美国46位总统的身高为例,讲解PyTorch中的数据结构。

请参照附录A的说明,在你的计算机上创建虚拟环境并安装PyTorch和Jupyter Notebook。然后在虚拟环境中打开Jupyter Notebook应用,并在新代码单元中运行以下命令:

!pip install matplotlib

该命令会安装Matplotlib库,使你能在Python中绘制图像。

2.1.1 创建PyTorch张量

训练深度神经网络时,我们输入的是数字数组。生成模型试图生成不同内容时,这些数字的类型也不同。例如,生成图像时,输入是0到255之间的像素整数,但我们会将其转换为-1到1之间的浮点数;生成文本时,有类似词典的“词汇表”,输入是整数序列,表示词语在词典中的索引。

注意:本章及其他章节的代码均可在本书的GitHub仓库获取:github.com/markhliu/DG…

假设你想用PyTorch计算46位美国总统的平均身高。首先收集46位总统的身高(单位厘米),并存入Python列表:

heights = [189, 170, 189, 163, 183, 171, 185,
           168, 173, 183, 173, 173, 175, 178,
           183, 193, 178, 173, 174, 183, 183,
           180, 168, 180, 170, 178, 182, 180,
           183, 178, 182, 188, 175, 179, 183,
           193, 182, 183, 177, 185, 188, 188,
           182, 185, 191, 183]

这些数字按时间顺序排列:列表中第一个数字189代表第一任总统乔治·华盛顿的身高,最后一个数字183是乔·拜登的身高。

我们可以用PyTorch的tensor()方法将Python列表转换成张量:

import torch
heights_tensor = torch.tensor(heights,      # ①
                             dtype=torch.float64)  # ②

① 将Python列表转换为PyTorch张量
② 指定PyTorch张量的数据类型

我们通过tensor()方法的dtype参数指定数据类型。PyTorch张量的默认数据类型是float32(32位浮点数)。上例中我们指定为float64(64位双精度浮点数),它比float32更精确,但计算时间更长。精度和计算成本之间需要权衡,具体使用哪种数据类型取决于具体任务。

表2.1列出了不同数据类型及其对应的PyTorch张量类型,包括不同精度的整数和浮点数,整数还可分为有符号和无符号。

PyTorch张量类型tensor()中dtype参数数据类型
FloatTensortorch.float32 / torch.float32位浮点数
HalfTensortorch.float16 / torch.half16位浮点数
DoubleTensortorch.float64 / torch.double64位浮点数
CharTensortorch.int88位有符号整数
ByteTensortorch.uint88位无符号整数
ShortTensortorch.int16 / torch.short16位有符号整数
IntTensortorch.int32 / torch.int32位有符号整数
LongTensortorch.int64 / torch.long64位有符号整数

创建指定数据类型的张量有两种方式:

  1. 使用表中第一列指定的PyTorch类,如torch.IntTensor
  2. 使用torch.tensor()方法,并用dtype参数指定数据类型(取值见表第二列)。

例如,将Python列表[1, 2, 3]转换为包含32位整数的PyTorch张量,可以用以下两种方法:

t1 = torch.IntTensor([1, 2, 3])            # ①
t2 = torch.tensor([1, 2, 3], dtype=torch.int)  # ②
print(t1)
print(t2)

① 使用torch.IntTensor()指定张量类型
② 使用dtype=torch.int指定张量类型

输出结果为:

tensor([1, 2, 3], dtype=torch.int32)
tensor([1, 2, 3], dtype=torch.int32)

练习2.1
用两种不同方法将Python列表[5, 8, 10]转换为64位浮点数的PyTorch张量,参考表2.1第三行。

很多时候你需要创建元素全为0的张量。例如,在GAN中,生成假样本的标签就是全0张量(第3章会涉及)。PyTorch的zeros()方法可生成指定形状的全0张量。张量是n维数组,其形状用元组表示每维的大小。下面代码生成一个2行3列的全0张量:

tensor1 = torch.zeros(2, 3)
print(tensor1)

输出:

tensor([[0., 0., 0.],
        [0., 0., 0.]])

张量形状为(2, 3),即二维数组,第一维有2个元素,第二维有3个元素。这里未指定数据类型,默认是float32

有时你需要创建元素全为1的张量。例如,在GAN中,生成真实样本的标签就是全1张量。下面代码使用ones()方法创建一个三维全1张量:

tensor2 = torch.ones(1, 4, 5)
print(tensor2)

输出:

tensor([[[1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1.]]])

我们创建了一个形状为(1, 4, 5)的三维张量。

练习2.2
创建一个元素全为0,形状为(2, 3, 4)的三维PyTorch张量。

你也可以用NumPy数组代替Python列表创建张量:

import numpy as np

nparr = np.array(range(10))
pt_tensor = torch.tensor(nparr, dtype=torch.int)
print(pt_tensor)

输出:

tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=torch.int32)

2.1.2 PyTorch张量的索引与切片

我们使用方括号([ ])对PyTorch张量进行索引和切片,方法与Python列表类似。索引和切片允许我们操作张量中的一个或多个元素,而非全部元素。继续以46位美国总统身高为例,若想查询第三任总统托马斯·杰斐逊的身高,可以这样写:

height = heights_tensor[2]
print(height)

输出为:

tensor(189., dtype=torch.float64)

结果显示杰斐逊身高为189厘米。

我们也可以使用负索引从张量末尾开始计数。例如,想查询倒数第二位总统唐纳德·特朗普的身高,使用索引-2:

height = heights_tensor[-2]
print(height)

输出为:

tensor(191., dtype=torch.float64)

显示特朗普身高191厘米。

如果想知道张量heights_tensor中最近五位总统的身高,可以获取张量的切片:

five_heights = heights_tensor[-5:]
print(five_heights)

冒号(:)分隔起始和结束索引,未指定起始默认为0,未指定结束则包含最后一个元素。负索引表示从后往前计数。输出为:

tensor([188., 182., 185., 191., 183.], dtype=torch.float64)

结果显示最近五位总统(克林顿、布什、奥巴马、特朗普、拜登)的身高分别为188、182、185、191和183厘米。

练习2.3
使用切片获取张量heights_tensor中前五位美国总统的身高。

2.1.3 PyTorch张量的形状

PyTorch张量有一个shape属性,表示张量的维度信息。了解张量形状很重要,因为形状不匹配会导致操作时出错。例如,查询heights_tensor的形状:

print(heights_tensor.shape)

输出为:

torch.Size([46])

说明heights_tensor是一个包含46个元素的一维张量。

你还可以修改PyTorch张量的形状。首先,我们将身高从厘米转换为英尺。1英尺约等于30.48厘米,可以通过除以30.48实现:

heights_in_feet = heights_tensor / 30.48
print(heights_in_feet)

输出示例(部分省略,完整代码见书籍GitHub仓库):

tensor([6.2008, 5.5774, 6.2008, 5.3478, 6.0039, 5.6102, 6.0696, …
        6.0039], dtype=torch.float64)

新张量heights_in_feet存储了以英尺为单位的身高。例如,最后一个值表示乔·拜登身高约6.0039英尺。

我们可以用PyTorch的cat()方法将两个张量拼接起来:

heights_2_measures = torch.cat([heights_tensor, heights_in_feet], dim=0)
print(heights_2_measures.shape)

dim参数指定拼接的维度,这里dim=0表示沿第一个维度拼接。输出为:

torch.Size([92])

得到的是一维张量,包含92个元素,前46个为厘米,后46个为英尺。我们需要将其重塑为两行46列,以便第一行表示厘米,第二行表示英尺:

heights_reshaped = heights_2_measures.reshape(2, 46)

新张量heights_reshaped是二维张量,形状为(2, 46)。多维张量同样可以用方括号进行索引和切片。例如,打印特朗普的英尺身高:

print(heights_reshaped[1, -2])

输出为:

tensor(6.2664, dtype=torch.float64)

表达式heights_reshaped[1, -2]表示取第二行倒数第二列的值,即特朗普的英尺身高6.2664。

提示:索引的数量与张量的维度相同。因为heights_tensor是一维张量,定位元素只需一个索引;而heights_reshaped是二维张量,需两个索引定位元素。

练习2.4
使用索引获取张量heights_reshaped中乔·拜登的厘米身高。

2.1.4 PyTorch张量的数学运算

我们可以通过多种方法对PyTorch张量进行数学运算,如mean()(均值)、median()(中位数)、sum()(求和)、max()(最大值)等。例如,计算46位总统身高的中位数(单位厘米),可以这样写:

print(torch.median(heights_reshaped[0, :]))

代码中heights_reshaped[0, :]表示取张量heights_reshaped的第一行所有列,上述代码返回第一行的中位数,输出为:

tensor(182., dtype=torch.float64)

这意味着美国总统的身高中位数是182厘米。

若想计算两行的平均身高,可以在mean()方法中使用参数dim=1

print(torch.mean(heights_reshaped, dim=1))

参数dim=1表示对第1维(列)求平均,结果是在第0维(行)方向上的均值。输出为:

tensor([180.0652, 5.9077], dtype=torch.float64)

结果显示两行的平均值分别为180.0652厘米和5.9077英尺。

要找出身高最高的总统,可以使用:

values, indices = torch.max(heights_reshaped, dim=1)
print(values)
print(indices)

输出为:

tensor([193.0000, 6.3320], dtype=torch.float64)
tensor([15, 15])

torch.max()方法返回两个张量:values是最高身高(厘米和英尺),indices是对应最高身高总统在张量中的索引。结果显示第16任总统林肯(索引15)身高最高,193厘米(6.332英尺)。

练习2.5
使用torch.min()方法找出身高最低的美国总统的索引和值。

2.2 使用PyTorch进行端到端深度学习项目

在接下来的几个小节中,你将通过一个示例深度学习项目,学习如何将灰度服装图片分类为10种类型之一。本节将首先对所涉及的步骤进行整体概述,然后讨论如何获取该项目的训练数据以及如何进行数据预处理。

2.2.1 PyTorch中的深度学习:整体概览

本项目的任务是使用PyTorch创建并训练一个深度神经网络,对灰度服装图片进行分类。图2.1展示了该项目涉及的各个步骤流程图。

image.png

首先,我们将获取一组灰度服装图片数据集,如图2.1左侧所示。图片以原始像素形式存在,我们将把它们转换为PyTorch张量,数据类型为浮点数(步骤1)。每张图片都有对应的标签。

接着,我们将在PyTorch中创建一个深度神经网络,如图2.1中间所示。本书中的部分神经网络采用卷积神经网络(CNN),但针对这个简单的分类问题,我们暂时只使用全连接层。

我们将选择一个适用于多分类任务的损失函数,交叉熵损失是该任务中常用的损失函数。交叉熵损失用于衡量预测的概率分布与标签的真实分布之间的差异。训练时,我们使用Adam优化器(一种梯度下降算法的变体)更新网络权重,并将学习率设为0.001。学习率控制训练过程中模型权重根据损失梯度调整的幅度。

机器学习中的优化器
优化器是根据梯度信息调整模型参数以最小化损失函数的算法。随机梯度下降(SGD)是最基础的优化器,它基于损失梯度进行简单更新。Adam是最流行的优化器,以其高效和开箱即用的性能著称,结合了自适应梯度算法(AdaGrad)和均方根传播(RMSProp)的优点。尽管各优化器方法不同,但都旨在通过迭代调整参数来最小化损失函数,并各自形成独特的优化路径。

我们将把训练数据分为训练集和验证集。在机器学习中,验证集用于对模型进行无偏评估,并选择最佳超参数,如学习率、训练轮数(epoch)等。验证集还可帮助避免过拟合,即模型在训练集表现良好,但在未见数据上效果差。一个epoch表示所有训练数据被用来训练模型一次且仅一次。

训练过程中,你将遍历训练数据。在前向传播时,将图片输入网络获得预测结果(步骤2),通过将预测标签与真实标签比较计算损失(步骤3,见图2.1右侧)。然后反向传播梯度以更新权重,这个过程即是模型学习的核心(步骤4,见图2.1底部)。

你将利用验证集来判断何时停止训练。通过计算验证集的损失,如果在固定训练轮数后模型性能不再提升,则认为训练结束。随后,我们会在测试集上评估训练好的模型,检验其对不同标签图片的分类性能。

现在你已对PyTorch深度学习的整体流程有了宏观认识,接下来让我们开始这个端到端的项目吧!

2.2.2 数据预处理

本项目中我们将使用Fashion Modified National Institute of Standards and Technology(Fashion MNIST)数据集。在过程中,你将学习如何使用Torchvision库中的datasets和transforms包,以及PyTorch中的Dataloader包,这些工具将在本书后续章节广泛应用,用于数据预处理。Torchvision库提供了图像处理工具,包括常用数据集、模型架构和深度学习常用图像变换。

首先,我们导入所需库,并实例化transforms包中的Compose()类,将原始图片转换为PyTorch张量。

代码示例(清单2.2):

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as T

torch.manual_seed(42)
transform = T.Compose([           # ①
    T.ToTensor(),                 # ②
    T.Normalize([0.5], [0.5])    # ③
])

① 将多个变换操作组合起来
② 将图像像素转换为PyTorch张量
③ 对数据归一化到[-1, 1]区间

使用manual_seed()方法固定随机种子,保证结果可复现。Torchvision的transforms包支持创建一系列变换来预处理图像。ToTensor()将图像数据(PIL格式或NumPy数组)转换为PyTorch张量。图像原始像素值为0-255的整数,ToTensor()转换后为0.0-1.0的浮点数。

Normalize()对张量图像进行归一化,参数为各通道的均值和标准差。Fashion MNIST为灰度图像,只有一个通道。后续章节会处理三通道彩色图像(红、绿、蓝)。此处Normalize([0.5], [0.5])表示对数据减去0.5后再除以0.5,结果映射到[-1, 1]区间。归一化使梯度下降过程更加高效,帮助训练更快收敛,本书中会多次使用。

注意:清单2.2代码只定义了数据转换过程,实际转换将在下一代码单元执行。

接下来,我们使用Torchvision的datasets包下载数据集到本地文件夹,并应用转换:

train_set = torchvision.datasets.FashionMNIST(    # ①
    root=".",                                      # ②
    train=True,                                    # ③
    download=True,                                 # ④
    transform=transform)                           # ⑤

test_set = torchvision.datasets.FashionMNIST(
    root=".",
    train=False,
    download=True,
    transform=transform)

① 指定要下载的数据集
② 数据存储路径
③ 训练集或测试集
④ 是否下载数据到本地
⑤ 应用数据转换

你可以打印训练集中第一个样本:

print(train_set[0])

第一个样本由一个含784个值的张量和标签9组成。784对应28×28的灰度图像像素数,标签9表示这是一只踝靴。你可能会问,如何知道标签9对应踝靴?数据集共有10类服装,标签编号0到9。你可以在线查找这些类别的文本标签(例如我这里查到的:github.com/pranay414/F…)。类别对应的文本标签列表text_labels如下:

text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
               'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']

例如标签0对应“t-shirt”。

我们可以绘制数据,直观展示服装图像。

代码示例(清单2.3):

!pip install matplotlib
import matplotlib.pyplot as plt

plt.figure(dpi=300, figsize=(8, 4))
for i in range(24):
    ax = plt.subplot(3, 8, i + 1)               # ①
    img = train_set[i][0]                        # ②
    img = img / 2 + 0.5                          # ③
    img = img.reshape(28, 28)                    # ④
    plt.imshow(img, cmap="binary")
    plt.axis('off')
    plt.title(text_labels[train_set[i][1]], fontsize=8)  # ⑤
plt.show()

① 指定图像放置位置
② 取训练集中的第i张图片
③ 将像素值从[-1,1]映射回[0,1]
④ 重塑图片为28×28
⑤ 添加文本标签

图2.2展示了24个服装图像样本,如外套、套头衫、凉鞋等。

image.png

在接下来的两个小节中,你将学习如何用PyTorch构建深度神经网络,完成二分类和多分类任务。

2.3 二分类

本节中,我们将先创建训练数据的批次,然后构建PyTorch深度神经网络模型,利用数据训练该模型。训练完成后,我们将用训练好的模型进行预测,并测试预测的准确度。二分类和多分类任务的步骤类似,只有少数细节不同,稍后我会重点说明。

2.3.1 创建批次

我们将构建只包含两类服装——T恤和踝靴的训练集和测试集。(在本章后面讨论多分类时,你还会学习如何创建验证集用来判断训练何时停止。)以下代码实现了上述目的:

binary_train_set = [x for x in train_set if x[1] in [0, 9]]
binary_test_set = [x for x in test_set if x[1] in [0, 9]]

我们只保留标签为0和9的样本,构建一个平衡的二分类问题训练集。接下来,我们为训练深度神经网络创建数据批次。

清单2.4 创建训练和测试批次

batch_size = 64
binary_train_loader = torch.utils.data.DataLoader(
    binary_train_set,      # ①
    batch_size=batch_size, # ②
    shuffle=True           # ③
)
binary_test_loader = torch.utils.data.DataLoader(
    binary_test_set,       # ④
    batch_size=batch_size,
    shuffle=True
)

① 为二分类训练集创建批次
② 每个批次的样本数量
③ 批次生成时打乱样本顺序
④ 为二分类测试集创建批次

PyTorch的DataLoader类帮助我们创建按批次迭代的数据。这里批次大小设置为64。清单2.4中创建了两个数据加载器,分别对应训练集和测试集。我们在生成批次时打乱数据,以避免原始数据集样本间的相关性,使训练更稳定,确保不同类别均匀分布。

2.3.2 构建并训练二分类模型
我们先构建一个二分类模型,然后用T恤和踝靴的图片训练它。训练完成后,测试模型能否正确区分这两类服装。下面使用PyTorch的nn.Sequential类创建该神经网络(后续章节你还会学习如何用nn.Module类创建神经网络)。

清单2.5 创建二分类模型

import torch.nn as nn

device = "cuda" if torch.cuda.is_available() else "cpu"  # ①

binary_model = nn.Sequential(                            # ②
    nn.Linear(28*28, 256),                              # ③
    nn.ReLU(),                                          # ④
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Linear(128, 32),
    nn.ReLU(),
    nn.Linear(32, 1),
    nn.Dropout(p=0.25),
    nn.Sigmoid()
).to(device)                                            # ⑤

① 自动检测是否有支持CUDA的GPU
② 创建PyTorch顺序神经网络
③ 线性层输入和输出神经元数量
④ 对层输出应用ReLU激活函数
⑤ 应用Sigmoid激活,将模型加载到GPU(若可用)

PyTorch中的Linear()类实现对输入数据的线性变换,即构建神经网络中的全连接层。输入维度是784,因为我们会将28×28的二维图像展平成一维向量(28×28=784)。展平是因为全连接层只接受一维输入。后续章节中,当使用卷积层时,不必再进行展平。该网络包含三个隐藏层,分别有256、128和32个神经元。这些数字较为随意,改成300、200、50等不会影响训练过程。我们对三个隐藏层分别应用ReLU激活函数,它根据加权和决定神经元是否激活,引入非线性,帮助网络学习输入输出间的非线性关系。ReLU是最常用的激活函数,除少数情况外都是首选,后续章节会介绍其他激活函数。

模型最后一层输出一个值,使用Sigmoid函数将其压缩到[0,1]区间,表示该对象为踝靴的概率。其余概率对应该对象为T恤。

我们设置学习率,定义优化器和损失函数:

lr = 0.001
optimizer = torch.optim.Adam(binary_model.parameters(), lr=lr)
loss_fn = nn.BCELoss()

学习率设为0.001。学习率的最佳值需要经验判断,也可通过验证集上的超参数调优确定。PyTorch中大多数优化器默认学习率为0.001。Adam优化器是梯度下降的变体,由Diederik Kingma和Jimmy Ba于2014年提出。传统梯度下降只考虑当前迭代梯度,Adam则结合了过去多次迭代的梯度信息。

我们使用nn.BCELoss(),即二分类交叉熵损失函数。损失函数衡量模型性能,训练过程就是调整参数使损失函数最小化。二分类交叉熵广泛用于二分类问题,衡量输出概率与真实标签的偏差,预测越偏离真实标签,损失越大。

下面是训练代码示例:
清单2.6 训练二分类模型

for i in range(50):                                # ①
    tloss = 0
    for imgs, labels in binary_train_loader:       # ②
        imgs = imgs.reshape(-1, 28*28)             # ③
        imgs = imgs.to(device)
        labels = torch.FloatTensor([x if x == 0 else 1 for x in labels])  # ④
        labels = labels.reshape(-1, 1).to(device)
        preds = binary_model(imgs)
        loss = loss_fn(preds, labels)               # ⑤
        optimizer.zero_grad()
        loss.backward()                             # ⑥
        optimizer.step()
        tloss += loss.detach()
    tloss = tloss / len(binary_train_loader)
    print(f"at epoch {i}, loss is {tloss}")

① 训练50个epoch
② 遍历所有批次
③ 展平图像后送入GPU
④ 标签转换为0和1
⑤ 计算损失
⑥ 反向传播

在PyTorch中,loss.backward()计算损失对各模型参数的梯度,实现反向传播;optimizer.step()根据梯度更新模型参数,减小损失。为了简单起见,我们训练50个epoch(一个epoch是用所有训练数据训练模型一次)。下一节你将学习使用验证集和早停法确定训练轮数。

在二分类任务中,目标标签用0和1表示。因为我们只保留了T恤和踝靴,对应原始标签0和9,故在清单2.6中将其转换为0和1。

如果使用GPU训练,该训练过程耗时几分钟;用CPU训练则更慢,但通常不超过一小时。

2.3.3 测试二分类模型

训练好的二分类模型输出的是一个介于0到1之间的数值。我们将使用torch.where()方法将预测结果转换为0和1:如果预测概率小于0.5,则标签为0;否则标签为1。随后,将这些预测标签与真实标签进行比较,以计算预测准确率。下面的代码示例演示了如何用训练好的模型对测试数据集进行预测。

清单2.7 计算预测准确率

import numpy as np
results = []
for imgs, labels in binary_test_loader:                # ①
    imgs = imgs.reshape(-1, 28*28).to(device)
    labels = (labels / 9).reshape(-1, 1).to(device)
    preds = binary_model(imgs)
    pred10 = torch.where(preds > 0.5, 1, 0)            # ②
    correct = (pred10 == labels)                        # ③
    results.append(correct.detach().cpu().numpy().mean())  # ④
accuracy = np.array(results).mean()                     # ⑤
print(f"the accuracy of the predictions is {accuracy}")

① 遍历测试集中的所有批次
② 使用训练好的模型进行预测
③ 将预测结果与真实标签进行比较
④ 计算该批次的准确率
⑤ 计算整个测试集的平均准确率

我们遍历测试集中的所有数据批次。模型输出该图片是踝靴的概率。利用torch.where()方法,根据0.5的阈值将概率转换成0或1。转换后,预测结果为0(T恤)或1(踝靴)。随后,将预测结果与真实标签进行比较,统计预测正确的次数。结果显示,模型在测试集上的预测准确率为87.84%。

2.4 多类别分类

本节中,我们将使用PyTorch构建一个深度神经网络,将服装图像分类为10个类别之一。随后用Fashion MNIST数据集训练模型,最后使用训练好的模型进行预测并评估准确度。我们首先创建验证集并定义早停类,以便确定何时停止训练。

2.4.1 验证集与早停

构建和训练深度神经网络时,有许多超参数可调(如学习率和训练轮数)。这些超参数影响模型性能。为寻找最佳超参数,我们可以创建验证集,用不同超参数训练模型并评估性能。
举例来说,在多类别分类中,我们创建验证集来确定训练的最佳轮数。使用验证集而非训练集来判断,是为了避免过拟合,即模型在训练集表现优异但在未见数据上效果差。

我们将6万条训练数据拆分为训练集和验证集:

train_set, val_set = torch.utils.data.random_split(train_set, [50000, 10000])

原训练集被拆成新训练集(5万条)和验证集(1万条)。

利用PyTorch utils包中的DataLoader类,将训练集、验证集和测试集分别转为批量数据迭代器:

train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size, shuffle=True)

接着定义一个EarlyStop类,用于判断训练何时停止。

清单2.8 EarlyStop类:

class EarlyStop:
    def __init__(self, patience=10):         # ①
        self.patience = patience
        self.steps = 0
        self.min_loss = float('inf')
    def stop(self, val_loss):                # ②
        if val_loss < self.min_loss:         # ③
            self.min_loss = val_loss
            self.steps = 0
        elif val_loss >= self.min_loss:      # ④
            self.steps += 1
        if self.steps >= self.patience:
            return True
        else:
            return False

stopper = EarlyStop()

① 设置默认耐心值(patience)为10
② 定义stop()方法
③ 如果验证损失出现新低,更新最小损失值
④ 统计自上次最低损失以来经过的轮数

该类用于监测验证集损失在最近patience=10轮内是否未进一步降低。可在实例化时传入不同的patience值。stop()方法追踪最小损失和距离最小损失后的轮数,若超过patience则返回True表示应停止训练。

2.4.2 构建并训练多类别分类模型
Fashion MNIST包含10类服装,我们构建多类别分类模型进行分类。你将学习如何创建、训练该模型,并用训练好的模型进行预测和准确度评估。

清单2.9 创建多类别分类模型:

model = nn.Sequential(
    nn.Linear(28*28, 256),
    nn.ReLU(),
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Linear(128, 64),
    nn.ReLU(),
    nn.Linear(64, 10)             # ①
).to(device)                     # ②

① 输出层有10个神经元
② 不对输出层使用softmax激活

相比上节的二分类模型,我们做了几处调整:输出层神经元变为10个,对应10类服装;倒数第二层神经元数量从32增至64,遵循逐层神经元数目逐渐增减的经验法则。64只是示例数值,换成100等也不会有显著差异。

损失函数采用PyTorch的nn.CrossEntropyLoss(),它结合了nn.LogSoftmax()nn.NLLLoss()。详见文档:mng.bz/pxd2 。该损失函数内部对模型输出先做softmax,再计算交叉熵,因此模型中不需显式使用softmax。若用nn.LogSoftmax()配合nn.NLLLoss(),结果一致。

softmax激活将10个输出值映射到[0,1]区间且和为1,可视为对应10类服装的概率。二分类中则用sigmoid激活。

使用与二分类相同的学习率和优化器:

lr = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
loss_fn = nn.CrossEntropyLoss()

定义训练一个epoch的函数:

def train_epoch():
    tloss = 0
    for n, (imgs, labels) in enumerate(train_loader):
        imgs = imgs.reshape(-1, 28*28).to(device)
        labels = labels.reshape(-1,).to(device)
        preds = model(imgs)
        loss = loss_fn(preds, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        tloss += loss.detach()
    return tloss / n

函数实现了对模型一次完整的训练,代码与二分类类似,只是标签是0到9的多类别。

定义验证epoch函数:

def val_epoch():
    vloss = 0
    for n, (imgs, labels) in enumerate(val_loader):
        imgs = imgs.reshape(-1, 28*28).to(device)
        labels = labels.reshape(-1,).to(device)
        preds = model(imgs)
        loss = loss_fn(preds, labels)
        vloss += loss.detach()
    return vloss / n

该函数计算模型在验证集上的平均损失。

训练多类别分类器:

for i in range(1, 101):
    tloss = train_epoch()
    vloss = val_epoch()
    print(f"at epoch {i}, tloss is {tloss}, vloss is {vloss}")
    if stopper.stop(vloss) == True:
        break

最多训练100个epoch。每轮先用训练集训练模型,再计算验证集平均损失。利用EarlyStop类监控验证损失变化,如10轮内无改进则停止训练。实际训练19轮后终止。

GPU训练约5分钟,时间比二分类训练长,因为训练集样本更多(10类服装vs. 2类)。

模型输出一个10维向量。用torch.argmax()取概率最高的位置作为预测标签,再与真实标签对比。

演示预测前5张测试集图片的代码:
清单2.10 在5张图片上测试训练模型

plt.figure(dpi=300, figsize=(5, 1))
for i in range(5):
    ax = plt.subplot(1, 5, i + 1)
    img = test_set[i][0]
    label = test_set[i][1]
    img = img / 2 + 0.5
    img = img.reshape(28, 28)
    plt.imshow(img, cmap="binary")
    plt.axis('off')
    plt.title(text_labels[label] + f"; {label}", fontsize=8)
plt.show()

for i in range(5):
    img, label = test_set[i]
    img = img.reshape(-1, 28*28).to(device)
    pred = model(img)
    index_pred = torch.argmax(pred, dim=1)
    idx = index_pred.item()
    print(f"the label is {label}; the prediction is {idx}")

① 绘制测试集前五张图片及其标签
② 获取测试集第i张图片和标签
③ 用训练模型预测
④ 用torch.argmax()获取预测标签
⑤ 打印真实标签与预测标签

以上代码执行后,应能看到图2.3所示的图片和对应的预测结果。

image.png

图2.3显示测试集中前五个服装项依次为踝靴、套头衫、裤子、裤子和衬衫,其对应的数字标签分别是9、2、1、1和6。

运行清单2.10中的代码后,输出如下:

the label is 9; the prediction is 9
the label is 2; the prediction is 2
the label is 1; the prediction is 1
the label is 1; the prediction is 1
the label is 6; the prediction is 6

上述输出表明模型对这五个服装项均做出了正确的预测。

在PyTorch中固定随机状态

torch.manual_seed()方法用来固定随机状态,以保证多次运行程序时结果一致。但即便使用相同随机种子,不同硬件设备和PyTorch不同版本对浮点运算的处理略有差异,可能导致结果存在细微差别。详见 mng.bz/RNva。总体差异通常很小,无需担忧。

接下来,我们计算模型在整个测试集上的预测准确率。

清单2.11 测试训练好的多类别分类模型:

results = []

for imgs, labels in test_loader:                    # ①
    imgs = imgs.reshape(-1, 28*28).to(device)
    labels = labels.reshape(-1,).to(device)
    preds = model(imgs)                              # ②
    pred10 = torch.argmax(preds, dim=1)              # ③
    correct = (pred10 == labels)                     # ④
    results.append(correct.detach().cpu().numpy().mean())

accuracy = np.array(results).mean()                  # ⑤
print(f"the accuracy of the predictions is {accuracy}")

① 遍历测试集所有批次
② 使用训练好的模型进行预测
③ 将概率转为预测标签
④ 将预测标签与真实标签比较
⑤ 计算测试集准确率

输出结果为:

the accuracy of the predictions is 0.8819665605095541

我们遍历测试集中的所有服装样本,使用训练模型进行预测,并与真实标签对比,测试集上的准确率约为88%。考虑到随机猜测的准确率只有约10%,88%的准确率相当高。这表明我们已经成功构建并训练了两个高效的PyTorch深度学习模型!在本书后续章节中你会频繁使用这些技能。例如,在第3章中你将构建的判别器网络,本质上就是一个类似于本章创建的二分类模型。

总结

在PyTorch中,我们使用张量来存储各种形式的输入数据,以便将它们输入深度学习模型。

你可以对PyTorch张量进行索引和切片、改变形状以及执行数学运算。

深度学习是一种机器学习方法,利用深度人工神经网络学习输入与输出数据之间的关系。

ReLU激活函数根据加权和决定神经元是否激活,为神经元输出引入非线性。

损失函数用于衡量机器学习模型的性能,模型训练的过程就是不断调整参数以最小化损失函数。

二分类是一种将样本划分为两类的机器学习模型。

多类别分类是一种将样本划分为多个类别的机器学习模型。