本章内容包括
- 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参数 | 数据类型 |
---|---|---|
FloatTensor | torch.float32 / torch.float | 32位浮点数 |
HalfTensor | torch.float16 / torch.half | 16位浮点数 |
DoubleTensor | torch.float64 / torch.double | 64位浮点数 |
CharTensor | torch.int8 | 8位有符号整数 |
ByteTensor | torch.uint8 | 8位无符号整数 |
ShortTensor | torch.int16 / torch.short | 16位有符号整数 |
IntTensor | torch.int32 / torch.int | 32位有符号整数 |
LongTensor | torch.int64 / torch.long | 64位有符号整数 |
创建指定数据类型的张量有两种方式:
- 使用表中第一列指定的PyTorch类,如
torch.IntTensor
。 - 使用
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展示了该项目涉及的各个步骤流程图。
首先,我们将获取一组灰度服装图片数据集,如图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个服装图像样本,如外套、套头衫、凉鞋等。
在接下来的两个小节中,你将学习如何用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所示的图片和对应的预测结果。
图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激活函数根据加权和决定神经元是否激活,为神经元输出引入非线性。
损失函数用于衡量机器学习模型的性能,模型训练的过程就是不断调整参数以最小化损失函数。
二分类是一种将样本划分为两类的机器学习模型。
多类别分类是一种将样本划分为多个类别的机器学习模型。