PyTorch-初学者系列教程笔记-全-
PyTorch 初学者系列教程笔记(全)
001:PyTorch简介

在本节课中,我们将要学习PyTorch的基础知识,包括其核心概念、相关工具和库。本概述假设您是初次使用PyTorch进行机器学习。


在本视频中,我们将涵盖以下内容:
- PyTorch相关项目概述。
- 张量,这是PyTorch的核心数据抽象。
- 自动求导,它支持即时执行模式计算,使模型的快速迭代成为可能。
- 使用PyTorch模块构建模型。
- 如何高效加载数据以训练模型。
- 演示一个基础的训练循环。
- 最后,讨论使用TorchScript进行部署。



在开始之前,您需要安装PyTorch和torchvision,以便能够跟随演示和练习。如果您尚未安装最新版本的PyTorch,请访问PyTorch.org。
首页有一个安装向导。这里有两个重要事项需要注意。首先,CUDA驱动程序不适用于Mac。因此,在Mac上无法通过PyTorch获得GPU加速。其次,如果您在配备一个或多个NVIDIA CUDA兼容GPU的Linux或Windows机器上工作,请确保您安装的CUDA工具包版本与机器上的CUDA驱动程序匹配。

那么,什么是PyTorch?



PyTorch.org告诉我们,PyTorch是一个开源机器学习框架,它加速了从研究原型到生产部署的路径。
我们来详细解读一下。首先,PyTorch是用于机器学习的软件。它包含一个完整的工具包,用于构建和部署机器学习应用,包括深度学习原语,如神经网络层类型、激活函数和基于梯度的优化器。它在NVIDIA GPU上具有硬件加速功能,并拥有用于计算机视觉、文本和自然语言以及音频应用的相关库。Torchvision是PyTorch的计算机视觉库,还包括预训练模型和打包的数据集,您可以用它们来训练自己的模型。
PyTorch旨在实现机器学习模型和应用的快速迭代。您可以使用常规的、符合Python习惯的代码进行工作。无需学习新的领域特定语言来构建计算图。通过自动求导,模型的反向传播只需一个函数调用即可完成,并且无论计算在代码中经过哪条路径,都能正确执行,为您在模型设计中提供了无与伦比的灵活性。

PyTorch拥有适用于企业级规模工作的工具,例如TorchScript,这是一种从PyTorch代码创建可序列化和可优化模型的方法;TorchServe,PyTorch的模型服务解决方案;以及用于量化模型以提升性能的多种选项。

最后,PyTorch是免费的开源软件,可以自由使用,并开放社区贡献。其开源性质也培育了丰富的社区项目生态系统,支持从随机过程到基于图的神经网络等各种用例。

PyTorch社区庞大且不断增长,拥有来自全球超过1200名项目贡献者,研究论文引用量年同比增长超过50%。

PyTorch在顶级公司中得到使用,并为诸如AllenNLP(用于自然语言深度学习的开源研究库)、FastAI(使用现代最佳实践简化快速准确神经网络训练)、Classy Vision(用于图像和视频分类的端到端框架)和Captum(帮助您理解和解释模型行为的开源可扩展库)等项目提供了基础。
现在您已经了解了PyTorch,让我们深入其内部。张量将是您在PyTorch中所做一切的核心。您的模型、输入、输出和学习权重都以张量的形式存在。

如果“张量”不是您通常的数学词汇,只需知道在此上下文中,我们谈论的是一个多维数组,但附带了许多额外的功能。
PyTorch张量捆绑了超过300种可以在其上执行的数学和逻辑运算。虽然您通过Python API访问张量,但计算实际上发生在为CPU和GPU优化的已编译C++代码中。
让我们看看PyTorch中一些典型的张量操作。

首先我们需要做的是通过 import torch 导入PyTorch。
然后我们将创建第一个张量。这里我将创建一个具有五行三列的二维张量,并用零填充。我将查询这些零的数据类型。
您可以看到我得到了请求的15个零的矩阵,数据是32位浮点数。默认情况下,PyTorch将所有张量创建为32位浮点数。
如果您想要整数怎么办?您总是可以覆盖默认值。在下一个单元格中,我创建了一个充满1的张量。我请求它们是16位整数,并注意当我打印它时,PyTorch告诉我这些是16位整数,因为它不是默认值,这可能不是我所期望的。
通常随机初始化学习权重,通常使用特定的随机数生成器种子,以便在后续运行中重现结果。这里我们演示一下。用特定数字为PyTorch随机数生成器设定种子。生成一个随机张量。生成第二个随机张量,我们期望它与第一个不同。用相同的输入重新设定随机数生成器的种子。最后,创建另一个随机张量,我们期望它与第一个匹配,因为它是设定种子后创建的第一个张量。果然,我们得到了预期的结果。第一个张量和第三个张量确实匹配,而第二个则不匹配。
PyTorch张量的算术运算是直观的。形状相似的张量可以相加、相乘等。标量和张量之间的运算将分布到张量的所有元素上。让我们看几个例子。首先,我将创建一个充满1的张量。然后我将创建另一个充满1的张量,但我会将其乘以标量2。结果将是所有的1都变成2。乘法运算分布到张量的每个元素上。然后我将两个张量相加。我可以这样做是因为它们形状相同。运算在它们之间逐元素进行。现在我们得到一个充满3的张量。当我查询该张量的形状时,它与加法运算的两个输入张量的形状相同。最后,我创建两个不同形状的随机张量并尝试相加。我得到一个运行时错误,因为无法在两个不同形状的张量之间进行清晰的逐元素算术运算。
以下是PyTorch张量上可用的数学运算的一小部分示例。我将创建一个随机张量并调整其值在-1到1之间。我可以取其绝对值,看到所有值都变为正数。我可以取其反正弦,因为值在-1到1之间,并得到一个角度。我可以进行线性代数运算,如计算行列式或进行奇异值分解。还有统计和聚合运算,如均值、标准差、最小值和最大值等。
关于PyTorch张量的强大功能还有很多需要了解,包括如何为GPU上的并行计算设置它们。我们将在另一个视频中深入探讨。
作为对PyTorch自动微分引擎自动求导的介绍,让我们考虑一次简单训练过程的基本机制。对于这个例子,我们将使用一个简单的循环神经网络。我们首先有四个张量:输入X,RNN的隐藏状态H(赋予其记忆),以及两组学习权重,每组分别用于输入和隐藏状态。接下来,我们将权重乘以它们各自的张量。这里的“@”代表矩阵乘法。之后,我们将两个矩阵乘法的输出相加。并将结果通过一个激活函数(这里是双曲正切)传递。最后,我们计算此输出的损失。损失是正确输出与模型实际预测之间的差异。
因此,我们获取了一个训练输入,通过模型运行它,得到一个输出,并确定了损失。这是训练循环中的关键点,我们需要计算该损失相对于模型每个参数的导数,并使用学习权重的梯度来决定如何调整这些权重以减少损失。即使对于这样的小模型,也有很多参数和导数需要计算。
但好消息是,您可以用一行代码完成。此计算生成的每个张量都知道它是如何产生的。例如,h_to_h 携带元数据,表明它来自 W_h 和 H 的矩阵乘法。这种历史跟踪一直延续到图的其余部分。这种历史跟踪使得 backward 方法能够快速计算模型学习所需的梯度。这种历史跟踪是模型灵活性和快速迭代的促成因素之一。即使在具有决策分支和循环的复杂模型中,计算历史也会跟踪特定输入在模型中经过的特定路径,并正确计算反向导数。
在后续视频中,我们将向您展示如何使用自动求导做更多技巧,例如使用自动求导分析器、计算二阶导数,以及如何在不需要时关闭自动求导。
到目前为止,我们已经讨论了张量和自动求导,以及它们与PyTorch模型交互的一些方式。但是,模型在代码中是什么样子的呢?让我们构建并运行一个简单的模型来感受一下。
首先,我们将导入PyTorch。我们还将导入 torch.nn,它包含我们将组合到模型中的神经网络层,以及模型本身的父类。我们将导入 torch.nn.functional 以获取激活函数和最大池化函数,我们将用它们来连接层。
这里我们有一个LeNet-5的示意图。它是最早的卷积神经网络之一,也是深度学习爆炸式发展的推动力之一。它被构建用于读取手写数字的小图像,即MNIST数据集,并正确分类图像中表示的是哪个数字。
以下是其工作原理的简化版本。层C1是一个卷积层,意味着它扫描输入图像以寻找在训练期间学到的特征。它输出一个激活图,显示它在此图像中看到每个学习特征的位置。该激活图在层S2中进行下采样。层C3是另一个卷积层,这次扫描C1的激活图以寻找特征组合。它也输出一个激活图,描述这些特征组合的空间位置,该图在层S4中进行下采样。最后,末端的全连接层F5、F6和输出是一个分类器,它获取最终的激活图并将其分类到代表10个数字的10个类别中。
那么,我们如何在代码中表达这个简单的神经网络呢?查看这段代码,您应该能够发现与上图的一些结构相似之处。这展示了一个典型PyTorch模型的结构。它继承自 torch.nn.Module。模块可以嵌套。事实上,这里的 Conv2d 和 Linear 层也是 torch.nn.Module 的子类。
每个模型都会有一个 __init__ 方法,在其中构建将组合到其计算图中的层,并加载它可能需要的任何数据工件。例如,一个NLP模型可能会加载一个词汇表。模型会有一个 forward 函数。这是实际计算发生的地方,输入通过各种函数通过网络层传递以生成输出,即预测。除此之外,您可以像构建任何其他Python类一样构建您的模型类,添加支持模型计算所需的任何属性和方法。
让我们实例化这个模型。并通过它运行一个输入。
这里发生了几件重要的事情。我们正在创建一个LeNet的实例。我们打印该对象。torch.nn.Module 的子类将报告它创建的层及其形状和参数。如果您想了解模型处理的要点,这可以提供方便的概述。
在那下面,我们创建了一个虚拟输入,代表一个具有一个颜色通道的32x32图像。通常,您会加载一个图像块并将其转换为这种形状的张量。您可能已经注意到我们的张量有一个额外的维度。这是批次维度。PyTorch模型假设它们处理的是批量数据。例如,一批16个我们的图像块将具有形状 [16, 1, 32, 32]。由于我们只使用一张图像,我们创建了一个批次大小为1的张量,形状为 [1, 1, 32, 32]。
我们通过像调用函数一样调用模型来请求推理:net(input)。此调用的输出代表模型对输入代表特定数字的置信度。由于这个模型实例尚未训练,我们不应期望在输出中看到任何信号。查看输出的形状,我们可以看到它也有一个批次维度,其大小应始终与输入批次维度匹配。如果我们传入一个包含16个实例的输入批次,输出将具有形状 [16, 10]。
您已经看到了如何构建模型,以及如何给它一批输入并检查输出。然而,模型并没有做太多事情,因为它尚未经过训练。为此,我们需要给它提供大量数据。为了训练我们的模型,我们需要一种批量提供数据的方法。这就是PyTorch的 Dataset 和 DataLoader 类发挥作用的地方。让我们看看它们的实际应用。
这里我声明了 %matplotlib inline,因为我们将在笔记本中渲染一些图像。我导入了PyTorch。我还导入了 torchvision 和 torchvision.transforms。这些将为我们提供数据集以及我们需要应用于图像的一些转换,以使它们能够被PyTorch模型消化。
我们需要做的第一件事是将输入的图像转换为PyTorch张量。这里我们为输入指定了两个转换。transforms.ToTensor() 获取由PIL库加载的图像,并将它们转换为PyTorch张量。transforms.Normalize() 调整张量的值,使其平均值为0,标准差为0.5。大多数激活函数在0点附近具有最强的梯度,因此将数据集中在那里可以加速学习。还有许多其他可用的转换,包括裁剪、居中、旋转、反射以及您可能对图像进行的大多数其他操作。
接下来,我们将创建一个CIFAR-10数据集的实例。这是一组32x32彩色图像块,代表10类对象:6种动物和4种车辆。当您运行上面的单元格时,数据集可能需要一两分钟才能完成下载,请注意这一点。这是在PyTorch中创建数据集的一个例子。像上面的CIFAR-10这样的可下载数据集是 torch.utils.data.Dataset 的子类。PyTorch中的数据集类包括 torchvision、torchtext 和 torchaudio 中的可下载数据集,以及实用数据集类,如 torchvision.datasets.ImageFolder,它将读取一个带标签图像的文件夹。您也可以创建自己的 Dataset 子类。
当我们实例化数据集时,我们需要告诉它几件事:我们希望数据存放的文件系统路径;我们是否将此集用于训练,因为大多数数据集会分为训练和测试子集;如果我们尚未下载数据集,是否希望下载它;以及我们想要应用于图像的转换。
准备好数据集后,您可以将其提供给数据加载器。数据集子类包装了对数据的访问,并专门针对所服务数据的类型。数据加载器对数据一无所知,但会根据您在上面示例中指定的参数,将数据集提供的输入张量组织成批次。我们要求数据加载器从训练集中给我们提供每批4张图像,通过 shuffle=True 随机化它们的顺序,并告诉它启动两个工作进程从磁盘加载数据。可视化数据加载器提供的批次是一个好习惯。运行单元格应该会显示一条包含四张图像的条带,并且您应该看到每张图像的正确标签。这里确实是我们的四张图像,看起来像一只猫、一只鹿和两辆卡车。
我们已经深入了解了张量和自动求导,并看到了PyTorch模型是如何构建的,以及如何高效地为它们提供数据。现在是时候将所有部分整合在一起,看看模型是如何被训练的了。
现在我们回到了我们的笔记本,您会看到这里的导入,除了 torch.optim(我很快就会谈到)之外,所有这些都应该从视频的前面部分看起来很熟悉。我们首先需要的是训练和测试数据集。因此,如果您还没有运行下面的单元格,请确保数据集已下载,如果您尚未这样做,可能需要一分钟。
我们将检查数据加载器的输出。再次,我们应该看到一个包含四张图像的条带:飞机、飞机、飞机、轮船。看起来正确。数据加载器工作正常。
这是我们将要训练的模型。如果这个模型看起来很熟悉,那是因为它是LeNet的一个变体,我们在本视频前面讨论过,但它被调整为接受三通道彩色图像。
我们需要的最后成分是损失函数和优化器。损失函数,如本视频前面所讨论的,是衡量模型预测与理想输出之间距离的指标。交叉熵损失是像我们这样的分类模型的典型损失函数。优化器是驱动学习的东西。这里,我们创建了一个实现随机梯度下降的优化器,这是更直接的优化算法之一。除了算法的参数(如学习率和动量)之外,我们还传入了 net.parameters(),这是模型中所有学习权重的集合,优化器将调整这些权重。
最后,所有这些都被组装到训练循环中。继续运行这个单元格,因为它需要几分钟才能执行。这里我们只进行两个训练周期,正如您从第1行看到的,这是对训练数据集的两次完整遍历。每次遍历都有一个内部循环,迭代训练数据,提供批量转换后的图像及其正确标签。
第9行清零梯度是非常重要的一步。当您运行一个批次时,梯度会在该批次上累积。如果我们不重置每个批次的梯度,它们将继续累积并提供不正确的值,学习将停止。在第12行,我们要求模型对批次进行实际预测。在接下来的第13行,我们计算损失,即输出与标签之间的差异。在第14行,我们进行反向传播并计算将指导学习的梯度。在第15行,优化器执行一个学习步骤。它使用来自反向调用的梯度,将学习权重朝着它认为会减少损失的方向微调。
循环的其余部分只是对周期编号、已完成多少训练实例以及整个训练周期内的累计损失进行一些简单的报告。请注意,损失是单调下降的,表明我们的模型在训练数据集上的性能持续提高。
作为最后一步,我们应该检查模型是否真的在进行泛化学习,而不是简单地记忆数据集。这被称为过拟合,通常表明您的数据集太小、示例不足,或者您的模型太大、对数据建模过度。我们防止过拟合的方法是测试模型在未训练过的数据上的表现,这就是为什么我们有一个测试数据集。这里我将运行测试数据,我们会得到一个准确率指标:55%。这虽然不是最先进的技术,但比我们从随机输出中预期的10%要好得多。这表明模型中确实发生了一些泛化学习。


当您不辞辛劳地构建和训练一个非平凡模型时,通常是因为您想用它来做一些事情。您需要将其连接到一个系统,该系统为其提供输入并处理模型的预测。如果您热衷于优化性能,您可能希望在没有Python解释器依赖的情况下完成此操作。好消息是PyTorch通过TorchScript为您提供了便利。
TorchScript是Python的一个静态、高性能子集。当您将模型转换为TorchScript时,模型的动态和Python特性得以完全保留。控制流在转换为TorchScript时得以保留,您仍然可以使用方便的Python数据结构,如列表和字典。查看右侧的代码,您会看到一个用Python定义的PyTorch模型。在其下方,创建了一个模型实例,然后我们调用 torch.jit.script(my_module)。这一行代码就是将Python模型转换为TorchScript所需的全部。此模型的序列化版本保存在最后一行中,它包含有关模型计算图及其学习权重的所有信息。模型的TorchScript表示显示在右侧。
TorchScript旨在由PyTorch即时编译器消费。JIT寻求运行时优化,例如操作重新排序和层融合,以最大化模型在CPU或GPU硬件上的性能。


那么,如何加载和执行TorchScript模型呢?您首先使用 torch.jit.load 加载序列化的包,然后可以像调用任何其他模型一样调用它。更重要的是,您可以在Python中执行此操作,或者您可以将其加载到PyTorch C++运行时中以消除解释型语言的依赖。在后续视频中,我们将更详细地介绍TorchScript、部署的最佳实践,并将涵盖TorchServe,PyTorch的模型服务解决方案。


这就是我们对PyTorch的快速概述。我们在这里使用的模型和数据集相当简单。但PyTorch在大型企业的生产中被用于强大的现实世界用例,例如在人类语言之间翻译、描述视频场景的内容或在视频中生成逼真的人声。在接下来的视频中,我们将让您掌握这种能力,我们将更深入地探讨这里涵盖的所有主题,并提供更复杂的用例,就像您在现实世界中看到的那样。
感谢您的时间和关注。希望能在PyTorch论坛见到您。
002:深入理解PyTorch张量 🧮

在本节课中,我们将深入学习PyTorch张量。张量是PyTorch深度学习模型的核心数据结构,所有的数据、输入、输出和学习权重都必须表示为张量。我们将涵盖张量的创建、数学与逻辑运算、复制、GPU加速、形状操作以及与NumPy的互操作性。
创建张量
上一节我们介绍了张量的重要性,本节中我们来看看如何创建张量。PyTorch提供了多种工厂方法来创建具有或不具有初始值的张量。
以下是几种创建张量的基本方法:
torch.empty():分配内存但不初始化值。例如,torch.empty(3, 4)创建一个3行4列的未初始化张量。torch.zeros():创建指定形状且所有元素为0的张量。例如,torch.zeros(2, 3)。torch.ones():创建指定形状且所有元素为1的张量。例如,torch.ones(2, 3)。torch.rand():创建指定形状且元素为[0, 1)区间内均匀分布随机数的张量。例如,torch.rand(2, 3)。
为了确保使用随机数生成的结果可复现,可以使用 torch.manual_seed() 函数设置随机种子。
torch.manual_seed(1729)
random_tensor = torch.rand(2, 3)
张量形状与数据类型
在操作多个张量时,它们通常需要具有相同的形状。我们可以使用 tensor.shape 属性来查询张量的形状。
张量可以包含不同的数据类型,如浮点数、整数或布尔值。创建时可以指定数据类型,也可以使用 .to() 方法进行转换。
以下是创建时指定数据类型的示例:
int_tensor = torch.ones((2, 3), dtype=torch.int16)
float_tensor = torch.ones((2, 3), dtype=torch.float64)
使用 .to() 方法转换数据类型:
float_tensor = torch.rand(2, 3)
int_tensor = float_tensor.to(torch.int32) # 浮点数将被截断为整数
张量运算
了解了如何创建张量后,我们来看看如何对它们进行运算。PyTorch支持丰富的数学和逻辑运算。
张量与标量运算
运算会应用于张量的每一个元素。
a = torch.zeros(2, 2)
b = a + 1 # b 现在是一个所有元素为1的2x2张量
c = b * 3 # c 现在是一个所有元素为3的2x2张量
张量与张量运算
当两个形状相同的张量进行运算时,运算是逐元素进行的。
a = torch.full((2, 2), 2.0)
b = torch.tensor([[1, 2], [3, 4]], dtype=torch.float)
c = a ** b # 逐元素求幂
d = a + b # 逐元素相加
广播机制
广播允许对不同形状但满足特定规则的张量进行运算。规则是:从最后一个维度开始向前比较,每个维度必须相等,或者其中一个为1,或者其中一个张量在该维度上不存在。
# 示例:将形状为(1, 4)的张量与形状为(2, 4)的张量相乘
random_tensor = torch.rand(2, 4)
multiplier = torch.tensor([[2., 2., 2., 2.]])
result = random_tensor * multiplier # 乘法被广播到每一行
常用操作与原地操作
PyTorch提供了超过300种张量操作,包括数学函数、三角函数、逻辑运算、比较运算和归约运算等。
有时,为了节省内存,我们希望在现有张量上直接修改结果,而不是创建新的张量。这可以通过使用带下划线 _ 的方法来实现。
以下是原地操作的示例:
a = torch.rand(2, 2)
b = torch.rand(2, 2)
a.add_(b) # 将b加到a上,结果存储在a中
许多函数也支持 out 参数,用于将结果直接输出到已分配的张量中。
a = torch.rand(2, 2)
b = torch.rand(2, 2)
c = torch.zeros(2, 2)
torch.matmul(a, b, out=c) # 矩阵乘法的结果存入c
复制张量
在Python中,简单的赋值 (b = a) 不会创建数据的副本,而只是创建了一个新的引用。要创建数据的独立副本,需要使用 .clone() 方法。
a = torch.ones(2, 2)
b = a.clone() # b是a数据的独立副本
a[0, 0] = 999
print(b[0, 0]) # 输出仍然是1,b未受影响
如果源张量启用了自动求导 (requires_grad=True),其克隆体也会跟踪梯度历史。若想克隆时不跟踪梯度,可以使用 .detach() 方法。
a = torch.rand(2, 2, requires_grad=True)
b = a.clone() # b也跟踪梯度
c = a.detach().clone() # c不跟踪梯度
GPU加速
PyTorch的一个核心优势是GPU加速。首先,检查GPU是否可用:
torch.cuda.is_available()
创建张量时指定设备,或使用 .to() 方法将现有张量移动到目标设备。
# 创建时指定
if torch.cuda.is_available():
gpu_tensor = torch.rand(2, 2, device='cuda')
# 移动现有张量
cpu_tensor = torch.rand(2, 2)
gpu_tensor = cpu_tensor.to('cuda')
最佳实践是使用设备句柄,避免在代码中硬编码字符串。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
my_tensor = torch.rand(2, 2, device=device)
操作张量形状
在模型构建中,经常需要改变张量的形状。
增加或减少维度 (unsqueeze / squeeze)
unsqueeze(dim):在指定维度索引处插入一个大小为1的新维度。常用于将单个实例变为批次大小为1的输入。squeeze(dim):移除指定维度(该维度大小必须为1)。常用于移除批次维度。
# 假设有一个代表单张图片的张量 (3, 226, 226)
single_image = torch.rand(3, 226, 226)
batch_of_one = single_image.unsqueeze(0) # 形状变为 (1, 3, 226, 226)
# 模型输出可能是 (1, 20)
model_output = torch.rand(1, 20)
single_prediction = model_output.squeeze(0) # 形状变为 (20,)
重塑形状 (reshape)
reshape 方法可以在保持元素总数不变的前提下,改变张量的维度。它通常会返回一个原张量的视图(共享内存),但在某些情况下会返回副本。
# 卷积层输出假设为 (6, 20, 20)
conv_output = torch.rand(6, 20, 20)
# 重塑为全连接层需要的1维向量
fc_input = conv_output.reshape(6 * 20 * 20) # 或 reshape(-1)
与NumPy的互操作性
如果你有现有的NumPy代码和数据,可以轻松地在NumPy数组和PyTorch张量之间转换。
import numpy as np
# NumPy数组 -> PyTorch张量
np_array = np.ones((2, 3))
torch_tensor = torch.from_numpy(np_array)
# PyTorch张量 -> NumPy数组
torch_tensor = torch.rand(2, 3)
np_array = torch_tensor.numpy()
重要提示:通过这种方式转换的对象共享底层内存。修改其中一个会影响到另一个。
np_array[0, 0] = 999
print(torch_tensor[0, 0]) # 输出 999

本节课中我们一起学习了PyTorch张量的核心概念与操作:从创建、指定数据类型,到进行数学运算和利用广播机制;从复制张量、使用GPU加速,到灵活操作张量形状;最后还了解了PyTorch与NumPy之间便捷的数据交换。掌握这些基础知识是构建和训练深度学习模型的关键第一步。建议结合PyTorch官方文档进行更深入的探索和实践。
003:自动求导基础
在本节课中,我们将要学习PyTorch的核心特性之一:自动求导。自动求导是PyTorch能够快速、灵活地支持基于反向传播的机器学习模型训练的关键。我们将了解它的作用、工作原理,以及如何在实践中使用和控制它。
什么是自动求导?🤔
PyTorch的自动求导功能是其成为快速、灵活的深度学习框架的重要组成部分。它通过简化偏导数(也称为梯度)的计算来实现这一点,这些梯度驱动着基于反向传播的学习。
在训练模型时,我们会计算一个损失函数,它告诉我们模型的预测与理想值相差多远。然后,我们需要找到损失函数相对于模型学习权重(即参数)的偏导数。这些导数告诉我们为了最小化损失,需要朝哪个方向调整权重。这涉及到在计算的每条路径上迭代应用微积分的链式法则。
自动求导通过在运行时追踪你的计算来加速这一过程。模型计算产生的每个输出张量都携带着导致它的一系列操作的历史记录。这个历史记录允许快速计算整个计算图的导数,一直回溯到模型的学习权重。此外,由于这个历史记录是在运行时收集的,即使你的模型具有包含决策分支和循环的动态结构,你也能得到正确的导数。这比依赖静态计算图分析的工具提供了更大的灵活性。

一个简单的自动求导示例 🔍
上一节我们介绍了自动求导的基本概念,本节中我们来看看一个具体的代码示例,感受一下自动求导在幕后做了什么。
首先,我们导入必要的库并创建一个需要计算梯度的张量。
import torch
import matplotlib.pyplot as plt
# 创建一个一维张量,值在0到2π之间,并设置需要计算梯度
a = torch.linspace(0, 2*torch.pi, steps=10, requires_grad=True)
print(a)
打印张量a时,PyTorch会提示它需要计算梯度。接下来,我们进行一个计算:取a中所有值的正弦。
b = torch.sin(a)
plt.plot(a.detach().numpy(), b.detach().numpy())
plt.show()
print(b)
打印张量b,我们看到PyTorch告诉我们它有一个grad_fn属性。这意味着b来自一个至少有一个输入需要计算梯度的计算。grad_fn告诉我们b来自正弦运算。
让我们再执行几个步骤:将b乘以2再加1。
c = 2 * b
d = c + 1
print(c)
print(d)
输出张量再次在其grad_fn属性中包含关于生成它们的操作的信息。
默认情况下,自动求导期望梯度计算的最终函数输出是单个标量值。当我们计算学习权重的导数时就是这种情况,因为损失函数的输出是一个标量值。它不一定必须是单值,但我们稍后会讨论。这里,我们只是对张量的元素求和,并将其称为此计算的最终输出。
output = d.sum()
print(output)
实际上,我们可以使用任何输出或中间张量的grad_fn属性,通过grad_fn.next_functions属性回溯到计算历史的起点。例如,d知道它来自加法操作,加法操作知道它来自乘法操作,依此类推,直到a。a没有grad_fn,它是这个计算图的输入或叶节点,因此代表我们想要计算梯度的目标变量。
那么,我们如何实际计算梯度呢?很简单,只需在输出张量上调用.backward()方法。
output.backward()
print(a.grad)
回顾计算过程,我们有一个正弦函数,其导数是余弦。我们乘以了2,这应该给梯度增加一个因子2。还有加法,它根本不应该改变导数。绘制a的.grad属性,我们确实看到计算出的梯度是余弦值的两倍。
需要注意的是,梯度只针对计算的输入或叶节点进行计算。在反向传播之后,中间张量不会附加梯度。
自动求导在训练循环中的角色 🔄
我们已经窥探了自动求导在简单情况下如何计算梯度。接下来,我们将检查它在PyTorch模型训练循环中的角色。
为了了解自动求导在训练中如何工作,让我们构建一个小模型,并观察它在单个训练批次中的变化。
首先,我们定义并实例化一个模型,并为训练输入和理想输出创建一些标准张量。
import torch.nn as nn
class SimpleModel(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Linear(3, 1)
def forward(self, x):
return self.layer(x)
model = SimpleModel()
input_data = torch.randn(5, 3) # 5个样本,每个3个特征
target = torch.randn(5, 1) # 5个样本的目标值
# 查看模型层的权重(学习参数)
print(model.layer.weight)
print(model.layer.weight.grad) # 此时梯度应为None
你可能已经注意到,在torch.nn.Module的子类中,我们没有为模型的层指定requires_grad=True,梯度跟踪是为你管理的。查看模型的层,你可以看到随机初始化的权重,并且它们还没有计算梯度。
现在让我们看看在一个训练批次后这是如何变化的。我们将使用预测值与理想输出之间的欧几里得距离平方作为损失函数,并设置一个使用随机梯度下降的基本优化器。
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 前向传播
prediction = model(input_data)
loss = loss_fn(prediction, target)
print(f"Loss before backward: {loss.item()}")
print(f"Weight grad before backward: {model.layer.weight.grad}") # 应为None
# 反向传播
loss.backward()
print(f"Weight grad after backward: {model.layer.weight.grad}") # 现在应有梯度值
print(f"Weight value after backward: {model.layer.weight}") # 权重应未改变
当我们调用loss.backward()时,可以看到权重没有改变,但我们已经计算了梯度。这些梯度指导优化器确定如何调整权重以最小化损失分数。
为了实际更新权重,我们必须调用optimizer.step()。
optimizer.step()
print(f"Weight value after optimizer.step(): {model.layer.weight}") # 权重已更新
这就是PyTorch模型中学习发生的方式。
过程中还有一个重要的步骤。在调用optimizer.step()之后,你需要调用optimizer.zero_grad()。如果不这样做,梯度将在每个训练批次中累积。
# 错误示例:不调用 zero_grad
for i in range(5):
prediction = model(input_data)
loss = loss_fn(prediction, target)
loss.backward()
print(f"Batch {i+1}, Gradient norm: {model.layer.weight.grad.norm()}")
# 注意:这里没有 optimizer.zero_grad()
# 正确做法:每次训练步骤后重置梯度
optimizer.zero_grad()
prediction = model(input_data)
loss = loss_fn(prediction, target)
loss.backward()
print(f"Gradient after zero_grad and new backward: {model.layer.weight.grad.norm()}")
如果你的模型没有学习或者训练给出了奇怪的结果,你应该检查的第一件事就是是否在每个训练步骤后调用了zero_grad。
控制梯度跟踪 🎛️
有时你会想要控制是否对某个计算进行梯度跟踪。有多种方法可以做到这一点,具体取决于情况。
最简单的方法是直接设置requires_grad标志。
x = torch.ones(2, 2, requires_grad=True)
y1 = x ** 2
print(f"y1 requires_grad: {y1.requires_grad}, grad_fn: {y1.grad_fn}")
x.requires_grad_(False) # 关闭x的梯度跟踪
y2 = x ** 2
print(f"y2 requires_grad: {y2.requires_grad}, grad_fn: {y2.grad_fn}")
我们可以看到y1有一个grad_fn,但y2没有,因为我们在计算y2之前关闭了x的历史跟踪。
如果你只需要临时关闭自动求导,可以使用torch.no_grad()上下文管理器。
x = torch.ones(2, 2, requires_grad=True)
with torch.no_grad():
y3 = x ** 2
print(f"Inside no_grad context, y3 requires_grad: {y3.requires_grad}")
y4 = x ** 2
print(f"Outside no_grad context, y4 requires_grad: {y4.requires_grad}")
no_grad也可以用作函数或方法装饰器,导致装饰函数内部的计算关闭历史跟踪。
对应的上下文管理器是torch.enable_grad(),用于在局部上下文中打开自动求导。它也可以用作装饰器。
最后,你可能有一个正在跟踪历史的张量,但需要一个不跟踪历史的副本。在这种情况下,张量对象有一个.detach()方法,可以创建一个与计算历史分离的张量副本。
x = torch.ones(2, 2, requires_grad=True)
y = x ** 2
z = y.detach() # z是y的副本,但不跟踪梯度
print(f"z requires_grad: {z.requires_grad}, grad_fn: {z.grad_fn}")
关于自动求导机制还有一个重要的注意事项:你必须小心对正在跟踪梯度的张量使用原地操作。这样做可能会破坏你稍后正确进行反向传播所需的信息。事实上,如果你尝试对需要梯度的输入张量执行原地操作,PyTorch甚至会给你一个运行时错误。
使用自动求导分析器 📊
自动求导追踪张量计算的每一步,将这些信息与时间测量相结合,对于分析梯度追踪计算非常有用。事实上,这个功能是自动求导的一部分。
以下是分析器的基本用法示例。
x = torch.randn(1000, 1000, requires_grad=True)
y = torch.randn(1000, 1000, requires_grad=True)
with torch.autograd.profiler.profile(use_cuda=False) as prof:
for _ in range(10):
z = x @ y # 矩阵乘法
z.sum().backward()
# 打印分析结果
print(prof.key_averages().table(sort_by="self_cpu_time_total"))
自动求导分析器还可以按代码块或输入形状对结果进行分组,并可以将结果导出给其他追踪工具。详细文档有完整说明。
自动求导高级API 🚀
PyTorch 1.5引入了自动求导高级API,它公开了自动求导底层的一些核心操作。为了最好地解释这一点,我们需要更深入地了解自动求导在幕后做了什么。
假设你有一个具有n个输入和m个输出的函数,即 y = f(x)。输出相对于输入的完整偏导数集合是一个称为雅可比矩阵的矩阵。
现在,如果你有第二个函数,我们称之为 L = g(y),它接受一个与第一个函数输出维度相同的n维输入,并返回一个标量输出。你可以将其相对于y的梯度表示为一个列向量(本质上是一个单列的雅可比矩阵)。
将其与我们一直在讨论的内容联系起来:将第一个函数想象成你的PyTorch模型,它可能有许多输入、许多学习权重和许多输出;将第二个函数想象成一个损失函数,它以模型的输出作为输入,以损失值作为标量输出。
如果我们用第二个函数的梯度乘以第一个函数的雅可比矩阵,并应用链式法则,我们会得到另一个列向量。这个列向量表示第二个函数相对于第一个函数输入的偏导数。或者,在我们的机器学习模型的情况下,就是损失相对于学习权重的偏导数。
torch.autograd是一个用于计算这些向量-雅可比积的引擎。这就是我们在反向传播过程中累积学习权重梯度的方式。
因此,.backward()调用也可以接受一个可选的向量输入。该向量表示输出张量上的一组梯度,这些梯度会乘以前面自动求导追踪张量的雅可比矩阵。
让我们尝试一个带有小向量的具体例子。
x = torch.randn(3, requires_grad=True)
y = x * 2
while y.norm() < 1000:
y = y * 2
print(f"y: {y}")
# 如果现在尝试调用 y.backward(),会得到一个运行时错误,提示梯度只能为标量输出隐式计算。
# 对于多维输出,自动求导期望你提供这三个输出的梯度,以便它可以乘入雅可比矩阵。
v = torch.tensor([1.0, 0.1, 0.01], dtype=torch.float)
y.backward(v)
print(f"x.grad: {x.grad}")
自动求导上有一个API,可以直接访问重要的微分矩阵和向量操作。特别是,它允许你计算特定函数在特定输入下的雅可比矩阵和海森矩阵。海森矩阵类似于雅可比矩阵,但表示所有二阶偏导数。
让我们计算一个简单函数的雅可比矩阵,并针对两个单元素输入进行评估。
from torch.autograd.functional import jacobian
def func(x):
return torch.stack([torch.exp(x) * 2, x * 3])
input1 = torch.tensor([1.0], requires_grad=False)
input2 = torch.tensor([2.0], requires_grad=False)
J1 = jacobian(func, input1)
J2 = jacobian(func, input2)
print(f"Jacobian at input1 (x=1.0): {J1}")
print(f"Jacobian at input2 (x=2.0): {J2}")
# 第一个输出应等于 2 * exp(x),因为 exp(x) 的导数是它自身。
# 第二个值应为 3。
当然,你也可以对更高阶的张量进行此操作。这里我们使用不同的输入集计算了同一个加法函数的雅可比矩阵。
如果你提供向量,还有一个函数可以直接计算向量-雅可比积。autograd的.jvp()方法执行与.vjp()相同的矩阵乘法,只是操作数顺序相反。.vhp()和.hvp()方法对向量-海森积执行相同的操作。更多信息,包括重要的性能说明,请参阅新的Autograd Functional API文档。
总结 📝


本节课中我们一起学习了PyTorch自动求导的基础知识。我们了解了自动求导如何通过追踪计算历史来高效计算梯度,从而驱动反向传播。我们通过代码示例观察了梯度计算的过程,并探讨了自动求导在模型训练循环中的核心作用。我们还学习了如何控制梯度跟踪的开关,例如使用requires_grad、no_grad上下文管理器和.detach()方法。最后,我们简要介绍了自动求导分析器和PyTorch 1.5引入的高级API,它们为更底层的梯度操作提供了工具。掌握这些概念是有效使用PyTorch进行深度学习模型开发的关键。
004:使用PyTorch构建模型 🏗️
在本节课中,我们将学习如何使用PyTorch的核心类来构建机器学习模型。我们将重点介绍Module和Parameter类,它们分别用于封装模型和学习权重。此外,我们还将探讨常见的神经网络层类型、其他功能层以及损失函数。

模型构建的核心:Module与Parameter类
模型构建的核心围绕着torch.nn模块中的两个类:Module类和Parameter类。Module类用于封装模型及其组件,例如神经网络层。Parameter类是torch.Tensor的子类,用于表示需要学习的权重。
当将一个Parameter对象作为Module的属性进行赋值时,该参数对象会被注册到该模块中。如果你将一个Module子类的实例注册为另一个模块的属性,那么被包含模块的参数也会被注册为拥有者类的参数。
为了更好地理解,让我们来看一个简单的例子。
以下是构建一个简单模型的方法:
import torch
import torch.nn as nn
class TinyModel(nn.Module):
def __init__(self):
super(TinyModel, self).__init__()
self.linear1 = nn.Linear(100, 200)
self.activation = nn.ReLU()
self.linear2 = nn.Linear(200, 10)
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
x = self.linear1(x)
x = self.activation(x)
x = self.linear2(x)
x = self.softmax(x)
return x
这个模型展示了PyTorch模型的常见结构。首先,它是torch.nn.Module的子类。__init__方法定义了模型的结构,即组成模型的层和函数。forward方法则将这些层和函数组合成实际的计算流程。
当我们创建这个模型的实例并打印它时,可以看到它不仅知道自己的层和属性,还知道它们被注册的顺序。打印其中一个层,会得到该层的描述。我们的模型和线性层都是torch.nn.Module的子类,因此可以通过parameters()方法访问它们的参数。模型会递归地注册其拥有的所有子模块的参数,这一点非常重要,因为在训练时,模型需要将所有参数传递给优化器。
常见的神经网络层类型
PyTorch提供了封装现代机器学习模型中常用层类型的类。
线性层(全连接层)
最基本的是全连接层或线性层,我们在上面的例子中已经见过。在这种层中,每个输入都会影响每个输出,因此称为“全连接”。影响的程度由该层的权重决定。
如果一个层有M个输入和N个输出,其权重将是一个M×N的矩阵。以下是一个简单的线性层示例:
lin = nn.Linear(3, 2) # 3个输入,2个输出
x = torch.rand(1, 3) # 随机3元素输入向量
y = lin(x) # 2元素输出向量
print(y)
当你打印参数时,它会显示这些参数需要梯度(requires_grad=True),这意味着它们会跟踪计算历史,以便我们可以计算用于学习的梯度。Parameter是torch.Tensor的子类,但其默认将requires_grad设置为True的行为与Tensor类不同。
线性层在深度学习模型中应用广泛,一个常见的地方是在分类器模型的末端,最后一层或最后几层通常是线性层。
卷积层
卷积层旨在处理在空间上强相关的数据。它们在计算机视觉模型中很常见,可用于检测有趣特征的紧密集群,并将其组合成更大的特征或识别对象。它们也出现在其他上下文中,例如自然语言处理应用,因为一个单词的意图通常受其附近单词的影响。
让我们以经典的LeNet-5模型为例,仔细看看卷积计算是如何构建的。LeNet-5旨在接收32x32像素的手写数字黑白图像块,并根据所表示的数字对其进行分类。
观察模型中的第一个卷积层:
conv1 = nn.Conv2d(1, 6, 5)
其参数是(1, 6, 5)。第一个参数是输入通道数,对于黑白图像,只有一个数据通道,所以是1。第二个参数6是该层要学习的特征数量,因此它可以识别输入中最多六种不同的像素排列。第三个参数5是卷积核的大小,你可以将其想象成一个在输入上扫描的窗口,收集这个5像素窗口内的特征。该卷积层的输出是一个激活图,即它发现某些特征的空间位置图。
第二个卷积层类似,它将第一层的输出作为输入,因此其第一个参数是6。我们要求它学习16个不同的特征,这些特征通过组合第一层的特征来形成。这里我们只使用一个3元素的窗口进行卷积。
在第二个卷积层将其特征组合成更高级别的激活图后,我们将输出传递给一组线性层,这些线性层充当分类器,最后一层有10个输出,代表输入表示10个数字中某一个的概率。


PyTorch为1维、2维和3维输入提供了卷积神经网络层。还有更多可选参数,如步长和填充,你可以在文档中查阅。
循环神经网络层
循环神经网络是为处理序列数据而设计的神经网络,例如自然语言句子中的一串单词,或仪器的一串实时测量值。RNN通过保持一个隐藏状态来实现这一点,该状态充当其对序列中迄今为止所见内容的记忆。
RNN层或其变体(长短期记忆网络LSTM和门控循环单元GRU)的内部结构相当复杂,超出了本视频的范围。但我们可以通过一个基于LSTM的词性标注器来展示它的实际应用。
以下是其构造函数的四个参数:
- 输入词汇表大小:即它要识别的整个单词库的大小。
- 标签集大小:模型要识别并输出的标签数量。
- 嵌入维度:词汇表嵌入空间的大小。
- 隐藏维度:LSTM记忆的大小。
输入将是一个句子,其中单词表示为独热向量的索引。嵌入层会将这些映射到嵌入维度的空间中。LSTM接收一系列嵌入,并对其进行迭代,产生一个长度为隐藏维度的输出向量。最后的线性层充当分类器,对输出应用log softmax,最后一层将输出转换为一组归一化的估计概率,表示给定单词映射到给定词性标签的概率。
如果你想看这个网络的运行,PyTorch官网上有一个相关的教程。
Transformer层
Transformer是多用途的神经网络,但近年来,随着BERT(一种Transformer模型)的成功,我们经常在自然语言应用中看到它们。关于Transformer架构的讨论有些复杂,超出了本视频的范围。但要知道,PyTorch有一个Transformer类,允许你定义Transformer模型的整体参数,例如编码器和解码器的层数、注意力头的数量、dropout和激活函数等。你甚至可以使用torch.nn.Transformer类,通过正确的参数从这一个类构建BERT模型。
PyTorch还有类来封装Transformer的各个组件,例如编码器、解码器以及组成它们的层。


其他功能层
除了学习层,模型中还有一些执行重要功能的非学习层类型。
池化层
一个例子是最大池化及其孪生兄弟最小池化。这些函数通过将单元格组合在一起,并将这些输入单元格的最大值分配给输出单元格来减少张量。这可能通过例子更容易解释。
# 假设有一个6x6的输入
pool = nn.MaxPool2d(3, stride=1)
# 使用3x3窗口,步长为1进行最大池化,输出将是4x4
归一化层
归一化层在将一层的输出馈送到另一层之前,对其进行重新中心和归一化。在计算过程中对中间张量进行中心和缩放有许多有益效果,例如允许你使用更高的学习率,而不会出现梯度消失或爆炸的问题。
运行以下代码,你向一个随机输入张量添加了一个大的缩放因子和偏移。你应该看到输入张量的均值大约在15附近。在我们通过归一化层运行它之后,你可以看到值都变小了,并且集中在0附近。事实上,它的均值应该非常小。这很好,因为许多激活函数(我们稍后会讨论)在零附近有最强的梯度,但对于使它们远离零的输入,有时会遭受梯度消失或爆炸的问题。将数据保持在梯度最陡的区域附近,意味着学习往往会更快发生并更快收敛,并且更高的学习率对你的训练将是可行的。
Dropout层


Dropout层是鼓励模型中稀疏表示的工具,即推动它用更少的数据进行推理。Dropout层通过在训练期间随机将输入张量的部分设置为0来工作。在推理时,Dropout层总是关闭的。这迫使模型学习如何针对被屏蔽或减少的数据输入进行推理。
例如,我创建一个随机输入张量,并将其通过一个dropout层两次。你会看到有一些零和一些值,但这些值总是相同的。它是在整个张量中随机设置零。你可以使用可选的p参数来设置概率,这里我们设置为40%,默认是0.5。
激活函数与损失函数
构建模型所需的最后成分是激活函数和损失函数。激活函数是使深度学习成为可能的部分原因。如果你回想一下前面的线性层例子,它只是一个简单的矩阵乘法,接收一个输入向量并得到一个输出向量。如果我们把许多这样的层堆叠在一起,无论我们堆叠多少层,我们总是可以将其简化为单个矩阵乘法,这意味着我们只能模拟线性方程。这就是激活函数的作用所在,通过在层之间插入非线性激活函数,我们获得了模拟非线性方程的能力。
torch.nn模块提供了所有主要的激活函数,包括其许多变体的整流线性单元、双曲正切、硬双曲正切、Sigmoid等。它还包括其他函数,如Softmax,在模型的输出阶段非常有用。
PyTorch有多种常见的损失函数,包括均方误差(与L2范数相同)、交叉熵损失和负对数似然损失,这些对分类器和其他应用很有用。
总结

在本节课中,我们一起学习了使用PyTorch构建模型的基础知识。我们深入了解了Module和Parameter这两个核心类如何协同工作来封装模型和学习参数。我们探讨了多种神经网络层,包括线性层、卷积层、循环神经网络层和Transformer层,并了解了它们各自的应用场景。此外,我们还介绍了池化层、归一化层和Dropout层等重要功能层的作用。最后,我们认识到非线性激活函数对于模型表达能力的关键性,并了解了PyTorch提供的常见损失函数。掌握这些组件是构建有效机器学习模型的第一步。
005:TensorBoard支持 🎛️
在本节课中,我们将学习如何在PyTorch项目中使用TensorBoard。TensorBoard是一个强大的可视化工具,可以帮助我们监控训练过程、理解模型结构以及探索数据集。我们将通过一个服装分类的实战项目,学习如何记录图像、绘制损失曲线、可视化模型计算图以及创建数据嵌入投影。
环境准备与项目介绍

首先,你需要设置一个包含最新版PyTorch和TensorBoard的Python环境。屏幕上的命令展示了如何使用Conda和Pip进行安装。我们还将使用matplotlib来处理图像。
安装好依赖项后,你可以在设置好的环境中运行本视频的配套笔记本。

对于本次模型,我们将训练一个简单的神经网络来识别不同的服装类别。我们将直接可视化数据元素,跟踪训练过程的成功与否。我们将使用TensorBoard来深入观察模型本身,并对整个数据集及其内部关系进行更高级的可视化。
我们将使用Fashion-MNIST数据集。这是一组描绘了各种服装的小图像块,根据所描绘的服装类型进行分类。
对于模型,我们将使用一个经过调整以适应Fashion-MNIST数据集的LeNet-5版本。


导入库与设置数据
我们将从导入所需的库开始,并从torch.utils.tensorboard导入SummaryWriter类。这个类封装了PyTorch中的TensorBoard支持,将是你与TensorBoard交互的主要接口。
在将数据输入模型之前,可视化你的训练数据是一个好习惯,尤其是在计算机视觉任务中。让我们来设置数据集。
我们将使用torchvision下载数据集的训练集和验证集拆分。我们稍后会详细讨论验证集。我们还将为每个数据集拆分设置数据加载器,并定义我们要进行分类的类别。
让我们可视化数据集的一些实例。我们将使用一个迭代器提取一些数据实例,并创建一个matplotlib辅助函数将它们批量组合在一个网格中。
让我们在笔记本中显示它们。那么,如何将它们添加到TensorBoard呢?
只需一行代码即可将数据写入日志目录。
请注意,我们还调用了summary writer对象的flush方法。这确保了我们通过writer记录的所有内容都已写入磁盘。



现在,让我们切换到终端并启动TensorBoard。

我们将复制TensorBoard命令行给出的URL,并查看“IMAGES”选项卡。
请注意,我们添加的图像有一个标题,其中包含我们将图像保存到TensorBoard日志目录时应用的标签。
使用TensorBoard评估训练过程
接下来,我们将使用TensorBoard来帮助评估我们的训练过程。
我们将绘制训练过程中定期累积的训练损失,并将其与在验证数据集上测量的损失进行比较。为了提供背景,这里简要说明一下我们正在做什么以及为什么这样做。
如果你上过数学课,很可能在完成一些作业后,你会参加考试。考试题目在性质上与你已经见过的作业题相似,但在具体内容上有所不同。这是为了确保你学习课程内容,而不仅仅是记住作业题。
类似地,我们可以使用验证数据集。这是总数据集中未用于训练的一部分,用于查看我们的模型是在进行泛化学习,还是过度拟合了训练数据(类似于记忆训练实例,而不是对我们试图优化的通用函数进行建模)。
让我们设置一个包含验证检查的训练循环,并绘制结果。
这里我们有一个训练循环。你可以看到,在代码顶部,我们声明了一个变量来累积模型预测的测量损失,该损失将每1000个训练步骤报告一次。我们还将对验证数据集进行单独的损失检查。
为了跟踪和比较两个不同的量,我们将使用summary writer的add_scalars调用,它允许我们添加一个包含多个标量值的字典,每个值都有不同的标签,在图表上会有各自的线条。
让我们运行单元格看看效果。切换到TensorBoard并查看“SCALARS”选项卡,我们可以看到我们的损失在训练过程中单调递减。这是一个很好的迹象,表明训练正在起作用。😊
但我们是否过拟合了呢?从图表上看,我们可以看到验证曲线和训练曲线很好地收敛在一起。
可视化模型计算图
接下来,让我们使用TensorBoard来更好地理解我们的模型以及数据如何流经它。
为此,我们将使用summary writer的add_graph方法。此方法将模型和一个样本输入作为参数,该样本输入将用于跟踪数据在模型中的流动。
我们将运行单元格并切换到TensorBoard。进入“GRAPHS”选项卡,我们可以看到一个非常简单的图,显示模型一侧输入数据,另一侧输出结果。
当然,我们希望看到更多细节,我们可以通过双击图中的模型节点来获取。在这里,我们可以看到包含我们所有层的图,以及指示数据如何流经它们的箭头。请注意,由于模型两次使用相同的最大池化对象,第二个卷积层看起来像是嵌入在一个循环中。但从代码中可以看出,数据流比那更线性。
创建数据嵌入投影
我们已经使用TensorBoard来显示数据实例的可视化。但整个数据集呢?
嵌入是将实例从高维空间映射到低维空间的一种技术。这在自然语言处理中很常见。如果你有一个由one-hot向量表示的10000个单词的词汇表,你的单词就是10000维空间中的单位向量。如果你训练一个将这些向量映射到低维空间的嵌入层,关系就会出现。例如,像“good”、“excellent”和“fabulous”这样的单词的新向量往往会聚集在那个低维空间中。
在我们的例子中,我们的28x28图像块可以被视为784维向量。我们可以使用summary writer的add_embedding方法将其投影到一个交互式的3D可视化中。
以下是一段代码,用于选择数据的随机样本,进行标记并投影。
请注意,和往常一样,我们使用flush方法来确保所有数据都写入磁盘。

切换到TensorBoard。

我们可以在“PROJECTOR”选项卡上看到我们新嵌入的3D可视化。

放大后,我们可以看到一些大的结构,3D空间内的一些弧线。放大这些结构中的一些,我们可以看到其中一些弧线聚集了相似的服装类型。
放大你自己的数据样本,看看是否能识别出不同类型服装在这个3D投影中是如何聚类的模式。

总结与资源

本节课中,我们一起学习了如何在PyTorch中集成和使用TensorBoard。我们涵盖了从记录图像、绘制标量图表以监控训练和验证损失,到可视化模型计算图以理解数据流,最后使用嵌入投影来探索高维数据集的结构。

以下是本教程涉及的核心操作代码示例:

-
创建SummaryWriter:
from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter('runs/fashion_mnist_experiment_1') -
记录图像:
writer.add_image('four_fashion_mnist_images', img_grid) writer.flush() -
记录标量(如损失):
writer.add_scalars('training vs. validation loss', {'training' : running_loss / 1000, 'validation' : val_loss}, epoch * len(trainloader) + i)

-
记录模型计算图:
writer.add_graph(model, images) -
记录嵌入投影:
writer.add_embedding(features, metadata=class_labels, label_img=images.unsqueeze(1)) writer.flush()
如果你想了解更多关于PyTorch TensorBoard支持的信息,可以访问以下资源:
- PyTorch官方文档
pytorch.org,查看torch.utils.tensorboard.SummaryWriter的完整文档。 - PyTorch教程部分
pytorch.org/tutorials,有关于使用TensorBoard的教程。 - TensorBoard文档本身,当然,提供了关于TensorBoard更详细的介绍。如果你想更深入地了解summary writer在底层做了什么,这些文档会很有帮助。


006:使用PyTorch进行模型训练 🚀
在本节课中,我们将学习使用PyTorch进行模型训练的核心流程。我们将从数据加载开始,逐步介绍损失函数、优化器,并最终将这些组件整合到一个完整的训练循环中。

数据加载:Dataset与DataLoader 📦
上一节我们介绍了模型构建和梯度计算。本节中,我们来看看如何高效地为模型提供训练数据。PyTorch通过两个主要类实现高效的数据处理:Dataset和DataLoader。
Dataset负责从你的数据源中访问和处理单个数据实例。PyTorch领域API(如torchvision、torchtext、torchaudio)提供了许多现成的数据集,你也可以通过继承Dataset父类来创建自己的数据集。
DataLoader从Dataset中提取数据实例(自动或通过自定义采样器),将它们收集成批次,并返回给你的训练循环使用。DataLoader适用于所有类型的数据集。
以下是创建自定义数据集的关键步骤:
- 子类化
torch.utils.data.Dataset。 - 重写
__len__方法以返回数据集中的项目数。 - 重写
__getitem__方法以通过键(通常是索引)访问数据实例。
from torch.utils.data import Dataset
class CustomDataset(Dataset):
def __init__(self, ...):
# 初始化数据、标签等
pass
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
sample = self.data[idx]
label = self.labels[idx]
return sample, label
创建DataLoader时,唯一必需的参数是一个Dataset对象。以下是几个常用的可选参数:
batch_size:设置每个训练批次中的实例数量。shuffle:设置为True会在每个epoch开始时打乱数据顺序,这对训练很重要。num_workers:设置用于并行加载数据的子进程数量。
from torch.utils.data import DataLoader
train_loader = DataLoader(dataset=train_dataset,
batch_size=32,
shuffle=True,
num_workers=2)
如果需要在训练期间将数据批次转移到GPU,建议使用固定内存缓冲区(pinned memory)以加速主机到GPU的数据传输。这可以通过在创建DataLoader时设置pin_memory=True来自动完成。
损失函数:衡量模型误差 📉
在准备好数据之后,我们需要一种方法来衡量模型的预测与真实值之间的差距,这就是损失函数。PyTorch提供了多种适用于不同任务的常用损失函数。
以下是几种常见的损失函数及其应用场景:
nn.MSELoss:用于回归任务(均方误差)。nn.KLDivLoss:用于比较连续概率分布(KL散度)。nn.BCELoss:用于二分类任务(二元交叉熵)。nn.CrossEntropyLoss:用于多分类任务(交叉熵)。
所有损失函数都将模型的输出与某些标签或期望值进行比较。对于本视频中的分类任务,我们将使用交叉熵损失。
import torch.nn as nn
loss_fn = nn.CrossEntropyLoss()
# 假设 outputs 是模型预测,labels 是真实标签
loss = loss_fn(outputs, labels) # 返回整个批次的平均损失
优化器:更新模型权重 ⚙️

知道了模型的误差(损失)后,我们需要一种算法来根据这个误差调整模型的权重,使其预测更准确,这就是优化器的作用。PyTorch提供了多种优化算法。

所有优化器在初始化时都需要模型的参数,这些参数是通过调用模型对象的.parameters()方法获得的。这些权重将在训练过程中被更新。
import torch.optim as optim
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
重要提示:在使用PyTorch优化器时,请确保你的模型参数存储在正确的设备上。如果你在GPU上训练,必须在初始化优化器之前将模型参数移动到GPU内存中。否则,优化器将更新错误的参数副本,导致损失不会随时间下降。


大多数基于梯度的优化器都有以下参数的组合:
lr(学习率):决定优化器更新权重时的步长大小。momentum(动量):使优化器在过去几个时间步中改进最强的方向上采取稍大的步骤。weight_decay(权重衰减):用于鼓励权重正则化,避免过拟合。
整合:完整的训练循环 🔄
现在,我们拥有了所有必需的组件:一个模型、一个包装在DataLoader中的数据集、一个损失函数和一个优化器。我们可以开始训练了。
以下是执行一个训练周期(epoch,即完整遍历一次训练数据)的函数示例:
def train_one_epoch(epoch_index):
running_loss = 0.
last_loss = 0.
for i, data in enumerate(train_loader):
# 每个批次包含输入张量和标签
inputs, labels = data
# 清零梯度
optimizer.zero_grad()
# 前向传播,获取预测
outputs = model(inputs)
# 计算损失
loss = loss_fn(outputs, labels)
# 反向传播,计算梯度
loss.backward()
# 优化器根据梯度更新权重
optimizer.step()
# 累计损失
running_loss += loss.item()
if i % 1000 == 999: # 每1000个批次记录一次
last_loss = running_loss / 1000
print(f' batch {i+1} loss: {last_loss}')
running_loss = 0.
return last_loss
接下来,我们将循环多个epoch。在每个epoch中,我们会:
- 将模型设置为训练模式(
model.train()),启用梯度计算跟踪。 - 训练一个epoch并记录其报告的平均批次损失。
- 将模型设置为推理模式(
model.eval()),禁用梯度计算跟踪(因为验证步骤不需要)。 - 在验证数据集上进行推理并计算损失,计算平均批次损失。
- 报告训练和验证的平均损失。
- 如果这是目前看到的最佳验证损失,则将模型的状态保存到文件中。
通过运行这个循环并可视化训练进度(例如使用TensorBoard),我们可以观察损失是否单调下降,并检查是否存在过拟合(训练损失持续下降但验证损失开始上升)等问题。


总结与拓展 📚
本节课中,我们一起学习了使用PyTorch进行模型训练的核心流程。我们介绍了如何使用Dataset和DataLoader高效加载数据,如何根据任务选择合适的损失函数,以及如何使用优化器根据损失函数的梯度更新模型权重。最后,我们将这些组件整合到一个完整的训练循环中,并了解了如何监控和评估训练过程。
模型训练和优化是深入的主题。PyTorch官方文档包含了大量关于模型训练的宝贵信息,例如:
- 教程部分:涵盖迁移学习、微调、生成对抗网络(GAN)、强化学习以及用于分布式训练的
torch.distributed框架。 - API文档:提供了我们本节课所涵盖工具(优化器、损失函数、
Dataset/DataLoader类)以及更多高级功能的完整细节。


鼓励你尝试更改模型结构或优化器参数,观察在像本课示例这样的相对简单案例中,训练结果(如收敛时间、模型准确率)会如何变化。实践是掌握这些概念的最佳途径。
007:使用Captum理解模型 🧠

在本节课中,我们将学习如何使用Captum,这是一个PyTorch的模型可解释性工具集。我们将探讨Captum的核心概念,并通过一个图像分类器的实例,演示如何执行和可视化特征归因与层归因。课程最后,我们将介绍Captum Insights,一个用于创建可视化交互界面的高级工具。


概述与安装 📦

上一节我们介绍了课程目标,本节中我们来看看如何准备环境。
要运行本教程的交互式笔记本,你需要安装Python 3.6或更高版本、Flask 1.1或更高版本,以及最新版本的PyTorch、TorchVision和Captum。
以下是安装Captum的两种方法:
- 使用pip安装:
pip install captum - 使用Anaconda安装:
conda install -c pytorch captum

Captum核心概念 🔍
我们将从一个预训练的ResNet图像分类器开始,并使用Captum的工具来理解模型如何对特定输入图像做出预测。
Captum的核心抽象是归因,这是一种将模型的特定输出或活动与其输入进行定量关联的方法。
Captum主要提供三种归因方法:
- 特征归因:用于确定输入的哪些部分对模型的预测最重要。
- 层归因:用于将模型隐藏层的活动归因于模型的输入。
- 神经元归因:与层归因类似,但深入到模型中的单个神经元级别。


在本教程中,我们将重点介绍特征归因和层归因。
特征归因实践 🖼️
首先,我们来看看特征归因。归因通过归因算法来实现,这是一种将模型活动映射到输入的特定方法。
集成梯度算法
我们首先使用的算法是集成梯度。该算法通过数值方法近似模型输出相对于其输入的梯度积分,从而找到给定输入-输出对在模型中的最重要路径。
以下是创建并运行集成梯度归因的代码示例:
from captum.attr import IntegratedGradients
ig = IntegratedGradients(model)
attributions_ig = ig.attribute(input_img, target=pred_label_idx, n_steps=200)
运行此代码可能需要几分钟,因为梯度积分过程计算量较大。
得到归因数值图后,我们需要将其与原始图像关联起来进行可视化。Captum的visualization模块提供了相应工具。
以下是可视化原始图像和归因热图的代码:
from captum.attr import visualization as viz
# 显示原始图像
viz.visualize_image_attr(None, transformed_img, method="original_image", title="Original Image")
# 显示归因热图
viz.visualize_image_attr(attributions_ig, transformed_img, method="heat_map", cmap=custom_cmap, sign="positive", title="Integrated Gradients")
运行后,我们可以看到模型主要关注猫的轮廓和脸部中心区域。
遮挡算法
接下来,我们尝试另一种特征归因算法:遮挡。与基于梯度的集成梯度不同,遮挡是一种基于扰动的方法,它通过遮挡图像的部分区域来观察其对输出的影响。
以下是使用遮挡算法的代码示例:
from captum.attr import Occlusion
occlusion = Occlusion(model)
attributions_occ = occlusion.attribute(input_img,
strides = (3, 8, 8),
target=pred_label_idx,
sliding_window_shapes=(3,15, 15),
baselines=0)
我们可以使用visualize_image_attr_multiple方法同时展示多种可视化结果。
以下是同时展示原始图像、正负归因热图以及掩码图像的代码:
viz.visualize_image_attr_multiple(attributions_occ,
transformed_img,
["original_image", "heat_map", "heat_map", "masked_image"],
["all", "positive", "negative", "positive"],
titles=["Original", "Positive Attribution", "Negative Attribution", "Masked"],
fig_size=(18, 6))
结果显示,模型关注的重点区域与集成梯度算法的结果一致,主要集中在猫的轮廓和脸部中心。
层归因实践 🧩
上一节我们探讨了模型的输入输出关系,本节中我们来看看模型内部发生了什么。让我们使用层归因算法来检查其中一个隐藏层的活动。
我们将使用GradCAM算法,这是另一种为卷积网络设计的基于梯度的归因算法。它计算输出相对于指定模型层的梯度,对每个通道的梯度进行平均,并将此平均值乘以层激活值,以此作为层输出重要性的度量。
以下是使用GradCAM进行层归因的代码:
from captum.attr import LayerGradCam
layer_gradcam = LayerGradCam(model, model.layer3[1].conv2)
attributions_lgc = layer_gradcam.attribute(input_img, target=pred_label_idx)
我们可以像之前一样用热图来可视化。由于卷积层的输出通常在空间上与输入相关,我们可以通过上采样激活图并将其直接与输入进行比较来更好地观察。
以下是上采样并生成混合热图的代码:
upsamp_attr_lgc = LayerAttribution.interpolate(attributions_lgc, input_img.shape[2:])
viz.visualize_image_attr_multiple(upsamp_attr_lgc,
transformed_img,
["original_image","blended_heat_map","masked_image"],
["all","positive","positive"],
titles=["Original", "Blended Heat Map", "Masked"],
fig_size=(18, 6))
这样的可视化可以帮助你理解隐藏层如何对模型的特定输出做出贡献。
使用Captum Insights进行高级探索 🚀
Captum附带了一个名为Captum Insights的高级可视化工具,它允许你将多个可视化结果组合在一个浏览器内的小部件中,并可以配置归因算法及其参数。Captum Insights可以可视化文本、图像和任意数据。
以下是设置Captum Insights可视化器的基本代码框架:
from captum.insights import AttributionVisualizer, Batch
from captum.insights.attr_vis.features import ImageFeature
# 定义数据集
def baseline_func(input):
return input * 0
visualizer = AttributionVisualizer(
models=[model],
score_func=softmax,
classes=list(map(lambda k: k[1], imagenet_labels)),
features=[ImageFeature("Photo", baseline_transforms=baseline_func)],
dataset=dataset
)
visualizer.render() # 渲染交互式界面
在渲染出的浏览器界面中,你可以选择不同的归因算法和可视化方法,然后点击“Fetch”按钮来获取并可视化归因结果。这使你可以用最少的代码,以可视化的方式实验不同的归因方法,并理解导致模型预测(无论正确与否)的内部活动。
总结与资源 📚
本节课中,我们一起学习了如何使用Captum工具集来增强对PyTorch模型的理解。我们介绍了特征归因和层归因的核心概念,并通过集成梯度、遮挡和GradCAM等算法进行了实践。最后,我们探索了Captum Insights这一强大的交互式可视化工具。

要了解更多信息,请访问Captum AI官网以获取深入的教程、文档和API参考,或在GitHub上访问其源代码。
008:PyTorch模型的推理与生产部署 🚀

在本节课中,我们将学习如何将训练好的PyTorch模型部署到生产环境中进行推理。我们将涵盖从设置模型评估模式,到使用TorchScript进行模型转换和优化,再到通过C++或TorchServe进行高性能部署的完整流程。
模型评估模式 📊
上一节我们介绍了模型训练,本节中我们来看看推理前的关键准备步骤:设置模型为评估模式。
评估模式与训练模式相反。它会关闭在推理阶段不需要的训练相关行为。
具体来说,它会关闭自动求导(autograd)。在之前的课程中我们了解到,PyTorch张量(包括模型的学习权重)会跟踪其计算历史,以帮助快速计算反向传播梯度。这在推理时既消耗内存又占用计算资源,是不必要的。
评估模式还会改变某些具有训练特定功能的模块的行为。例如,Dropout层仅在训练时激活,评估模式下会失效。BatchNorm层在训练时会跟踪计算出的均值和方差的运行统计量,但在评估模式下此行为会被关闭。
以下是设置模型为评估模式的步骤:
- 加载模型。对于基于Python的模型,这通常涉及从磁盘加载模型的状态字典,并用它初始化模型对象。
- 在模型上调用
.eval()方法。
至此,模型已关闭训练相关行为,准备进行推理。
值得注意的是,.eval() 方法实际上是调用 .train(False) 的别名。如果你的代码中已有标志位来指示是进行训练还是推理,这可能会很有用。
一旦模型处于评估模式,你就可以开始向其发送数据批次进行推理。在后续的部署方法中,确保模型处于评估模式始终是第一步。
TorchScript:模型序列化与优化 ⚙️
上一节我们介绍了如何准备模型进行推理,本节中我们来看看如何将模型转换为TorchScript以实现序列化和性能优化。
TorchScript是Python的一个静态类型子集,用于表示PyTorch模型。它旨在被PyTorch即时编译器(JIT)使用,JIT会执行运行时优化(如算子融合和矩阵乘法批处理)以提高模型性能。TorchScript还允许你将模型和权重保存在单个文件中,并加载为一个脚本模块对象,你可以像调用原始模型一样调用它。
以下是使用TorchScript的流程:
- 像往常一样,在Python中构建、测试和训练你的模型。
- 当你想要导出模型用于生产推理时,可以使用
torch.jit.trace或torch.jit.script调用将模型转换为TorchScript。 - 之后,你可以在TorchScript模块上调用
.save()方法,将其保存为包含模型计算图和学习权重的单个文件。
即时编译器会执行你的TorchScript模型,执行运行时优化。
你也可以用C++编写自己的TorchScript自定义扩展。右侧代码展示了TorchScript的样子,但在一般情况下,你无需自己编辑它,它由你的Python代码生成。
让我们更详细地了解使用TorchScript的过程。
该过程从你在Python中构建并训练到可以部署的模型开始。下一步是将模型转换为TorchScript。有两种方法:torch.jit.script 和 torch.jit.trace。了解这两种转换技术的区别很重要。
torch.jit.script:通过直接检查你的代码并通过TorchScript编译器运行它来转换模型。它保留了控制流(如果你的前向函数有条件语句或循环,则需要这个),并且支持常见的Python数据结构。然而,由于TorchScript编译器对Python操作符支持的限制,有些模型无法使用此方法转换。torch.jit.trace:获取一个样本输入,并追踪它通过计算图的路径,以生成模型的TorchScript版本。这种方法不受torch.jit.script操作符覆盖范围的限制。但是,因为它只追踪代码中的单一路径,所以不会考虑可能导致可变或非确定性运行时行为的条件语句或其他控制流结构。
在转换模型时,也可以混合使用追踪和脚本化。有关混合使用这两种技术的说明,请参阅 torch.jit 模块的文档。
值得查看文档以了解 script 和 trace 的可选参数,其中包含用于检查TorchScript模型一致性和容差性的额外选项。
现在,我们将保存TorchScript模型。这会将你的计算图和学习权重保存在一个文件中,这意味着当你想要部署到生产环境时,无需附带包含模型类定义的Python文件。

当需要进行推理时,你可以在模型上调用 torch.jit.load,并以与Python版本模型相同的方式向其馈送输入批次。
在C++中加载TorchScript模型 🖥️
到目前为止,我所展示的一切都涉及在Python代码中操作模型,即使在将其转换为TorchScript之后。然而,在某些环境和情况下,你可能需要高吞吐量或实时推理,并希望避免Python解释器的开销。也可能你的生产环境已经围绕C++代码构建,你希望尽可能继续使用C++。
你可能在本系列前面的课程中记得,PyTorch中重要的张量计算发生在LibTorch中,这是一个经过编译和优化的C++库。PyTorch也有这个库的C++前端。这意味着你可以在C++中加载TorchScript模型并运行它,而无需Python运行时依赖。

以下是使用C++加载和运行TorchScript模型的基本步骤:
- 访问
pytorch.org并下载最新版本的LibTorch。解压包并将其放在你的构建系统可以找到的位置。 - 创建一个CMake项目。请注意,你需要使用C++14或更高版本才能使用LibTorch。
- 在C++代码中,包含
torch/script.h。这是使用TorchScript和C++的一站式头文件。 - 声明一个
torch::jit::script::Module变量。 - 使用
torch::jit::load将模型加载到内存中。 - 要获取模型的预测,使用适当的输入调用其
forward方法。这里,我们使用torch::ones创建了一个虚拟输入。在实际应用中,你需要根据模型要求传入自己的输入。 - 一旦你获得了作为张量的输出预测,就可以使用与PyTorch Python前端中你习惯的张量方法等效的C++方法来操作它们。
pytorch.org 的教程部分包含指导你设置C++项目的内容,以及多个演示C++前端各个方面的教程。
使用TorchServe进行模型服务部署 🌐
设置生产模型服务环境可能很复杂,特别是如果你需要服务多个模型、处理模型的多个版本、要求可扩展性,或者需要详细的日志记录或指标。TorchServe是PyTorch的模型服务解决方案,涵盖了所有这些需求。
TorchServe将你的模型实例加载到独立的进程空间中,并将传入的请求分发给它们。它具有许多功能,使其适用于创建基于ML的Web服务。
以下是TorchServe的主要特性:
- 数据处理器:涵盖常见用例,包括图像分类与分割、目标检测和文本分类。
- 版本管理:允许你为模型设置版本标识符,并且可以管理并同时服务一个模型的多个版本。
- 请求批处理:可以选择性地将来自多个源的输入请求进行批处理,这有时可以提高吞吐量。
- 日志与指标:具有强大的日志记录功能,并能够记录你自己的指标。
- API分离:具有独立的用于推理和模型管理的RESTful API,可以通过HTTPS进行安全保护。
我将通过使用 github.com/pytorch/serve 上 examples 文件夹中的一个示例,来演示如何设置和运行TorchServe,以此结束本视频。我们将为一个预训练的图像分类模型设置推理服务。
以下是设置和运行TorchServe的基本步骤:
- 安装TorchServe:创建一个新的Conda环境,克隆源代码仓库,并运行依赖安装脚本。根据你的系统(Linux/Mac/Windows)和是否使用GPU,安装步骤可能有所不同。你需要安装两个程序:
torchserve和torch-model-archiver。 - 准备模型存储目录:TorchServe需要一个模型存储目录,所有由TorchServe服务的模型都存储在此文件夹中。
- 创建模型存档:TorchServe期望模型被打包成模型存档(
.mar文件),其中包含模型的代码、权重以及支持模型所需的任何其他文件。使用torch-model-archiver工具创建存档,需要指定模型名称、版本、模型文件(或序列化的TorchScript文件)、权重文件、额外支持文件以及一个处理器(handler)来处理输入数据。 - 启动TorchServe:将模型存档放入模型存储目录,然后使用
torchserve命令启动服务,指定模型存储路径和要加载的模型。 - 进行推理:TorchServe启动后,你可以通过其推理API(默认端口8080)发送请求。例如,使用
curl命令发送一张图片进行图像分类。 - 管理模型:通过管理API(默认端口8081)可以查看服务器状态、管理正在服务的模型、调整工作进程数量等。例如,你可以注册/注销模型、更改模型的默认版本、调整工作进程数量。
- 停止服务:使用
torchserve --stop命令停止TorchServe。
TorchServe的GitHub仓库还提供了许多常见任务的演练和示例,包括特定的服务器管理任务、设置HTTPS、编写自定义处理器等。
总结 📝
本节课中我们一起学习了将PyTorch模型部署到生产环境进行推理的完整流程。
我们首先学习了如何将模型设置为评估模式,这是关闭Dropout、BatchNorm统计量跟踪和自动求导等训练行为的关键步骤。
接着,我们探讨了使用TorchScript将模型序列化和优化的方法,包括 torch.jit.script 和 torch.jit.trace 两种转换方式的区别与适用场景。
然后,我们了解了如何在C++环境中加载和运行TorchScript模型,以实现高性能、无Python依赖的推理。


最后,我们介绍了使用TorchServe这一完整的模型服务解决方案,它提供了模型版本管理、请求批处理、日志记录和易于使用的REST API等功能,极大地简化了生产环境中的模型部署与管理。



所有这些内容以及更多细节,都可以在 pytorch.org 的文档和教程中找到。

浙公网安备 33010602011771号