精通-PyTorch-第二版-三-

315 阅读1小时+

精通 PyTorch 第二版(三)

原文:zh.annas-archive.org/md5/ed4780a817b954d8a29cd07c34f589a6

译者:飞龙

协议:CC BY-NC-SA 4.0

第十三章:将 PyTorch 模型操作化为生产环境

加入我们的书籍社区 Discord

packt.link/EarlyAccessCommunity

img

到目前为止,在本书中,我们已经介绍了如何使用 PyTorch 训练和测试不同类型的机器学习模型。我们首先回顾了 PyTorch 的基本元素,使我们能够高效地处理深度学习任务。然后,我们探索了使用 PyTorch 编写的广泛的深度学习模型架构和应用程序。

本章中,我们将重点讨论将这些模型投入生产环境的过程。但这意味着什么呢?基本上,我们将讨论不同的方式,将经过训练和测试的模型(对象)带入一个独立的环境,使其能够对输入数据进行预测或推断。这就是所谓的模型生产化,因为模型正在部署到生产系统中。

我们将从讨论一些常见的方法开始,这些方法可以用来在生产环境中服务 PyTorch 模型,从定义简单的模型推断函数开始,一直到使用模型微服务。然后,我们将介绍 TorchServe,这是一个可扩展的 PyTorch 模型服务框架,由 AWS 和 Facebook 联合开发。

然后,我们将深入探讨使用TorchScript导出 PyTorch 模型的世界,通过序列化使我们的模型独立于 Python 生态系统,从而可以在基于C++的环境中加载,例如。我们还将超越 Torch 框架和 Python 生态系统,探索ONNX - 一种用于机器学习模型的开放源代码通用格式,这将帮助我们将 PyTorch 训练的模型导出到非 PyTorch 和非 Python 环境中。

最后,我们将简要讨论如何使用 PyTorch 在一些著名的云平台(如亚马逊网络服务AWS)、Google CloudMicrosoft Azure)上提供模型服务。

在本章中,我们将使用我们在《第一章,使用 PyTorch 进行深度学习概述》中训练的手写数字图像分类卷积神经网络CNN)模型作为参考。我们将演示如何使用本章讨论的不同方法部署和导出该训练过的模型。

本章分为以下几个部分:

  • PyTorch 中的模型服务

  • 使用 TorchServe 提供 PyTorch 模型服务

  • 使用 TorchScript 和 ONNX 导出通用 PyTorch 模型

  • 在云中提供 PyTorch 模型服务

PyTorch 中的模型服务

在本节中,我们将开始构建一个简单的 PyTorch 推断管道,该管道可以在给定一些输入数据和先前训练和保存的 PyTorch 模型的位置的情况下进行预测。之后,我们将把这个推断管道放在一个模型服务器上,该服务器可以监听传入的数据请求并返回预测结果。最后,我们将从开发模型服务器进阶到使用 Docker 创建模型微服务。

创建 PyTorch 模型推断管道

我们将继续在第一章,PyTorch 使用深度学习的概述中构建的手写数字图像分类 CNN 模型上进行工作,使用MNIST数据集。利用这个训练过的模型,我们将构建一个推断管道,能够为给定的手写数字输入图像预测 0 到 9 之间的数字。

对于构建和训练模型的过程,请参考第一章,PyTorch 使用深度学习的概述中的训练神经网络使用 PyTorch部分。关于这个练习的完整代码,您可以参考我们的 github 仓库[13.1]。

保存和加载训练好的模型

在本节中,我们将演示如何有效地加载保存的预训练 PyTorch 模型,这些模型稍后将用于处理请求。

因此,使用来自第一章,PyTorch 使用深度学习的概述的笔记本代码,我们训练了一个模型,并对测试数据样本进行了评估。但接下来呢?在现实生活中,我们希望关闭这个笔记本,并且以后仍然能够使用我们辛苦训练过的模型来推断手写数字图像。这就是模型服务概念的应用之处。

从这里开始,我们将处于一个位置,可以在一个单独的 Jupyter 笔记本中使用先前训练过的模型,而无需进行任何(重新)训练。关键的下一步是将模型对象保存到一个文件中,稍后可以恢复/反序列化。PyTorch 提供了两种主要的方法来实现这一点:

  • 不推荐的方式是保存整个模型对象,如下所示:
torch.save(model, PATH_TO_MODEL)

然后,稍后可以按如下方式读取保存的模型:

model = torch.load(PATH_TO_MODEL)

尽管这种方法看起来最直接,但在某些情况下可能会有问题。这是因为我们不仅保存了模型参数,还保存了我们源代码中使用的模型类和目录结构。如果以后我们的类签名或目录结构发生变化,加载模型可能会以无法修复的方式失败。

  • 第二种更推荐的方法是仅保存模型参数,如下所示:
torch.save(model.state_dict(), PATH_TO_MODEL)

当我们需要恢复模型时,首先我们实例化一个空模型对象,然后将模型参数加载到该模型对象中,如下所示:

model = ConvNet()
model.load_state_dict(torch.load(PATH_TO_MODEL))

我们将使用更推荐的方式保存模型,如下代码所示:

PATH_TO_MODEL = "./convnet.pth"
torch.save(model.state_dict(), PATH_TO_MODEL)

convnet.pth文件本质上是一个包含模型参数的 pickle 文件。

此时,我们可以安全地关闭我们正在工作的笔记本,并打开另一个可以在我们的 github 仓库[13.2]中找到的笔记本:

  1. 作为第一步,我们再次需要导入库:
import torch
  1. 接下来,我们需要再次实例化一个空的 CNN 模型。理想情况下,模型定义可以写在一个 Python 脚本中(比如 cnn_model.py),然后我们只需要写如下代码:
from cnn_model import ConvNet
model = ConvNet()

然而,在这个练习中,由于我们正在使用 Jupyter 笔记本,我们将重写模型定义,然后像这样实例化它:

class ConvNet(nn.Module):
    def __init__(self):
       …
    def forward(self, x):
        …
model = ConvNet()
  1. 现在,我们可以将保存的模型参数恢复到实例化的模型对象中,方法如下:
PATH_TO_MODEL = "./convnet.pth"
model.load_state_dict(torch.load(PATH_TO_MODEL, map_location="cpu"))

您将看到以下输出:

图 13 .1 – 模型参数加载

图 13 .1 – 模型参数加载

这基本上意味着参数加载成功。也就是说,我们实例化的模型与保存并现在正在恢复其参数的模型具有相同的结构。我们指定在 CPU 设备上加载模型,而不是 GPU(CUDA)。

  1. 最后,我们希望指定不希望更新或更改加载模型的参数值,并将使用以下代码行来执行:
model.eval()

这应该给出以下输出:

图 13 .2 – 加载模型并进行评估模式

图 13 .2 – 加载模型并进行评估模式

这再次验证我们确实使用与训练过的相同模型(架构)进行工作。

构建推断流水线

在前一节成功在新环境(笔记本)中加载了预训练模型后,我们现在将构建我们的模型推断流水线,并用它来运行模型预测:

  1. 此时,我们已经完全恢复了之前训练过的模型对象。现在,我们将加载一张图像,然后可以使用以下代码对其进行模型预测:
image = Image.open("./digit_image.jpg")

图像文件应该在练习文件夹中,并且如下所示:

图 13 .3 – 模型推断输入图像

图 13 .3 – 模型推断输入图像

在本练习中不必使用此特定图像。您可以使用任何图像来检查模型对其的反应。

  1. 在任何推断流水线中,核心有三个主要组件:(a) 数据预处理组件,(b) 模型推断(神经网络的前向传播),以及 (c) 后处理步骤。

我们将从第一部分开始,通过定义一个函数,该函数接收图像并将其转换为作为模型输入的张量:

def image_to_tensor(image):
    gray_image = transforms.functional.to_grayscale(image)
    resized_image = transforms.functional.resize(gray_image, (28, 28))
    input_image_tensor = transforms.functional.to_tensor(resized_image)
    input_image_tensor_norm = transforms.functional.normalize(input_image_tensor, (0.1302,), (0.3069,))
    return input_image_tensor_norm

这可以看作是以下一系列步骤的一部分:

  1. 首先,RGB 图像转换为灰度图像。

  2. 然后,将图像调整为 28x28 像素的图像,因为这是模型训练时使用的图像尺寸。

  3. 接下来,将图像数组转换为 PyTorch 张量。

  4. 最后,对张量中的像素值进行归一化,归一化使用与模型训练时相同的均值和标准差值。

定义了此函数后,我们调用它将加载的图像转换为张量:

input_tensor = image_to_tensor(image)
  1. 接下来,我们定义模型推断功能。这是模型接收张量作为输入并输出预测的地方。在这种情况下,预测将是 0 到 9 之间的任何数字,输入张量将是输入图像的张量化形式:
def run_model(input_tensor):
    model_input = input_tensor.unsqueeze(0)
    with torch.no_grad():
        model_output = model(model_input)[0]
    model_prediction = model_output.detach().numpy().argmax()
    return model_prediction

model_output包含模型的原始预测,其中包含每个图像预测的列表。因为我们只有一个输入图像,所以这个预测列表只有一个在索引0处的条目。索引0处的原始预测本质上是一个张量,其中有 10 个数字 0 到 9 的概率值,按顺序排列。这个张量被转换为一个numpy数组,最后我们选择具有最高概率的数字。

  1. 现在我们可以使用这个函数生成我们的模型预测。下面的代码使用第 3 步run_model模型推断函数来为给定的输入数据input_tensor生成模型预测:
output = run_model(input_tensor)
print(output)
print(type(output))

这应该会输出以下内容:

图 13 .4 – 模型推断输出

图 13 .4 – 模型推断输出

如前面的截图所示,模型输出为一个numpy整数。基于图 13 .3中显示的图像,模型输出似乎相当正确。

  1. 除了仅输出模型预测外,我们还可以编写调试函数来更深入地了解诸如原始预测概率等指标,如下面的代码片段所示:
def debug_model(input_tensor):
    model_input = input_tensor.unsqueeze(0)
    with torch.no_grad():
        model_output = model(model_input)[0]
    model_prediction = model_output.detach().numpy()
    return np.exp(model_prediction)

这个函数与run_model函数完全相同,只是它返回每个数字的原始概率列表。由于模型最终层使用了log_softmax层,所以模型原始返回的是 softmax 输出的对数(参考本练习的第 2 步)。

因此,我们需要对这些数字进行指数运算,以返回 softmax 输出,这些输出等同于模型预测的概率。使用这个调试函数,我们可以更详细地查看模型的表现,比如概率分布是否平坦或者是否有明显的峰值:

print(debug_model(input_tensor))

这应该会产生类似以下的输出:

图 13 .5 – 模型推断调试输出

图 13 .5 – 模型推断调试输出

我们可以看到列表中第三个概率远远最高,对应数字 2。

  1. 最后,我们将对模型预测进行后处理,以便其他应用程序可以使用。在我们的情况下,我们将仅将模型预测的数字从整数类型转换为字符串类型。

在其他场景中,后处理步骤可能会更复杂,比如语音识别,我们可能希望通过平滑处理、移除异常值等方式处理输出波形:

def post_process(output):
    return str(output)

因为字符串是可序列化格式,这使得模型预测可以在服务器和应用程序之间轻松传递。我们可以检查我们的最终后处理数据是否符合预期:

final_output = post_process(output)
print(final_output)
print(type(final_output))

这应该会为您提供以下输出:

图 13 .6 – 后处理模型预测

图 13 .6 – 后处理模型预测

如预期,现在输出的类型为字符串。

这结束了我们加载保存的模型架构,恢复其训练权重,并使用加载的模型为样本输入数据(一张图像)生成预测的练习。我们加载了一个样本图像,对其进行预处理以将其转换为 PyTorch 张量,将其作为输入传递给模型以获取模型预测,并对预测进行后处理以生成最终输出。

这是朝着为经过训练的模型提供明确定义的输入和输出接口的方向迈出的一步。在这个练习中,输入是一个外部提供的图像文件,输出是一个包含 0 到 9 之间数字的生成字符串。这样的系统可以通过复制并粘贴提供的代码嵌入到任何需要手写数字转换功能的应用程序中。

在接下来的部分,我们将深入探讨模型服务的更高级别,我们的目标是构建一个可以被任何应用程序交互使用的系统,以使用数字化功能,而无需复制和粘贴任何代码。

构建一个基本的模型服务器

到目前为止,我们已经构建了一个模型推断管道,其中包含独立执行预训练模型预测所需的所有代码。在这里,我们将致力于构建我们的第一个模型服务器,这本质上是一个托管模型推断管道的机器,通过接口主动监听任何传入的输入数据,并通过接口对任何输入数据输出模型预测。

使用 Flask 编写一个基本应用

为了开发我们的服务器,我们将使用一个流行的 Python 库 – Flask [13.3]。Flask将使我们能够用几行代码构建我们的模型服务器。关于该库如何工作的一个很好的示例如下所示:

from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
    return 'Hello, World!'
if __name__ == '__main__':
    app.run(host='localhost', port=8890)

假设我们将这个 Python 脚本保存为example.py并从终端运行它:

python example.py

它将在终端显示以下输出:

图 13 .7 – Flask 示例应用启动

图 13 .7 – Flask 示例应用启动

基本上,它将启动一个 Flask 服务器,用于提供名为example的应用程序。让我们打开一个浏览器,并转到以下 URL:

http://localhost:8890/

它将在浏览器中产生以下输出:

图 13 .8 – Flask 示例应用测试

图 13 .8 – Flask 示例应用测试

本质上,Flask 服务器在 IP 地址为0.0.0.0(localhost)的端口号8890上监听端点/。当我们在浏览器搜索栏中输入localhost:8890/并按Enter时,该服务器将接收到一个请求。然后,服务器运行hello_world函数,该函数根据example.py中提供的函数定义返回字符串Hello, World!

使用 Flask 构建我们的模型服务器

使用前面部分演示的运行 Flask 服务器的原则,我们现在将使用前一部分构建的模型推理管道来创建我们的第一个模型服务器。 在练习结束时,我们将启动服务器以侦听传入请求(图像数据输入)。

此外,我们还将编写另一个 Python 脚本,通过向此服务器发送图 13 .3中显示的示例图像,向此服务器发出请求。 Flask 服务器将对该图像进行模型推理并输出后处理的预测结果。

此练习的完整代码在 GitHub 上可用,包括 Flask 服务器代码 [13.4] 和客户端(请求生成器)代码 [13.5]。

为 Flask 服务设置模型推理

在本节中,我们将加载预训练模型并编写模型推理管道代码:

  1. 首先,我们将构建 Flask 服务器。 为此,我们再次开始导入必要的库:
from flask import Flask, request
import torch

除了numpyjson等其他基本库外,flasktorch对于这个任务都是至关重要的。

  1. 接下来,我们需要定义模型类(架构):
class ConvNet(nn.Module):
    def __init__(self):
    def forward(self, x):
  1. 现在我们已经定义了空模型类,我们可以实例化一个模型对象,并将预训练模型参数加载到该模型对象中,方法如下:
model = ConvNet()
PATH_TO_MODEL = "./convnet.pth"
model.load_state_dict(torch.load(PATH_TO_MODEL, map_location="cpu"))
model.eval()
  1. 我们将重复使用在“构建推理管道”部分“步骤 3”中定义的精确run_model函数:
def run_model(input_tensor):
    …
    return model_prediction

作为提醒,此函数接受张量化的输入图像并输出模型预测,即介于 0 到 9 之间的任何数字。

  1. 接下来,我们将重复使用在“构建推理管道”部分的“第 6 步”中定义的精确post_process函数:
def post_process(output):
    return str(output)

这将从run_model函数的整数输出转换为字符串。

构建一个用于提供模型的 Flask 应用

在上一节中建立了推理管道之后,我们现在将构建我们自己的 Flask 应用并使用它来提供加载的模型服务:

  1. 我们将如下代码示例化我们的 Flask 应用:
app = Flask(__name__)

这将创建一个与 Python 脚本同名的 Flask 应用,这在我们的案例中是server(.py)

  1. 这是关键步骤,我们将在 Flask 服务器中定义端点功能。 我们将暴露/test端点并定义在服务器上进行POST请求时发生的事件如下:
@app.route("/test", methods=["POST"])
def test():
    data = request.files['data'].read()
    md = json.load(request.files['metadata'])
    input_array = np.frombuffer(data, dtype=np.float32)
    input_image_tensor = torch.from_numpy(input_array).view(md["dims"])
    output = run_model(input_image_tensor)
    final_output = post_process(output)
    return final_output

让我们逐步进行这些步骤:

  1. 首先,在函数下面定义一个装饰器test。 此装饰器告诉 Flask 应用程序,每当有人向/test端点发出POST请求时,运行此函数。

  2. 接下来,我们将定义test函数内部发生的确切事件。 首先,我们从POST请求中读取数据和元数据。 因为数据是序列化形式,所以我们需要将其转换为数值格式 - 我们将其转换为numpy数组。 从numpy数组中,我们迅速将其转换为 PyTorch 张量。

  3. 接下来,我们使用元数据中提供的图像尺寸来重塑张量。

  4. 最后,我们对之前加载的模型执行前向传播。这会给我们模型的预测结果,然后经过后处理并由我们的测试函数返回。

  5. 我们已经准备好启动我们的 Flask 应用程序所需的所有组件。我们将这最后两行添加到我们的server.py Python 脚本中:

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8890)

这表明 Flask 服务器将托管在 IP 地址0.0.0.0(也称为localhost)和端口号8890。我们现在可以保存 Python 脚本,并在新的终端窗口中执行以下操作:

python server.py

这将运行前面步骤中编写的整个脚本,并将看到以下输出:

图 13 .9 - Flask 服务器启动

图 13 .9 - Flask 服务器启动

这看起来类似于图 13 .7 中演示的示例。唯一的区别是应用程序名称。

使用 Flask 服务器运行预测。

我们已成功启动了我们的模型服务器,它正在积极监听请求。现在让我们继续处理发送请求的工作:

  1. 接下来的几步我们将编写一个单独的 Python 脚本来完成这项工作。我们首先导入库:
import requests
from PIL import Image
from torchvision import transforms

requests库将帮助我们向 Flask 服务器发起实际的POST请求。Image帮助我们读取样本输入图像文件,而transforms则帮助我们预处理输入图像数组。

  1. 接下来,我们读取一个图像文件:
image = Image.open("./digit_image.jpg")

这里读取的图像是 RGB 图像,可能具有任意尺寸(不一定是模型期望的 28x28 尺寸)。

  1. 现在我们定义一个预处理函数,将读取的图像转换为模型可读取的格式:
def image_to_tensor(image):
    gray_image = transforms.functional.to_grayscale(image)
    resized_image = transforms.functional.resize(gray_image, (28, 28))
    input_image_tensor = transforms.functional.to_tensor(resized_image)
    input_image_tensor_norm = transforms.functional.normalize(input_image_tensor, (0.1302,), (0.3069,))
    return input_image_tensor_norm

定义了函数之后,我们可以执行它:

image_tensor = image_to_tensor(image)

image_tensor是我们需要发送给 Flask 服务器的输入数据。

  1. 现在让我们将数据打包在一起发送过去。我们希望发送图像的像素值以及图像的形状(28x28),这样接收端的 Flask 服务器就知道如何将像素值流重构为图像:
dimensions = io.StringIO(json.dumps({'dims': list(image_tensor.shape)}))
data = io.BytesIO(bytearray(image_tensor.numpy()))

我们将张量的形状转换为字符串,并将图像数组转换为字节,使其可序列化。

  1. 这是客户端代码中最关键的一步。这是我们实际发起POST请求的地方:
r = requests.post('http://localhost:8890/test',
                  files={'metadata': dimensions,                          'data' : data})

使用requests库,我们在 URLlocalhost:8890/test发起POST请求。这是 Flask 服务器监听请求的地方。我们将实际图像数据(以字节形式)和元数据(以字符串形式)发送为字典的形式。

  1. 在上述代码中,r变量将接收来自 Flask 服务器请求的响应。这个响应应该包含经过后处理的模型预测结果。我们现在读取该输出:
response = json.loads(r.content)

response变量实际上将包含 Flask 服务器输出的内容,这是一个介于 0 和 9 之间的数字字符串。

  1. 我们可以打印响应以确保一切正常:
print("Predicted digit :", response)

此时,我们可以将此 Python 脚本保存为make_request.py,并在终端中执行以下命令:

python make_request.py

这应该输出以下内容:

图 13 .10 – Flask 服务器响应

图 13 .10 – Flask 服务器响应

基于输入图像(见图 13 .3),响应看起来相当正确。这结束了我们当前的练习。

因此,我们已成功构建了一个独立的模型服务器,可以为手写数字图像进行预测。同样的步骤集可以轻松扩展到任何其他机器学习模型,因此使用 PyTorch 和 Flask 创建机器学习应用程序的可能性是无限的。

到目前为止,我们已经不仅仅是编写推理函数,而是创建了可以远程托管并在网络上进行预测的模型服务器。在我们接下来和最后的模型服务冒险中,我们将再进一步。您可能已经注意到,在遵循前两个练习的步骤时,有一些固有的依赖需要考虑。我们需要安装某些库,保存和加载模型在特定位置,读取图像数据等等。所有这些手动步骤都会减慢模型服务器的开发速度。

接下来,我们将致力于创建一个可以通过一条命令快速启动并在多台机器上复制的模型微服务。

创建模型微服务

想象一下,您对训练机器学习模型一无所知,但希望使用已经训练好的模型,而不必涉及任何 PyTorch 代码。这就是诸如机器学习模型微服务 [13.6] 这样的范式发挥作用的地方。

可以将机器学习模型微服务看作是一个黑盒子,您向其发送输入数据,它向您发送预测。而且,仅需几行代码就可以在给定的机器上快速启动这个黑盒子。最好的部分是它可以轻松扩展。您可以通过使用更大的机器(更多内存、更多处理能力)来垂直扩展微服务,也可以通过在多台机器上复制微服务来水平扩展。

我们如何部署一个机器学习模型作为微服务?多亏了在前面的练习中使用 Flask 和 PyTorch 所做的工作,我们已经领先了几步。我们已经使用 Flask 构建了一个独立的模型服务器。

在本节中,我们将这个想法推向前进,并构建一个独立的模型服务环境,使用Docker。 Docker 有助于容器化软件,这基本上意味着它帮助虚拟化整个操作系统OS),包括软件库、配置文件,甚至数据文件。

注意

Docker 本身是一个广泛讨论的话题。然而,由于本书专注于 PyTorch,我们只会涵盖 Docker 的基本概念和用法,以适应我们有限的目的。如果您有兴趣进一步了解 Docker,他们自己的文档是一个很好的起点 [13.7] 。

在我们的案例中,到目前为止,在构建我们的模型服务器时,我们已经使用了以下库:

  • Python

  • PyTorch

  • Pillow(用于图像 I/O)

  • Flask

此外,我们使用了以下数据文件:

  • 预训练模型检查点文件 (convnet.pth)

我们不得不通过安装库并将文件放置在当前工作目录中手动安排这些依赖关系。如果我们需要在新机器上重新执行所有操作会怎样?我们将不得不手动安装库并再次复制粘贴文件。这种工作方式既不高效,也不可靠,例如,我们可能会在不同的机器上安装不同版本的库。

为了解决这个问题,我们想创建一个可以在各个机器上一致重复的操作系统级蓝图。这就是 Docker 发挥作用的地方。Docker 让我们可以创建一个 Docker 镜像的形式来实现这个蓝图。这个镜像可以在任何空白的机器上构建,不需要假设预先安装了 Python 库或已经可用的模型。

让我们实际上使用 Docker 为我们的数字分类模型创建这样的蓝图。作为一个练习,我们将从基于 Flask 的独立模型服务器转向基于 Docker 的模型微服务。在深入练习之前,您需要安装 Docker [13.8]:

  1. 首先,我们需要列出 Flask 模型服务器的 Python 库需求。需求(及其版本)如下:
torch==1.5.0
torchvision==0.5.0
Pillow==6.2.2
Flask==1.1.1

作为一般惯例,我们将把这个列表保存为一个文本文件 – requirements.txt。这个文件也可以在我们的 GitHub 仓库中找到 [13.9]。这个列表将有助于在任何给定的环境中一致地安装这些库。

  1. 接下来,我们直接进入蓝图,用 Docker 术语来说,这将是 DockerfileDockerfile 是一个脚本,实质上是一系列的指令。运行这个 Dockerfile 的机器需要执行文件中列出的指令。这会生成一个 Docker 镜像,这个过程称为 构建镜像

在这里,一个 镜像 是一个系统快照,可以在任何机器上执行,只要该机器具备最低必要的硬件资源(例如,仅安装 PyTorch 就需要多个 GB 的磁盘空间)。

让我们看看我们的Dockerfile并逐步理解它的作用。完整的Dockerfile代码可在我们的 GitHub 仓库中找到 [13.10]。

  1. FROM 关键字指示 Docker 获取一个预先安装了 python 3.8 的标准 Linux 操作系统:
FROM python:3.8-slim

这确保我们将安装 Python。

  1. 接下来,安装 wget,这是一个 Unix 命令,有助于通过命令行下载互联网资源:
RUN apt-get -q update && apt-get -q install -y wget

&& 符号表示在符号前后写的命令是顺序执行的。

  1. 在这里,我们正在将两个文件从我们的本地开发环境复制到这个虚拟环境中:
COPY ./server.py ./
COPY ./requirements.txt ./

我们复制了在 步骤 1 中讨论过的 requirements 文件,以及在前一个练习中工作过的 Flask 模型服务器代码。

  1. 接下来,我们下载预训练的 PyTorch 模型检查点文件:
RUN wget -q https://github.com/arj7192/MasteringPyTorchV2/raw/main/Chapter13/convnet.pth 

这是我们在本章的保存和加载训练好的模型部分中保存的同一模型检查点文件。

  1. 在这里,我们正在安装requirements.txt下列出的所有相关库:
RUN pip install -r requirements.txt

这个txt文件是我们在步骤 1下编写的文件。

  1. 接下来,我们给 Docker 客户端赋予root访问权限:
USER root

这一步在本练习中非常重要,因为它确保客户端具有执行所有必要操作的凭据,例如在磁盘上保存模型推断日志。

注意

总体而言,建议根据数据安全的最小特权原则[13.11],不要赋予客户端 root 权限。

  1. 最后,我们指定在执行了所有前面的步骤之后,Docker 应该执行python server.py命令:
ENTRYPOINT ["python", "server.py"]

这将确保在虚拟机中启动一个 Flask 模型服务器。

  1. 现在让我们运行这个 Dockerfile。换句话说,让我们使用步骤 2中的 Dockerfile 构建一个 Docker 镜像。在当前工作目录中,只需在命令行上运行以下命令:
docker build -t digit_recognizer .

我们正在为我们的 Docker 镜像分配一个名为digit_recognizer的标签。这应该输出以下内容:

图 13 .11 – 构建 Docker 镜像

图 13 .11 – 构建 Docker 镜像

图 13 .11显示了步骤 2中提到的步骤的顺序执行。根据您的互联网连接速度,此步骤的运行时间可能会有所不同,因为它需要下载整个 PyTorch 库等内容以构建镜像。

  1. 在这个阶段,我们已经有一个名为digit_recognizer的 Docker 镜像。我们已经准备好在任何机器上部署这个镜像。为了暂时在您自己的机器上部署这个镜像,只需运行以下命令:
docker run -p 8890:8890 digit_recognizer

使用这个命令,我们本质上是在我们的机器内部启动一个虚拟机,使用digit_recognizer Docker 镜像。因为我们原来的 Flask 模型服务器设计为监听端口8890,我们使用-p参数将我们实际机器的端口8890转发到虚拟机的端口8890。运行这个命令应该输出以下内容:

图 13 .12 – 运行 Docker 实例

图 13 .12 – 运行 Docker 实例

前面的截图与上一练习中的图 13 .9非常相似,这并不奇怪,因为 Docker 实例正在运行我们之前手动运行的相同 Flask 模型服务器。

  1. 现在,我们可以测试我们的 Docker 化 Flask 模型服务器(模型微服务)是否按预期工作,方法是使用前一练习中使用的make_request.py文件向我们的模型发送预测请求。从当前本地工作目录,简单执行以下命令:
python make_request.py

这应该输出以下内容:

图 13 .13 – 微服务模型预测

图 13 .13 – 微服务模型预测

微服务似乎在发挥作用,因此我们成功地使用 Python、PyTorch、Flask 和 Docker 构建和测试了自己的机器学习模型微服务。

  1. 完成前面的步骤后,可以按照 第 4 步 中指示的方式,通过按下 Ctrl+C 关闭启动的 Docker 实例(见 图 13 .12)。一旦运行的 Docker 实例停止,可以通过运行以下命令删除该实例:
docker rm $(docker ps -a -q | head -1)

此命令基本上移除了最近不活跃的 Docker 实例,也就是我们刚刚停止的 Docker 实例。

  1. 最后,还可以通过运行以下命令删除我们在 第 3 步 下构建的 Docker 镜像:
docker rmi $(docker images -q "digit_recognizer")

这将基本上删除已标记为 digit_recognizer 标签的镜像。

这结束了我们为 PyTorch 编写模型服务的部分。我们首先设计了一个本地模型推理系统。然后,我们将这个推理系统包装成基于 Flask 的模型服务器,创建了一个独立的模型服务系统。

最后,我们使用基于 Flask 的模型服务器放置在 Docker 容器内,实质上创建了一个模型服务微服务。使用本节讨论的理论和练习,您应该能够开始在不同的用例、系统配置和环境中托管/提供您训练好的模型。

在接下来的部分中,我们将继续与模型服务主题保持一致,但会讨论一种特定的工具,该工具正是为了精确为 PyTorch 模型提供服务而开发的:TorchServe。我们还将进行一个快速练习,演示如何使用这个工具。

使用 TorchServe 服务 PyTorch 模型

TorchServe 是一个专用的 PyTorch 模型服务框架,于 2020 年 4 月发布。使用 TorchServe 提供的功能,我们可以同时为多个模型提供服务,具有低预测延迟,并且无需编写大量自定义代码。此外,TorchServe 还提供模型版本控制、指标监控以及数据预处理和后处理等功能。

显然,TorchServe 是一个更高级的模型服务替代方案,比我们在前一节中开发的模型微服务更为先进。然而,创建定制的模型微服务仍然被证明是解决复杂机器学习流水线问题的强大解决方案(这比我们想象的更常见)。

在本节中,我们将继续使用我们的手写数字分类模型,并演示如何使用 TorchServe 进行服务。阅读本节后,您应该能够开始使用 TorchServe 并进一步利用其完整的功能集。

安装 TorchServe

在开始练习之前,我们需要安装 Java 11 SDK 作为先决条件。对于 Linux 操作系统,请运行以下命令:

sudo apt-get install openjdk-11-jdk

对于 macOS,我们需要在命令行上运行以下命令:

brew tap AdoptOpenJDK/openjdk
brew      install --cask adoptopenjdk11

然后,我们需要运行以下命令安装 torchserve

pip install torchserve==0.6.0 torch-model-archiver==0.6.0

有关详细的安装说明,请参阅 torchserve 文档 [13.12]。

注意,我们还安装了一个名为torch-model-archiver的库 [13.13]。这个归档工具旨在创建一个模型文件,该文件将包含模型参数以及模型架构定义,以独立序列化格式存储为.mar文件。

启动和使用 TorchServe 服务器。

现在我们已经安装了所有需要的东西,可以开始组合先前练习中的现有代码来使用 TorchServe 提供我们的模型。以下是我们将通过练习步骤进行的几个步骤:

  1. 首先,我们将现有的模型架构代码放入一个名为convnet.py的模型文件中:
==========================convnet.py===========================
import torch
import torch.nn as nn
import torch.nn.functional as F
class ConvNet(nn.Module):
    def __init__(self):
        …
    def forward(self, x):
        …

我们将需要将这个模型文件作为torch-model-archiver的输入之一,以产生一个统一的.mar文件。您可以在我们的 GitHub 仓库 [13.14] 中找到完整的模型文件。

记得我们曾讨论过模型推断流程的三个部分:数据预处理、模型预测和后处理。TorchServe 提供了处理程序来处理流行的机器学习任务的预处理和后处理部分:image_classifierimage_segmenterobject_detectortext_classifier

由于在撰写本书时 TorchServe 正在积极开发中,因此这个列表可能会在未来增加。

  1. 对于我们的任务,我们将创建一个自定义的图像处理程序,它是从默认的Image_classifier处理程序继承而来。我们选择创建一个自定义处理程序,因为与处理彩色(RGB)图像的常规图像分类模型不同,我们的模型处理特定尺寸(28x28 像素)的灰度图像。以下是我们的自定义处理程序的代码,您也可以在我们的 GitHub 仓库 [13.15] 中找到:
========================convnet_handler.py=======================
from torchvision import transforms
from ts.torch_handler.image_classifier import ImageClassifier
class ConvNetClassifier(ImageClassifier):
    image_processing = transforms.Compose([
        transforms.Grayscale(), transforms.Resize((28, 28)),
        transforms.ToTensor(),  transforms.Normalize((0.1302,), (0.3069,))])
    def postprocess(self, output):
        return output.argmax(1).tolist()

首先,我们导入了image_classifier默认处理程序,它将提供大部分基本的图像分类推断流程处理能力。接下来,我们继承ImageClassifer处理程序类来定义我们的自定义ConvNetClassifier处理程序类。

这里有两个自定义代码块:

  1. 数据预处理步骤,我们将数据应用一系列变换,正如我们在“构建推断流程”部分的“步骤 3”中所做的那样。

  2. 后处理步骤,在postprocess方法下定义,我们从所有类别预测概率的列表中提取预测的类标签。

  3. 在本章的“保存和加载训练模型”部分我们已经生成了一个convnet.pth文件用于创建模型推断流程。使用convnet.pyconvnet_handler.pyconvnet.pth,我们最终可以通过运行以下命令使用torch-model-archiver来创建.mar文件:

torch-model-archiver --model-name convnet --version 1.0 --model-file ./convnet.py --serialized-file ./convnet.pth --handler  ./convnet_handler.py

这个命令应该会在当前工作目录写入一个convnet.mar文件。我们指定了一个model_name参数,它为.mar文件命名。我们指定了一个version参数,在同时处理多个模型变体时有助于模型版本控制。

我们已经找到了我们的 convnet.py(用于模型架构)、convnet.pth(用于模型权重)和 convnet_handler.py(用于前处理和后处理)文件的位置,分别使用了 model_fileserialzed_filehandler 参数。

  1. 接下来,我们需要在当前工作目录中创建一个新目录,并将第 3 步 中创建的 convnet.mar 文件移动到该目录中,通过以下命令完成:
mkdir model_store
mv convnet.mar model_store/

我们必须这样做来遵循 TorchServe 框架的设计要求。

  1. 最后,我们可以使用 TorchServe 启动我们的模型服务器。在命令行上,只需运行以下命令:
torchserve --start --ncs --model-store model_store --models convnet.mar

这将静默地启动模型推断服务器,并在屏幕上显示一些日志,包括以下内容:

图 13 .14 – TorchServe 启动输出

图 13 .14 – TorchServe 启动输出

正如你所见,TorchServe 会检查机器上可用的设备及其他详细信息。它为推断管理指标分配了三个独立的 URL。为了检查启动的服务器是否确实在为我们的模型提供服务,我们可以使用以下命令来 ping 管理服务器:

curl http://localhost:8081/models

这应该输出以下内容:

图 13 .15 – TorchServe 服务模型

图 13 .15 – TorchServe 服务模型

这验证了 TorchServe 服务器确实在托管模型。

  1. 最后,我们可以通过发送推断请求来测试我们的 TorchServe 模型服务器。这一次,我们不需要编写 Python 脚本,因为处理程序已经处理任何输入图像文件。因此,我们可以通过运行以下命令,直接使用 digit_image.jpg 示例图像文件进行请求:
curl http://127.0.0.1:8080/predictions/convnet -T ./digit_image.jpg

这应该在终端输出 2,这确实是正确的预测,正如图 13 .3 所示。

  1. 最后,一旦我们完成了对模型服务器的使用,可以通过在命令行上运行以下命令来停止它:
torchserve --stop

这结束了我们如何使用 TorchServe 快速启动自己的 PyTorch 模型服务器并用其进行预测的练习。这里还有很多内容需要挖掘,比如模型监控(指标)、日志记录、版本控制、性能基准测试等 [13.16] 。TorchServe 的网站是深入研究这些高级主题的好地方。

完成本节后,您将能够使用 TorchServe 来为自己的模型提供服务。我鼓励您为自己的用例编写自定义处理程序,探索各种 TorchServe 配置设置 [13.17] ,并尝试 TorchServe 的其他高级功能 [13.18] 。

注意

TorchServe 在不断发展中,充满了许多潜力。我的建议是密切关注 PyTorch 领域的快速更新。

在下一节中,我们将探讨如何导出 PyTorch 模型,以便在不同的环境、编程语言和深度学习库中使用。

使用 TorchScript 和 ONNX 导出通用 PyTorch 模型

我们已经在本章前几节广泛讨论了提供 PyTorch 模型服务,这也许是在生产系统中实现 PyTorch 模型操作的最关键方面。在这一部分,我们将探讨另一个重要方面 – 导出 PyTorch 模型。我们已经学会了如何在经典的 Python 脚本环境中保存 PyTorch 模型并从磁盘加载它们。但是,我们需要更多导出 PyTorch 模型的方式。为什么呢?

对于初学者来说,Python 解释器一次只允许一个线程运行,使用全局解释器锁GIL)。这使得我们无法并行操作。其次,Python 可能不受我们希望在其上运行模型的每个系统或设备的支持。为了解决这些问题,PyTorch 提供了支持以高效的格式导出其模型,并以与平台或语言无关的方式,使模型能够在与其训练环境不同的环境中运行。

首先,我们将探讨 TorchScript,它使我们能够将序列化和优化的 PyTorch 模型导出为一个中间表示,然后可以在独立于 Python 的程序(比如说,C++ 程序)中运行。

接下来,我们将看看 ONNX 及其如何让我们将 PyTorch 模型保存为通用格式,然后加载到其他深度学习框架和不同编程语言中。

理解 TorchScript 的实用性

当涉及将 PyTorch 模型投入生产时,TorchScript 是一个至关重要的工具的两个关键原因:

  • PyTorch 基于急切执行,正如本书第一章“使用 PyTorch 进行深度学习概述”中讨论的那样。这有其优点,如更容易调试。然而,逐步执行步骤/操作,通过写入和读取中间结果到内存,可能导致高推理延迟,同时限制整体操作优化。为了解决这个问题,PyTorch 提供了自己的即时JIT)编译器,基于 Python 的 PyTorch 中心部分。

JIT 编译器编译 PyTorch 模型而不是解释,这相当于一次性查看模型的所有操作并创建一个复合图。JIT 编译的代码是 TorchScript 代码,它基本上是 Python 的静态类型子集。这种编译带来了多种性能改进和优化,比如去除 GIL,从而实现多线程。

  • PyTorch 本质上是与 Python 编程语言一起使用的。请记住,本书几乎完全使用了 Python。然而,在将模型投入生产时,有比 Python 更高效(即更快)的语言,如 C++。而且,我们可能希望在不支持 Python 的系统或设备上部署训练过的模型。

这就是 TorchScript 的作用。一旦我们将 PyTorch 代码编译成 TorchScript 代码,这是我们的 PyTorch 模型的中间表示,我们可以使用 TorchScript 编译器将这个表示序列化为一个符合 C++ 格式的文件。此后,可以在 C++ 模型推理程序中使用 LibTorch(PyTorch 的 C++ API)读取这个序列化文件。

我们在本节中已多次提到 PyTorch 模型的 JIT 编译。现在让我们看看将我们的 PyTorch 模型编译成 TorchScript 格式的两种可能选项中的两种。

使用 TorchScript 进行模型跟踪

将 PyTorch 代码转换为 TorchScript 的一种方法是跟踪 PyTorch 模型。跟踪需要 PyTorch 模型对象以及一个模型的虚拟示例输入。正如其名称所示,跟踪机制跟踪这个虚拟输入通过模型(神经网络)的流程,记录各种操作,并生成 TorchScript 中间表示IR),可以将其视为图形以及 TorchScript 代码进行可视化。

现在,我们将逐步介绍使用手写数字分类模型跟踪 PyTorch 模型的步骤。此练习的完整代码可在我们的 github 仓库 [13.19] 中找到。

此练习的前五个步骤与“保存和加载训练模型”和“构建推理流水线”部分的步骤相同,我们在这些部分构建了模型推理流水线:

  1. 我们将通过运行以下代码开始导入库:
import torch
...
  1. 接下来,我们将定义并实例化 model 对象:
class ConvNet(nn.Module):
    def __init__(self):
       …
    def forward(self, x):
        …
model = ConvNet()
  1. 接下来,我们将使用以下代码恢复模型权重:
PATH_TO_MODEL = "./convnet.pth"
model.load_state_dict(torch.load(PATH_TO_MODEL, map_location="cpu"))
model.eval()
  1. 然后我们加载一个示例图像:
image = Image.open("./digit_image.jpg")
  1. 接下来,我们定义数据预处理函数:
def image_to_tensor(image):
    gray_image = transforms.functional.to_grayscale(image)
    resized_image = transforms.functional.resize(gray_image, (28, 28))
    input_image_tensor = transforms.functional.to_tensor(resized_image)
    input_image_tensor_norm = transforms.functional.normalize(input_image_tensor, (0.1302,), (0.3069,))
    return input_image_tensor_norm

然后,我们将对样本图像应用预处理函数:

input_tensor = image_to_tensor(image)
  1. 除了 步骤 3 下的代码之外,我们还执行以下代码:
for p in model.parameters():
    p.requires_grad_(False)

如果我们不这样做,跟踪的模型将具有所有需要梯度的参数,我们将不得不在 torch.no_grad() 上下文中加载模型。

  1. 我们已经加载了带有预训练权重的 PyTorch 模型对象。接下来,我们将使用一个虚拟输入跟踪该模型:
demo_input = torch.ones(1, 1, 28, 28)
traced_model = torch.jit.trace(model, demo_input)

虚拟输入是一个所有像素值都设为 1 的图像。

  1. 现在我们可以通过运行这个来查看跟踪的模型图:
print(traced_model.graph)

这将输出以下内容:

图 13 .16 – 跟踪模型图

图 13 .16 – 跟踪模型图

直观地,图中的前几行展示了该模型层的初始化,如cn1cn2等。到了最后,我们看到了最后一层,也就是 softmax 层。显然,该图是用静态类型变量编写的低级语言,与 TorchScript 语言非常相似。

  1. 除了图形之外,我们还可以通过运行以下内容查看跟踪模型背后的确切 TorchScript 代码:
print(traced_model.code)

这将输出以下类似 Python 代码的行,定义了模型的前向传递方法:

Figure 13 .17 – 跟踪模型代码

图 13.17 – 跟踪模型代码

这恰好是我们在步骤 2中使用 PyTorch 编写的代码的 TorchScript 等效代码。

  1. 接下来,我们将导出或保存跟踪模型:
torch.jit.save(traced_model, 'traced_convnet.pt')
  1. 现在我们加载保存的模型:
loaded_traced_model = torch.jit.load('traced_convnet.pt')

注意,我们无需分别加载模型的架构和参数。

  1. 最后,我们可以使用此模型进行推断:
loaded_traced_model(input_tensor.unsqueeze(0))

输出如下:

这应该输出以下内容:Figure 13 .18 – 跟踪模型推断

图 13.18 – 跟踪模型推断

  1. 我们可以通过在原始模型上重新运行模型推断来检查这些结果:
model(input_tensor.unsqueeze(0))

这应该产生与图 13 .18相同的输出,从而验证我们的跟踪模型正常工作。

您可以使用跟踪模型而不是原始 PyTorch 模型对象来构建更高效的 Flask 模型服务器和 Docker 化的模型微服务,这要归功于 TorchScript 无 GIL 的特性。尽管跟踪是 JIT 编译 PyTorch 模型的可行选项,但它也有一些缺点。

例如,如果模型的前向传播包含诸如iffor语句等控制流,则跟踪只会呈现流程中的一条可能路径。为了准确地将 PyTorch 代码转换为 TorchScript 代码,以处理这种情况,我们将使用另一种称为脚本化的编译机制。

使用 TorchScript 进行模型脚本化

请按照上一练习中的 1 到 6 步骤,然后按照此练习中给出的步骤进行操作。完整代码可在我们的 github 仓库[13.20]中找到:

  1. 对于脚本化,我们无需为模型提供任何虚拟输入,并且以下代码行将 PyTorch 代码直接转换为 TorchScript 代码:
scripted_model = torch.jit.script(model)
  1. 让我们通过运行以下代码来查看脚本化模型图:
print(scripted_model.graph)

这应该以与跟踪模型代码图类似的方式输出脚本化模型图,如下图所示:

Figure 13 .19 – 脚本模型图

图 13.20 – 脚本模型代码

再次可以看到类似的冗长低级脚本,按行列出图的各种边缘。请注意,此处的图表与图 13 .16中的不同,这表明在使用跟踪而不是脚本化的代码编译策略时存在差异。

  1. 我们还可以通过运行此命令查看等效的 TorchScript 代码:
print(scripted_model.code)

这应该输出以下内容:

Figure 13 .20 – Scripted model code

图 13.20 – 脚本模型代码

本质上,流程与图 13 .17中的流程类似;但是,由于编译策略的不同,代码签名中存在细微差异。

  1. 再次,可以按以下方式导出脚本化模型并重新加载:
torch.jit.save(scripted_model, 'scripted_convnet.pt')
loaded_scripted_model = torch.jit.load('scripted_convnet.pt')
  1. 最后,我们使用此脚本化模型进行推断:
loaded_scripted_model(input_tensor.unsqueeze(0))

这应该产生与图 13 .18完全相同的结果,从而验证脚本化模型按预期工作。

与追踪类似,脚本化的 PyTorch 模型是 GIL-free 的,因此在与 Flask 或 Docker 一起使用时,可以提高模型服务的性能。表 13 .1 快速比较了模型追踪和脚本化的方法:

表 13 .1 – 追踪与脚本化的比较

表 13 .1 – 追踪与脚本化的比较

到目前为止,我们已经演示了如何将 PyTorch 模型转换并序列化为 TorchScript 模型。在接下来的部分中,我们将完全摆脱 Python,并演示如何使用 C++ 加载 TorchScript 序列化模型。

在 C++ 中运行 PyTorch 模型

Python 有时可能有限,或者我们可能无法运行使用 PyTorch 和 Python 训练的机器学习模型。在本节中,我们将使用在前一节中导出的序列化 TorchScript 模型对象(使用追踪和脚本化)来在 C++ 代码中运行模型推理。

注意

假设你具备基本的 C++ 工作知识 [13.21] 。本节专门讨论了关于 C++ 代码编译的内容 [13.22]

为了完成这个练习,我们需要按照 [13.23] 中提到的步骤安装 CMake 以便能够构建 C++ 代码。之后,我们将在当前工作目录下创建一个名为 cpp_convnet 的文件夹,并从该目录中工作:

  1. 让我们直接开始编写运行模型推理流水线的 C++ 文件。完整的 C++ 代码可以在我们的 github 仓库 [13.24] 中找到:
#include <torch/script.h>
...
int main(int argc, char **argv) {
    Mat img = imread(argv[2], IMREAD_GRAYSCALE);

首先,使用 OpenCV 库将 .jpg 图像文件读取为灰度图像。你需要根据你的操作系统要求安装 OpenCV 库 - Mac [13.25],Linux [13.26] 或 Windows [13.27]。

  1. 灰度图像随后被调整大小为 28x28 像素,因为这是我们 CNN 模型的要求:
resize(img, img, Size(28, 28));
  1. 然后,将图像数组转换为 PyTorch 张量:
auto input_ = torch::from_blob(img.data, { img.rows, img.cols, img.channels() }, at::kByte);

对于所有与 torch 相关的操作,如本步骤中所示,我们使用 libtorch 库,这是所有 torch C++ 相关 API 的家园。如果你已经安装了 PyTorch,就不需要单独安装 LibTorch。

  1. 因为 OpenCV 读取的灰度图像维度是 (28, 28, 1),我们需要将其转换为 (1, 28, 28) 以符合 PyTorch 的要求。然后,张量被重塑为形状为 (1,1,28,28),其中第一个 1 是推断的 batch_size,第二个 1 是通道数,对于灰度图像为 1
    auto input = input_.permute({2,0,1}).unsqueeze_(0).reshape({1, 1, img.rows, img.cols}).toType(c10::kFloat).div(255);
    input = (input0.1302) / 0.3069;

因为 OpenCV 读取的图像像素值范围是从 0255,我们将这些值归一化到 01 的范围。然后,我们使用平均值 0.1302 和标准差 0.3069 对图像进行标准化,就像我们在前面的章节中做的那样(参见构建推理流水线的第二步)。

  1. 在这一步中,我们加载了在前一个练习中导出的 JIT-ed TorchScript 模型对象:
    auto module = torch::jit::load(argv[1]);
    std::vector<torch::jit::IValue> inputs;
    inputs.push_back(input);
  1. 最后,我们来到模型预测阶段,在这里我们使用加载的模型对象对提供的输入数据进行前向传播(在本例中是一幅图像):
auto output_ = module.forward(inputs).toTensor();

output_ 变量包含每个类别的概率列表。让我们提取具有最高概率的类别标签并打印出来:

auto output = output_.argmax(1);
cout << output << '\n';

最后,我们成功退出 C++ 程序:

    return 0;
}
  1. 虽然 步骤 1-6 关注于我们 C++ 的各个部分,但我们还需要在相同的工作目录下编写一个 CMakeLists.txt 文件。此文件的完整代码可在我们的 github 仓库 [13.28] 中找到:
cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(cpp_convnet)
find_package(Torch REQUIRED)
find_package(OpenCV REQUIRED)
add_executable(cpp_convnet cpp_convnet.cpp)
...

此文件基本上是类似于 Python 项目中的 setup.py 的库安装和构建脚本。除此代码外,还需要将 OpenCV_DIR 环境变量设置为 OpenCV 构建产物的路径,如下面的代码块所示:

export OpenCV_DIR=/Users/ashish.jha/code/personal/MasteringPyTorchV2     /     Chapter13     /cpp_convnet/build_opencv/
  1. 接下来,我们需要实际运行 CMakeLists 文件以创建构建产物。我们通过在当前工作目录中创建一个新目录并从那里运行构建过程来完成这一步。在命令行中,我们只需运行以下命令:
mkdir build
cd build
cmake -DCMAKE_PREFIX_PATH=/Users/ashish.jha/opt/anaconda3/envs/mastering_pytorch/lib/python3.9/site-packages/torch     /share/cmake/ ..
cmake --build . --config Release

在第三行中,您应提供 LibTorch 的路径。要找到您自己的路径,请打开 Python 并执行以下操作:

import torch; torch.__path__

对于我来说,输出如下所示:

['/Users/ashish.jha/opt/anaconda3/envs/mastering_pytorch/lib/python3.9/site-packages/torch     ']_

执行第三行将输出以下内容:

图 13 .21 – C++ CMake 输出

图 13 .21 – C++ CMake 输出的结果

第四行应输出如下内容:

图 13 .22 – C++ 模型构建

图 13 .22 – C++ 模型构建

  1. 在成功执行上一步骤后,我们将生成一个名为 cpp_convnet 的 C++ 编译二进制文件。现在是执行这个二进制程序的时候了。换句话说,我们现在可以向我们的 C++ 模型提供一个样本图像进行推断。我们可以使用脚本化的模型作为输入:
./cpp_convnet ../../scripted_convnet.pt ../../digit_image.jpg

或者,我们可以使用跟踪的模型作为输入:

./cpp_convnet ../../traced_convnet.pt ../../digit_image.jpg

任何一种方法都应该产生以下输出:

图 13 .23 – C++ 模型预测

图 13 .23 – C++ 模型预测

根据 图 13 .3,C++ 模型似乎工作正常。由于我们在 C++ 中使用了不同的图像处理库(即 OpenCV),与 Python(PIL)相比,像素值稍有不同编码,这将导致略有不同的预测概率,但如果应用正确的归一化,两种语言的最终模型预测应该没有显著差异。

这结束了我们关于使用 C++ 进行 PyTorch 模型推断的探索。这个练习将帮助您开始将使用 PyTorch 编写和训练的喜爱深度学习模型转移到 C++ 环境中,这样做不仅可以使预测更高效,还可以在无 Python 环境(例如某些嵌入式系统、无人机等)中托管模型成为可能。

在接下来的部分,我们将远离 TorchScript,并讨论一个通用的神经网络建模格式 – ONNX,它使得模型可以跨深度学习框架、编程语言和操作系统进行使用。我们将在 TensorFlow 中加载一个 PyTorch 训练的模型进行推断。

使用 ONNX 导出 PyTorch 模型

在生产系统的某些场景中,大多数已部署的机器学习模型都是使用某种特定的深度学习库编写的,比如 TensorFlow,并配备了自己复杂的模型服务基础设施。但是,如果某个模型是使用 PyTorch 编写的,我们希望它能够在 TensorFlow 中运行,以符合服务策略。这是 ONNX 等框架在各种其他用例中有用的一个例子。

ONNX 是一个通用格式,用于标准化深度学习模型的基本操作,例如矩阵乘法和激活函数,在不同的深度学习库中编写时会有所不同。它使我们能够在不同的深度学习库、编程语言甚至操作环境中互换地运行相同的深度学习模型。

在这里,我们将演示如何在 TensorFlow 中运行使用 PyTorch 训练的模型。我们首先将 PyTorch 模型导出为 ONNX 格式,然后在 TensorFlow 代码中加载 ONNX 模型。

ONNX 与受限版本的 TensorFlow 兼容,因此我们将使用tensorflow==1.15.0。由于这个原因,我们将使用 Python 3.7,因为tensorflow==1.15.0在更新的 Python 版本中不可用。您可以使用以下命令创建并激活一个新的带有 Python 3.7 的 conda 环境:

conda create -n <env_name> python=3.7
source activate <env_name> 

我们还需要为本练习安装onnx==1.7.0onnx-tf==1.5.0库。本练习的完整代码可在我们的 github 仓库[13.29]中找到。请按照TorchScript 模型跟踪部分的第 1 到 11 步,然后执行本练习中的步骤:

  1. 类似于模型跟踪,我们再次通过加载的模型传递一个虚拟输入:
demo_input = torch.ones(1, 1, 28, 28)
torch.onnx.export(model, demo_input, "convnet.onnx")

这将保存一个模型onnx文件。在底层,序列化模型所使用的机制与模型跟踪中使用的相同。

  1. 接下来,我们加载保存的onnx模型并将其转换为 TensorFlow 模型:
import onnx
from onnx_tf.backend import prepare
model_onnx = onnx.load("./convnet.onnx")
tf_rep = prepare(model_onnx)
tf_rep.export_graph("./convnet.pb")
  1. 接下来,我们加载序列化的tensorflow模型以解析模型图。这将帮助我们验证已正确加载模型架构并标识图的输入和输出节点:
with tf.gfile.GFile("./convnet.pb", "rb") as f:
    graph_definition = tf.GraphDef()
    graph_definition.ParseFromString(f.read())
with tf.Graph().as_default() as model_graph:
    tf.import_graph_def(graph_definition, name="")
for op in model_graph.get_operations():
    print(op.values())

这应输出以下内容:

图 13 .24 – TensorFlow 模型图

图 13 .24 – TensorFlow 模型图

从图中,我们能够识别标记的输入和输出节点。

  1. 最后,我们可以为神经网络模型的输入和输出节点分配变量,实例化 TensorFlow 会话,并运行图以生成样本图像的预测:
model_output = model_graph.get_tensor_by_name('18:0')
model_input = model_graph.get_tensor_by_name('input.1:0')
sess = tf.Session(graph=model_graph)
output = sess.run(model_output, feed_dict={model_input: input_tensor.unsqueeze(0)})
print(output)

这应输出以下内容:

图 13 .25 – TensorFlow 模型预测

图 13 .25 – TensorFlow 模型预测

如您所见,在与 Figure 13 .18 进行比较后,我们模型的 TensorFlow 和 PyTorch 版本的预测完全相同。这验证了 ONNX 框架成功运行的有效性。我鼓励您进一步分析 TensorFlow 模型,了解 ONNX 如何通过利用模型图中的基础数学操作,在不同的深度学习库中重现完全相同的模型。

我们已经讨论了导出 PyTorch 模型的不同方式。本节介绍的技术将在将 PyTorch 模型部署到生产系统以及在各种平台上使用时非常有用。随着深度学习库、编程语言甚至操作系统的新版本不断推出,这一领域将迅速发展。

因此,强烈建议密切关注发展动态,并确保使用最新和最高效的模型导出和操作化方法。

到目前为止,我们一直在本地机器上为服务和导出 PyTorch 模型进行工作。在本章的下一个也是最后一个部分中,我们将简要介绍如何在一些知名的云平台上为 PyTorch 模型提供服务,例如 AWS、Google Cloud 和 Microsoft Azure。

在云中提供 PyTorch 模型

深度学习计算成本高,因此需要强大和复杂的计算硬件。并非每个人都能访问到本地机器,其具备足够的 CPU 和 GPU 来在合理时间内训练庞大的深度学习模型。此外,对于为推断服务提供训练好的模型的本地机器,我们无法保证其百分之百的可用性。出于这些原因,云计算平台是训练和服务深度学习模型的重要选择。

在本节中,我们将讨论如何在一些最流行的云平台上使用 PyTorch — AWSGoogle CloudMicrosoft Azure。我们将探讨在每个平台上为训练好的 PyTorch 模型提供服务的不同方式。我们在本章早期部分讨论的模型服务练习是在本地机器上执行的。本节的目标是让您能够使用云上的 虚拟机 (VMs) 执行类似的练习。

使用 PyTorch 和 AWS

AWS 是最古老和最流行的云计算平台之一。它与 PyTorch 有深度集成。我们已经在 TorchServe 中看到了一个例子,它是由 AWS 和 Facebook 共同开发的。

在本节中,我们将介绍一些使用 AWS 为服务 PyTorch 模型的常见方法。首先,我们将简单了解如何使用 AWS 实例来替代我们的本地机器(笔记本电脑)来服务 PyTorch 模型。然后,我们将简要讨论 Amazon SageMaker,这是一个专门的云机器学习平台。我们将简要讨论如何将 TorchServe 与 SageMaker 结合使用进行模型服务。

注意

本节假设您对 AWS 有基本的了解。因此,我们不会详细讨论诸如 AWS EC2 实例是什么、AMI 是什么、如何创建实例等主题[13.30]。相反,我们将专注于与 PyTorch 相关的 AWS 组件的组成部分。

使用 AWS 实例为 PyTorch 模型提供服务

在本节中,我们将演示如何在 VM 中使用 PyTorch —— 在这种情况下是 AWS 实例。阅读本节后,您将能够在 AWS 实例中执行 PyTorch 模型服务 部分讨论的练习。

首先,如果您还没有 AWS 账户,您需要创建一个。创建账户需要一个电子邮件地址和支付方式(信用卡)[13.31]。

一旦您拥有 AWS 账户,您可以登录 AWS 控制台[13.32] 。从这里,我们基本上需要实例化一个虚拟机(AWS 实例),在这里我们可以开始使用 PyTorch 进行模型训练和服务。创建虚拟机需要做出两个决定[13.33]:

  • 选择虚拟机的硬件配置,也称为AWS 实例类型

  • 选择Amazon Machine ImageAMI),它包含了所需的所有软件,如操作系统(Ubuntu 或 Windows)、Python、PyTorch 等

. 通常情况下,当我们提到 AWS 实例时,我们指的是弹性云计算实例,也称为EC2实例。

根据虚拟机的计算需求(RAM、CPU 和 GPU),您可以从 AWS 提供的 EC2 实例长列表中进行选择[13.34] 。由于 PyTorch 需要大量 GPU 计算能力,建议选择包含 GPU 的 EC2 实例,尽管它们通常比仅有 CPU 的实例更昂贵。

关于 AMI,选择 AMI 有两种可能的方法。您可以选择仅安装操作系统(如 Ubuntu(Linux))的基本 AMI。在这种情况下,您可以手动安装 Python[13.35] ,随后安装 PyTorch[13.36] 。

另一种更推荐的方法是从已安装了 PyTorch 的预构建 AMI 开始。AWS 提供了深度学习 AMI,这大大加快了在 AWS 上开始使用 PyTorch 的过程[13.37] 。

一旦您成功启动了一个实例,可以使用各种可用的方法简单地连接到该实例[13.38] 。

SSH 是连接实例的最常见方式之一。一旦您进入实例,它将与在本地机器上工作的布局相同。然后,其中一个首要逻辑步骤将是测试 PyTorch 是否在该机器内正常工作。

要进行测试,首先在命令行上输入 python 来打开 Python 交互会话。然后执行以下代码:

import torch

如果执行时没有错误,这意味着您已经在系统上安装了 PyTorch。

此时,您可以简单地获取本章前几节中编写的所有代码。在您的主目录命令行中,通过运行以下命令简单地克隆本书的 GitHub 仓库:

git clone https://github.com/arj7192/MasteringPyTorchV2.git 

然后,在Chapter13子文件夹内,您将拥有在前几节中处理的 MNIST 模型的所有代码。您可以基本上重新运行这些练习,这次在 AWS 实例上而不是您的本地计算机上。

让我们回顾一下在 AWS 上使用 PyTorch 需要采取的步骤:

  1. 创建一个 AWS 账号。

  2. 登录到 AWS 控制台。

  3. 在控制台上,单击启动虚拟机按钮。

  4. 选择一个 AMI。例如,选择 Deep Learning AMI(Ubuntu)。

  5. 选择一个 AWS 实例类型。例如,选择p.2x large,因为它包含 GPU。

  6. 单击启动

  7. 单击创建新的密钥对。为密钥对命名并在本地下载。

  8. 通过在命令行上运行以下命令修改此密钥对文件的权限:

chmod 400 downloaded-key-pair-file.pem
  1. 在控制台上,单击查看实例以查看启动实例的详细信息,并特别注意实例的公共 IP 地址。

  2. 使用 SSH,在命令行上运行以下命令连接到实例:

ssh -i downloaded-key-pair-file.pem ubuntu@<Public IP address>

公共 IP 地址与上一步获取的相同。

  1. 连接后,在python shell 中启动并运行import torch,确保 PyTorch 正确安装在实例上。

  2. 在实例的命令行上运行以下命令,克隆本书的 GitHub 仓库:

git clone https://github.com/arj7192/MasteringPyTorchV2.git 
  1. 转到仓库中的chapter13文件夹,并开始处理本章前几节中涉及的各种模型服务练习。

这将带我们来到本节的结束,我们基本上学会了如何在远程 AWS 实例上开始使用 PyTorch [13.39]。接下来,我们将了解 AWS 的完全专用云机器学习平台 – Amazon SageMaker。

使用 TorchServe 与 Amazon SageMaker

我们已经在前面的章节详细讨论了 TorchServe。正如我们所知,TorchServe 是由 AWS 和 Facebook 开发的 PyTorch 模型服务库。您可以使用 TorchServe 而不是手动定义模型推理流水线、模型服务 API 和微服务,TorchServe 提供所有这些功能。

另一方面,Amazon SageMaker 是一个云机器学习平台,提供诸如训练大规模深度学习模型以及在自定义实例上部署和托管训练模型等功能。在使用 SageMaker 时,我们只需执行以下操作:

  • 指定我们想要启动以服务模型的 AWS 实例类型和数量。

  • 提供存储的预训练模型对象的位置。

我们不需要手动连接到实例并使用 TorchServe 提供模型服务。SageMaker 会处理所有事务。AWS 网站有一些有用的博客文章,可以帮助您开始使用 SageMaker 和 TorchServe 在工业规模上使用 PyTorch 模型,并在几次点击内完成 [13.40] 。AWS 博客还提供了在使用 PyTorch 时使用 Amazon SageMaker 的用例 [13.41] 。

诸如 SageMaker 等工具在模型训练和服务期间非常有用。然而,在使用这类一键式工具时,我们通常会失去一些灵活性和可调试性。因此,您需要决定哪一套工具最适合您的用例。这结束了我们关于使用 AWS 作为 PyTorch 的云平台的讨论。接下来,我们将看看另一个云平台 - Google Cloud。

在 Google Cloud 上提供 PyTorch 模型

与 AWS 类似,如果您还没有 Google 账户(*@gmail.com),则首先需要创建一个。此外,要能够登录 Google Cloud 控制台 [13.42] ,您需要添加一个付款方式(信用卡详细信息)。

注意

我们这里不会涵盖 Google Cloud 的基础知识 [13.43] 。相反,我们将专注于在 VM 中用于提供 PyTorch 模型的 Google Cloud 使用方法。

一旦进入控制台,我们需要按照类似 AWS 的步骤启动一个 VM,在其中可以提供我们的 PyTorch 模型。您始终可以从基础的 VM 开始,并手动安装 PyTorch。但是,我们将使用预先安装了 PyTorch 的 Google Deep Learning VM 镜像 [13.44] 。以下是启动 Google Cloud VM 并用其提供 PyTorch 模型的步骤:

  1. 在 Google Cloud Marketplace 上启动深度学习 VM 镜像 [13.45] 。

  2. 在命令窗口中输入部署名称。该名称后缀为 -vm 作为已启动 VM 的名称。该 VM 内的命令提示符如下:

<user>@<deployment-name>-vm:~/

在这里,user 是连接到 VM 的客户端,deployment-name 是在此步骤中选择的 VM 的名称。

  1. 在下一个命令窗口中选择 PyTorch 作为 Framework 。这告诉平台在 VM 中预安装 PyTorch。

  2. 为此机器选择区域。最好选择地理位置最接近您的区域。此外,不同的区域有略微不同的硬件配置(VM 配置),因此您可能需要为特定的机器配置选择特定的区域。

  3. 步骤 3 中指定了软件要求后,现在我们将指定硬件要求。在命令窗口的 GPU 部分,我们需要指定 GPU 类型,并随后指定要包含在 VM 中的 GPU 数量。

Google Cloud 提供各种 GPU 设备/配置 [13.46] 。在 GPU 部分,还要勾选自动安装 NVIDIA 驱动程序的复选框,这是利用深度学习 GPU 所必需的。

  1. 同样,在 CPU 部分下,我们需要提供机器类型[13.47]。关于步骤 5步骤 6,请注意,不同区域提供不同的机器和 GPU 类型,以及不同的 GPU 类型和 GPU 数量的组合。

  2. 最后,点击Deploy按钮。这将启动虚拟机,并带您到一个页面,该页面包含连接本地计算机到虚拟机所需的所有指令。

  3. 在此时,您可以连接到虚拟机,并通过尝试在 Python shell 中导入 PyTorch 来确保 PyTorch 已正确安装。验证后,克隆本书的 GitHub 存储库。转到Chapter13文件夹,并开始在该虚拟机中进行模型服务练习。

您可以阅读有关在 Google Cloud 博客上创建 PyTorch 深度学习虚拟机的更多信息[13.48]。这结束了我们关于使用 Google Cloud 作为与 PyTorch 模型服务相关的云平台的讨论。正如您可能注意到的那样,该过程与 AWS 非常相似。在接下来的最后一节中,我们将简要介绍使用 Microsoft 的云平台 Azure 来使用 PyTorch。

在 Azure 上提供 PyTorch 模型

再次强调,与 AWS 和 Google Cloud 类似,Azure 需要一个 Microsoft 认可的电子邮件 ID 来注册,以及一个有效的支付方式。

注意

我们假设您对 Microsoft Azure 云平台有基本的了解[13.49]。

一旦您访问到 Azure 门户[13.50],有两种推荐的方法可以开始使用 PyTorch 在 Azure 上进行工作:

  • 数据科学虚拟机DSVM

  • Azure 机器学习

现在我们将简要讨论这些方法。

在 Azure 的数据科学虚拟机上工作

与 Google Cloud 的深度学习虚拟机映像类似,Azure 提供了其自己的 DSVM 映像[13.51],这是一个专门用于数据科学和机器学习(包括深度学习)的完全专用虚拟机映像。

这些映像适用于 Windows[13.52]以及 Linux/Ubuntu[13.53]。

使用此映像创建 DSVM 实例的步骤与讨论的 Google Cloud 的步骤非常相似,适用于 Windows[13.54]和 Linux/Ubuntu[13.55]。

创建 DSVM 后,您可以启动 Python shell 并尝试导入 PyTorch 库,以确保其已正确安装。您还可以进一步测试 Linux[13.56]和 Windows[13.57]上此 DSVM 可用的功能。

最后,您可以在 DSVM 实例内克隆本书的 GitHub 存储库,并使用Chapter13文件夹中的代码来进行本章讨论的 PyTorch 模型服务练习。

讨论 Azure 机器学习服务

与 Amazon SageMaker 相似且早于其,Azure 提供了一个端到端的云机器学习平台。Azure 机器学习服务(AMLS)包括以下内容(仅举几例):

  • Azure 机器学习虚拟机

  • 笔记本

  • 虚拟环境

  • 数据存储

  • 跟踪机器学习实验

  • 数据标记

AMLS VMs 和 DSVMs 之间的一个关键区别在于前者是完全托管的。例如,它们可以根据模型训练或服务的需求进行横向扩展或缩减 [13.58]。

就像 SageMaker 一样,Azure 机器学习既适用于训练大规模模型,也适用于部署和提供这些模型的服务。Azure 网站提供了一个很好的教程,用于在 AMLS 上训练 PyTorch 模型,以及在 Windows [13.59] 和 Linux [13.60] 上部署 PyTorch 模型。

Azure 机器学习旨在为用户提供一键式界面,用于所有机器学习任务。因此,重要的是要考虑灵活性的权衡。虽然我们在这里没有涵盖 Azure 机器学习的所有细节,但 Azure 的网站是进一步阅读的好资源 [13.61]。

这就是我们对 Azure 作为云平台为处理 PyTorch 提供的一切的讨论的结束 [13.62]。

这也结束了我们关于在云端使用 PyTorch 为模型提供服务的讨论。在本节中,我们讨论了 AWS、Google Cloud 和 Microsoft Azure。虽然还有更多的云平台可供选择,但它们的提供方式以及在这些平台上使用 PyTorch 的方式与我们讨论的类似。这一节将帮助您开始在云端的 VM 上处理您的 PyTorch 项目。

总结

在本章中,我们探讨了在生产系统中部署训练好的 PyTorch 深度学习模型的世界。

在下一章中,我们将探讨与在 PyTorch 中使用模型相关的另一个实用方面,这在训练和验证深度学习模型时能极大地节省时间和资源。

第十六章:PyTorch 和 AutoML

加入我们的书籍社区,在 Discord 上交流讨论。

packt.link/EarlyAccessCommunity

img

自动化机器学习AutoML)为给定神经网络提供了寻找最佳神经架构和最佳超参数设置的方法。在讨论第五章混合高级模型中详细介绍了神经架构搜索,例如RandWireNN模型。

在本章中,我们将更广泛地探讨用于 PyTorch 的 AutoML 工具——Auto-PyTorch——它既执行神经架构搜索又执行超参数搜索。我们还将研究另一个名为Optuna的 AutoML 工具,它专门为 PyTorch 模型执行超参数搜索。

在本章末尾,非专家将能够设计具有少量领域经验的机器学习模型,而专家将大大加快其模型选择过程。

本章分解为以下主题:

  • 使用 AutoML 寻找最佳神经架构

  • 使用 Optuna 进行超参数搜索

使用 AutoML 寻找最佳神经架构

想象一下机器学习算法的一种方式是它们自动化了学习给定输入和输出之间关系的过程。在传统软件工程中,我们必须明确地编写/编码这些关系,以函数形式接受输入并返回输出。在机器学习世界中,机器学习模型为我们找到这样的函数。尽管我们在一定程度上实现了自动化,但还有很多工作要做。除了挖掘和清理数据外,还有一些例行任务需要完成以获得这些函数:

  • 选择机器学习模型(或者模型家族,然后再选择模型)

  • 决定模型架构(特别是在深度学习情况下)

  • 选择超参数

  • 根据验证集性能调整超参数

  • 尝试不同的模型(或者模型家族)

这些是需要人类机器学习专家的任务类型。大多数步骤都是手动的,要么耗时很长,要么需要大量专业知识以缩短所需时间,而我们缺少足够数量的机器学习专家来创建和部署越来越受欢迎、有价值且有用的机器学习模型,这在工业界和学术界都如此。

这就是 AutoML 发挥作用的地方。AutoML 已成为机器学习领域内的一个学科,旨在自动化前述步骤及更多内容。

在本节中,我们将看看 Auto-PyTorch——一个专为与 PyTorch 配合使用而创建的 AutoML 工具。通过一项练习,我们将找到一个最优的神经网络以及执行手写数字分类的超参数——这是我们在第一章使用 PyTorch 进行深度学习概述中进行的任务。

与第一章的不同之处在于,这一次我们不决定架构或超参数,而是让 Auto-PyTorch 为我们找出最佳方案。我们将首先加载数据集,然后定义一个 Auto-PyTorch 模型搜索实例,最后运行模型搜索例程,以提供最佳性能模型。

工具引用

Auto-PyTorch [16.1] Auto-PyTorch Tabular: 多精度元学习以实现高效和稳健的 AutoDLLucas ZimmerMarius LindauerFrank Hutter [16.2]

使用 Auto-PyTorch 进行最佳 MNIST 模型搜索

我们将以 Jupyter Notebook 的形式执行模型搜索。在文本中,我们只展示代码的重要部分。完整的代码可以在我们的 github 代码库中找到 [16.3]

加载 MNIST 数据集

现在我们将逐步讨论加载数据集的代码,如下所示:

  1. 首先,我们导入相关的库,如下所示:
import torch
from autoPyTorch import AutoNetClassification

最后一行非常关键,因为我们在这里导入相关的 Auto-PyTorch 模块。这将帮助我们设置和执行模型搜索会话。

  1. 接下来,我们使用 Torch 的应用程序编程接口 (APIs)加载训练和测试数据集,如下所示:
train_ds = datasets.MNIST(...)
test_ds = datasets.MNIST(...)
  1. 然后,我们将这些数据集张量转换为训练和测试的输入(X)和输出(y)数组,如下所示:
X_train, X_test, y_train, y_test = train_ds.data.numpy().reshape(-1, 28*28), test_ds.data.numpy().reshape(-1, 28*28) ,train_ds.targets.numpy(), test_ds.targets.numpy()

注意,我们正在将图像重塑为大小为 784 的扁平化向量。在下一节中,我们将定义一个期望扁平化特征向量作为输入的 Auto-PyTorch 模型搜索器,因此我们进行了重塑。

在撰写本文时,Auto-PyTorch 目前仅支持以特征化和图像数据的形式提供支持,分别为AutoNetClassificationAutoNetImageClassification。虽然在本练习中我们使用的是特征化数据,但我们留给读者的练习是改用图像数据[16.4] 。

运行使用 Auto-PyTorch 进行神经架构搜索

在上一节加载了数据集之后,我们现在将使用 Auto-PyTorch 定义一个模型搜索实例,并使用它来执行神经架构搜索和超参数搜索的任务。我们将按以下步骤进行:

  1. 这是练习中最重要的一步,我们在此定义一个autoPyTorch模型搜索实例,如下所示:
autoPyTorch = AutoNetClassification("tiny_cs",  # config preset
             log_level='info', max_runtime=2000, min_budget=100, max_budget=1500)

这里的配置是从 Auto-PyTorch 仓库提供的示例中衍生出来的 [16.5] 。但通常情况下,tiny_cs用于更快速的搜索,且硬件要求较少。

预算参数主要是为了设置对 Auto-PyTorch 过程资源消耗的限制。默认情况下,预算的单位是时间,即我们愿意在模型搜索上花费多少中央处理单元/图形处理单元CPU/GPU)时间。

  1. 实例化了一个 Auto-PyTorch 模型搜索实例后,我们通过尝试将实例适配到训练数据集上来执行搜索,如下所示:
autoPyTorch.fit(X_train, y_train, validation_split=0.1)

内部,Auto-PyTorch 将基于原始论文中提到的方法运行多个试验,尝试不同的模型架构和超参数设置 [16.2] 。

不同的试验将与 10%的验证数据集进行基准测试,并将最佳性能的试验作为输出返回。前述代码片段中的命令应该会输出以下内容:

图 16 .1 – Auto-PyTorch 模型准确性

图 16 .1 – Auto-PyTorch 模型准确性

图 16 .1 基本上展示了 Auto-PyTorch 为给定任务找到的最佳超参数设置,例如学习率为0.068,动量为0.934等。前面的截图还显示了所选最佳模型配置的训练集和验证集准确性。

  1. 已经收敛到最佳训练模型后,我们现在可以使用该模型对测试集进行预测,如下所示:
y_pred = autoPyTorch.predict(X_test)print("Accuracy score", np.mean(y_pred.reshape(-1) == y_test))

它应该输出类似于这样的内容:

图 16 .2 – Auto-PyTorch 模型准确性

图 16 .2 – Auto-PyTorch 模型准确性

正如我们所见,我们获得了一个测试集性能达到了 96.4%的模型。为了对比,随机选择将导致 10%的性能水平。我们在没有定义模型架构或超参数的情况下获得了这样的良好性能。在设置更高预算后,更广泛的搜索可能会导致更好的性能。

此外,性能将根据执行搜索的硬件(机器)而变化。具有更多计算能力和内存的硬件可以在相同的时间预算内运行更多搜索,因此可能导致更好的性能。

可视化最优 AutoML 模型

在本节中,我们将查看通过在前一节中运行模型搜索例程获得的最佳性能模型。我们将按以下步骤进行:

  1. 在前面的章节中已经查看了超参数,现在让我们看一下 Auto-PyTorch 为我们设计的最佳模型架构,如下所示:
pytorch_model = autoPyTorch.get_pytorch_model()
print(pytorch_model)

它应该输出类似于这样的内容:

图 16 .3 – Auto-PyTorch 模型架构

图 16 .3 – Auto-PyTorch 模型架构

该模型由一些结构化的残差块组成,其中包含全连接层、批量归一化层和 ReLU 激活函数。最后,我们看到一个最终的全连接层,具有 10 个输出,每个输出对应于从 0 到 9 的一个数字。

  1. 我们还可以使用torchviz来可视化实际的模型图,如下代码片段所示:
x = torch.randn(1, pytorch_model[0].in_features)
y = pytorch_model(x)
arch = make_dot(y.mean(), params=dict(pytorch_model.named_parameters()))
arch.format="pdf"
arch.filename = "convnet_arch"
arch.render(view=False)

这应该会在当前工作目录中保存一个convnet_arch.pdf文件,在打开时应该看起来像这样:

图 16 .4 – Auto-PyTorch 模型图示

图 16 .4 – Auto-PyTorch 模型图示

  1. 要查看模型如何收敛到此解决方案,我们可以查看在模型查找过程中使用的搜索空间代码如下:
autoPyTorch.get_hyperparameter_search_space()

这应该会输出以下内容:

图 16 .5 – Auto-PyTorch 模型搜索空间

图 16 .5 – Auto-PyTorch 模型搜索空间

它基本上列出了构建模型所需的各种要素,并为每个要素分配了一个范围。例如,学习率被分配了0.00010.1的范围,并且这个空间是以对数尺度进行采样——这不是线性采样而是对数采样。

图 16 .1中,我们已经看到了 Auto-PyTorch 从这些范围中采样的确切超参数值作为给定任务的最优值。我们还可以手动更改这些超参数范围,甚至添加更多超参数,使用 Auto-PyTorch 模块下的HyperparameterSearchSpaceUpdates子模块 [16.6] 。

这就结束了我们对 Auto-PyTorch 的探索——一个用于 PyTorch 的自动机器学习工具。我们成功地使用 Auto-PyTorch 构建了一个 MNIST 数字分类模型,而无需指定模型架构或超参数。此练习将帮助您开始使用此类和其他自动机器学习工具以自动化方式构建 PyTorch 模型。这里列出了一些类似的其他工具 - Hyperopt [16.7]、Tune [16.8]、Hypersearch [16.9]、Skorcj [16.10]、BoTorch [16.11] 和 Optuna [16.12]。

虽然我们在本章中无法涵盖所有这些工具,在下一节中我们将讨论 Optuna,这是一个专注于查找最佳超参数集的工具,并且与 PyTorch 兼容良好。

使用 Optuna 进行超参数搜索

Optuna 是支持 PyTorch 的超参数搜索工具之一。您可以详细了解该工具使用的搜索策略,如TPE(树形结构帕尔森估计)和CMA-ES(协方差矩阵适应进化策略),在Optuna论文 [16.13] 中。除了先进的超参数搜索方法,该工具还提供了一个简洁的 API,我们将在下一节中探讨。

工具引用

Optuna: 下一代超参数优化框架。

Takuya Akiba, Shotaro Sano, Toshihiko Yanase, Takeru OhtaMasanori Koyama(2019 年,KDD)。

在本节中,我们将再次构建和训练MNIST模型,这次使用 Optuna 来找出最佳的超参数设置。我们将逐步讨论代码的重要部分,以练习的形式进行。完整的代码可以在我们的 github [16.14]上找到。

定义模型架构和加载数据集

首先,我们将定义一个符合 Optuna 要求的模型对象。所谓 Optuna 兼容,是指在模型定义代码中添加 Optuna 提供的 API,以便对模型超参数进行参数化。为此,我们将按照以下步骤进行:

  1. 首先,我们导入必要的库,如下所示:
import torch
import optuna

optuna库将在整个练习中管理超参数搜索。

  1. 接下来,我们定义模型架构。因为我们希望对一些超参数(如层数和每层单位数)保持灵活,所以需要在模型定义代码中包含一些逻辑。因此,首先,我们声明需要在 14 个卷积层和之后的 12 个全连接层,如下面的代码片段所示:
class ConvNet(nn.Module):
    def __init__(self, trial):
        super(ConvNet, self).__init__()
        num_conv_layers =  trial.suggest_int("num_conv_layers", 1, 4)
        num_fc_layers = trial.suggest_int("num_fc_layers", 1, 2)
  1. 然后,我们逐个添加卷积层。每个卷积层紧接着一个 ReLU 激活层,对于每个卷积层,我们声明该层的深度在 1664 之间。

步幅和填充分别固定为 3True,整个卷积块之后是一个 MaxPool 层,然后是一个 Dropout 层,dropout 概率范围在 0.10.4 之间(另一个超参数),如下面的代码片段所示:

 self.layers = []
        input_depth = 1 # grayscale image
        for i in range(num_conv_layers):
            output_depth = trial.suggest_int(f"conv_depth_{i}", 16, 64)
            self.layers.append(nn.Conv2d(input_depth, output_depth, 3, 1))
            self.layers.append(nn.ReLU())
            input_depth = output_depth
        self.layers.append(nn.MaxPool2d(2))
        p = trial.suggest_float(f"conv_dropout_{i}", 0.1, 0.4)
        self.layers.append(nn.Dropout(p))
        self.layers.append(nn.Flatten())
  1. 接下来,我们添加一个展平层,以便后续可以添加全连接层。我们必须定义一个 _get_flatten_shape 函数来推导展平层输出的形状。然后,我们逐步添加全连接层,其中单位数声明为介于 1664 之间。每个全连接层后面跟着一个 Dropout 层,再次使用概率范围为 0.10.4

最后,我们附加一个固定的全连接层,输出 10 个数字(每个类别/数字一个),然后是一个 LogSoftmax 层。定义了所有层之后,我们实例化我们的模型对象,如下所示:

 input_feat = self._get_flatten_shape()
        for i in range(num_fc_layers):
            output_feat = trial.suggest_int(f"fc_output_feat_{i}", 16, 64)
            self.layers.append(nn.Linear(input_feat, output_feat))
            self.layers.append(nn.ReLU())
            p = trial.suggest_float(f"fc_dropout_{i}", 0.1, 0.4)
            self.layers.append(nn.Dropout(p))
            input_feat = output_feat
        self.layers.append(nn.Linear(input_feat, 10))
        self.layers.append(nn.LogSoftmax(dim=1))
        self.model = nn.Sequential(*self.layers)
    def _get_flatten_shape(self):
        conv_model = nn.Sequential(*self.layers)
        op_feat = conv_model(torch.rand(1, 1, 28, 28))
        n_size = op_feat.data.view(1, -1).size(1)
        return n_size

这个模型初始化函数是依赖于 trial 对象的条件设置,该对象由 Optuna 轻松处理,并决定我们模型的超参数设置。最后,forward 方法非常简单,可以在下面的代码片段中看到:

 def forward(self, x):
        return self.model(x)

因此,我们已经定义了我们的模型对象,现在可以继续加载数据集。

  1. 数据集加载的代码与 第一章,使用 PyTorch 进行深度学习概述 中相同,并在下面的代码片段中再次显示:
train_dataloader = torch.utils.data.DataLoader(...)
test_dataloader = ...

在本节中,我们成功地定义了我们的参数化模型对象,并加载了数据集。现在,我们将定义模型训练和测试程序,以及优化调度。

定义模型训练程序和优化调度

模型训练本身涉及超参数,如优化器、学习率等。在本练习的这一部分中,我们将定义模型训练过程,同时利用 Optuna 的参数化能力。我们将按以下步骤进行:

  1. 首先,我们定义训练例程。再次强调,这段代码与 第一章,使用 PyTorch 进行深度学习概述 中此模型的训练例程代码相同,并在此处再次显示:
def train(model, device, train_dataloader, optim, epoch):
    for b_i, (X, y) in enumerate(train_dataloader):
        …
  1. 模型测试例程需要稍作调整。为了按照 Optuna API 的要求操作,测试例程需要返回一个模型性能指标——在本例中是准确率,以便 Optuna 可以根据这一指标比较不同的超参数设置,如以下代码片段所示:
def test(model, device, test_dataloader):
    with torch.no_grad():
        for X, y in test_dataloader:
            …
    accuracy = 100\. * success/ len(test_dataloader.dataset)
    return accuracy
  1. 以前,我们会使用学习率来实例化模型和优化函数,并在任何函数外部启动训练循环。但是为了遵循 Optuna API 的要求,我们现在将所有这些都放在一个objective函数中进行,该函数接受与我们模型对象的__init__方法中传递的trial对象相同的参数。

这里也需要trial对象,因为涉及到决定学习率值和选择优化器的超参数,如以下代码片段所示:

def objective(trial):
    model = ConvNet(trial)
    opt_name = trial.suggest_categorical("optimizer", ["Adam", "Adadelta", "RMSprop", "SGD"])
    lr = trial.suggest_float("lr", 1e-1, 5e-1, log=True)
    optimizer = getattr(optim,opt_name)(model.parameters(), lr=lr)    
    for epoch in range(1, 3):
        train(model, device, train_dataloader, optimizer, epoch)
        accuracy = test(model, device,test_dataloader)
        trial.report(accuracy, epoch)
        if trial.should_prune():
            raise optuna.exceptions.TrialPruned()
    return accuracy

对于每个 epoch,我们记录模型测试例程返回的准确率。此外,在每个 epoch,我们还检查是否会剪枝——即是否会跳过当前 epoch。这是 Optuna 提供的另一个功能,用于加速超参数搜索过程,以避免在糟糕的超参数设置上浪费时间。

运行 Optuna 的超参数搜索

在这个练习的最后部分,我们将实例化所谓的Optuna study,并利用模型定义和训练例程,为给定的模型和给定的数据集执行 Optuna 的超参数搜索过程。我们将按如下步骤进行:

  1. 在前面的章节中准备了所有必要的组件后,我们已经准备好开始超参数搜索过程——在 Optuna 术语中称为study。一个trialstudy中的一个超参数搜索迭代。代码可以在以下代码片段中看到:
study = optuna.create_study(study_name="mastering_pytorch", direction="maximize")
study.optimize(objective, n_trials=10, timeout=2000)

direction参数帮助 Optuna 比较不同的超参数设置。因为我们的指标是准确率,我们将需要maximize这个指标。我们允许最多 2000 秒的study或最多 10 个不同的搜索——以先完成者为准。前述命令应输出以下内容:

图 16 .6 – Optuna 日志

图 16 .6 – Optuna 日志

正如我们所见,第三个trial是最优的试验,产生了 98.77%的测试集准确率,最后三个trials被剪枝。在日志中,我们还可以看到每个未剪枝trial的超参数。例如,在最优的trial中,有三个分别具有 27、28 和 46 个特征映射的卷积层,然后有两个分别具有 57 和 54 个单元/神经元的全连接层,等等。

  1. 每个trial都有一个完成或被剪枝的状态。我们可以用以下代码标记它们:
pruned_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.PRUNED]complete_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
  1. 最后,我们可以具体查看最成功trial的所有超参数,使用以下代码:
print("results: ")
trial = study.best_trial
for key, value in trial.params.items():
    print("{}: {}".format(key, value))

您将看到以下输出:

图 16 .7 – Optuna 最优超参数

图 16 .7 – Optuna 最优超参数

正如我们所见,输出显示了总trials数和执行的成功trials数。它进一步显示了最成功trial的模型超参数,如层数、层中神经元数量、学习率、优化进度等。

这将我们带到了练习的尾声。我们成功地使用 Optuna 定义了不同类型超参数的值范围,适用于手写数字分类模型。利用 Optuna 的超参数搜索算法,我们运行了 10 个不同的trials,在其中一个trial中获得了 98.77% 的最高准确率。最成功trial中的模型(架构和超参数)可以用于在更大数据集上进行训练,从而服务于生产系统。

使用本节的教训,您可以使用 Optuna 找到任何用 PyTorch 编写的神经网络模型的最佳超参数。如果模型非常庞大和/或需要调整的超参数过多,Optuna 也可以在分布式环境中使用 [16.15]。

最后,Optuna 不仅支持 PyTorch,还支持其他流行的机器学习库,如TensorFlowSklearnMXNet等等。

摘要

在本章中,我们讨论了自动机器学习(AutoML),旨在提供模型选择和超参数优化的方法。AutoML 对于初学者非常有用,他们在做出诸如模型中应放置多少层、使用哪种优化器等决策时缺乏专业知识。AutoML 对于专家也很有用,可以加快模型训练过程,发现给定任务的优越模型架构,这些任务手动推断几乎是不可能的。

在下一章中,我们将研究另一个越来越重要和关键的机器学习方面,特别是深度学习。我们将密切关注如何解释由 PyTorch 模型生成的输出——这一领域通常被称为模型可解释性或可解释性。

第十七章:PyTorch 和可解释 AI

在我们的 Discord 书籍社区中加入我们

packt.link/EarlyAccessCommunity

img

在本书中,我们构建了几个可以为我们执行不同任务的深度学习模型。例如,手写数字分类器,图像字幕生成器,情感分类器等等。尽管我们已经掌握了如何使用 PyTorch 训练和评估这些模型,但我们不知道这些模型在做出预测时内部究竟发生了什么。模型可解释性或解释性是机器学习的一个领域,我们在这个领域的目标是回答这样一个问题,为什么模型做出了那个预测?更详细地说,模型在输入数据中看到了什么,以做出特定的预测?

在本章中,我们将使用来自 第一章《使用 PyTorch 概述深度学习》的手写数字分类模型,来理解其内部工作原理,并因此解释模型为给定输入做出特定预测的原因。我们将首先使用纯粹的 PyTorch 代码来解剖模型。然后,我们将使用一种专门的模型可解释性工具包,称为Captum,进一步调查模型内部发生的情况。Captum 是一个专门为 PyTorch 提供模型解释工具的第三方库,包括基于图像和文本的模型。

本章应该为您提供解开深度学习模型内部的技能所必需的知识。以这种方式查看模型内部可以帮助您理解模型的预测行为。在本章的结尾,您将能够利用实践经验开始解释您自己的深度学习模型,使用 PyTorch(和 Captum)。

本章分解为以下主题:

  • PyTorch 中的模型可解释性

  • 使用 Captum 解释模型

PyTorch 中的模型可解释性

在本节中,我们将使用 PyTorch 对已训练的手写数字分类模型进行解剖,作为一项练习。更确切地说,我们将查看训练的手写数字分类模型的卷积层的详细信息,以了解模型从手写数字图像中学到了哪些视觉特征。我们将查看卷积滤波器/核心以及这些滤波器产生的特征图。

这些细节将帮助我们理解模型如何处理输入图像,从而进行预测。练习的完整代码可以在我们的 github 仓库 [13.1] 中找到。

训练手写数字分类器 - 重温

我们将快速回顾涉及训练手写数字分类模型的步骤,如下所示:

  1. 首先,我们导入相关的库,然后设置随机种子,以便能够重现这次练习的结果:
import torch
np.random.seed(123)
torch.manual_seed(123)
  1. 接下来,我们将定义模型架构:
class ConvNet(nn.Module):
    def __init__(self):
    def forward(self, x):
  1. 接下来,我们将定义模型的训练和测试过程:
def train(model, device, train_dataloader, optim,  epoch):
def test(model, device, test_dataloader):
  1. 然后,我们定义训练和测试数据集加载器:
train_dataloader = torch.utils.data.DataLoader(...)
test_dataloader = torch.utils.data.DataLoader(...)
  1. 接下来,我们实例化我们的模型,并定义优化计划:
device = torch.device("cpu")
model = ConvNet()
optimizer = optim.Adadelta(model.parameters(), lr=0.5)
  1. 最后,我们开始模型训练循环,训练我们的模型进行 20 个 epochs:
for epoch in range(1, 20):
    train(model, device, train_dataloader, optimizer, epoch)
    test(model, device, test_dataloader)

这应该输出以下内容:

图 13.1 – 模型训练日志

图 13.1 – 模型训练日志

  1. 最后,我们可以在一个样本测试图像上测试训练好的模型。这个样本测试图像的加载方式如下:
test_samples = enumerate(test_dataloader)
b_i, (sample_data, sample_targets) = next(test_samples)
plt.imshow(sample_data[0][0], cmap='gray', interpolation='none')
plt.show()

这应该输出以下内容:

图 13.2 – 一个手写图像示例

图 13.2 – 一个手写图像示例

  1. 然后,我们使用这个样本测试图像进行模型预测,如下所示:
print(f"Model prediction is : {model(sample_data).data.max(1)[1][0]}")
print(f"Ground truth is : {sample_targets[0]}")

这应该输出以下内容:

图 13.3 – 模型预测

图 13.3 – 模型预测

因此,我们已经训练了一个手写数字分类模型,并用它对一个样本图像进行了推断。现在我们将看看训练模型的内部结构。我们还将研究这个模型学习到了哪些卷积滤波器。

可视化模型的卷积滤波器

在本节中,我们将详细讨论已训练模型的卷积层,并查看模型在训练期间学习到的滤波器。这将告诉我们卷积层在输入图像上的操作方式,正在提取哪些特征等等:

  1. 首先,我们需要获取模型中所有层的列表,如下所示:
model_children_list = list(model.children())
convolutional_layers = []
model_parameters = []
model_children_list

这应该输出以下内容:

图 13.4 – 模型层

图 13.4 – 模型层

正如您所看到的,这里有 2 个卷积层,它们都有 3x3 大小的滤波器。第一个卷积层使用了16个这样的滤波器,而第二个卷积层使用了32个。我们重点在本练习中可视化卷积层,因为它们在视觉上更直观。然而,您可以通过类似的方式探索其他层,比如线性层,通过可视化它们学到的权重。

  1. 接下来,我们从模型中选择只有卷积层,并将它们存储在一个单独的列表中:
for i in range(len(model_children_list)):
    if type(model_children_list[i]) == nn.Conv2d:
        model_parameters.append(model_children_list[i].w      eight)
        convolutional_layers.append(model_children_list[i])

在这个过程中,我们还确保存储每个卷积层中学到的参数或权重。

  1. 现在我们准备好可视化卷积层学到的滤波器。我们从第一层开始,该层每个都有 16 个 3x3 大小的滤波器。下面的代码为我们可视化了这些滤波器:
plt.figure(figsize=(5, 4))
for i, flt in enumerate(model_parameters[0]):
    plt.subplot(4, 4, i+1)
    plt.imshow(flt[0, :, :].detach(), cmap='gray')
    plt.axis('off')
plt.show()

这应该输出以下内容:

图 13.5 – 第一个卷积层的滤波器

图 13.5 – 第一个卷积层的滤波器

首先,我们可以看到所有学习到的滤波器都略有不同,这是一个好迹象。这些滤波器通常在内部具有对比值,以便在图像周围卷积时提取某些类型的梯度。在模型推断期间,这 16 个滤波器中的每一个都会独立地在输入的灰度图像上操作,并产生 16 个不同的特征图,我们将在下一节中进行可视化。

  1. 类似地,我们可以使用与前一步骤相同的代码来可视化第二个卷积层学习到的 32 个滤波器,但需要进行以下更改:
plt.figure(figsize=(5, 8))
for i, flt in enumerate(model_parameters[1]):
plt.show()

这应该输出以下内容:

图 13.6 – 第二个卷积层的滤波器

图 13.6 – 第二个卷积层的滤波器

再次,我们有 32 个不同的滤波器/内核,它们具有对比值,旨在从图像中提取梯度。这些滤波器已经应用于第一个卷积层的输出,因此产生了更高级别的输出特征图。具有多个卷积层的 CNN 模型通常的目标是持续生成更复杂或更高级别的特征,可以表示复杂的视觉元素,例如面部的鼻子,道路上的交通灯等。

接下来,我们将看看这些卷积层在它们的输入上操作/卷积时产生了什么。

可视化模型的特征图

在这一部分中,我们将通过卷积层运行一个样本手写图像,并可视化这些层的输出:

  1. 首先,我们需要将每个卷积层输出的结果收集到一个列表中,可以通过以下代码实现:
per_layer_results = convolutional_layers[0]
for i in range(1, len(convolutional_layers)):
    per_layer_results.append(convolutional_layersi)

请注意,我们分别为每个卷积层调用前向传播,同时确保第 n 个卷积层接收第(n-1)个卷积层的输出作为输入。

  1. 现在我们可以可视化由这两个卷积层产生的特征图。我们将从第一层开始运行以下代码:
plt.figure(figsize=(5, 4))
layer_visualisation = per_layer_results[0][0, :, :, :]
layer_visualisation = layer_visualisation.data
print(layer_visualisation.size())
for i, flt in enumerate(layer_visualisation):
    plt.subplot(4, 4, i + 1)
    plt.imshow(flt, cmap='gray')
    plt.axis("off")
plt.show()
  1. 这应该输出以下内容:

图 13.7 – 第一个卷积层的特征图

图 13.7 – 第一个卷积层的特征图

数字**(16, 26, 26)**表示第一卷积层的输出维度。实际上,样本图像尺寸为(28, 28),滤波器尺寸为(3,3),并且没有填充。因此,生成的特征图大小为(26, 26)。由于有 16 个这样的特征图由 16 个滤波器产生(请参考图 13.5),因此总体输出维度为(16, 26, 26)。

正如您所看到的,每个滤波器从输入图像中生成一个特征图。此外,每个特征图代表图像中的不同视觉特征。例如,左上角的特征图基本上颠倒了图像中的像素值(请参考图 13.2),而右下角的特征图表示某种形式的边缘检测。

这些 16 个特征图然后传递到第二个卷积层,其中另外 32 个滤波器分别在这 16 个特征图上卷积,产生 32 个新的特征图。我们接下来将查看这些特征图。

  1. 我们可以使用与前面类似的代码,稍作更改(如下面的代码所示),来可视化下一个卷积层产生的 32 个特征图:
plt.figure(figsize=(5, 8))
layer_visualisation = per_layer_results[1][0, :, :, :]
    plt.subplot(8, 4, i + 1)
plt.show()

这应该输出以下内容:

图 13.8 – 第二个卷积层的特征图

图 13.8 – 第二个卷积层的特征图

与之前的 16 个特征图相比,这 32 个特征图显然更复杂。它们似乎不仅仅是边缘检测,这是因为它们已经在第一个卷积层的输出上操作,而不是原始输入图像。

在这个模型中,2 个卷积层之后是 2 个线性层,分别有(4,608x64)和(64x10)个参数。虽然线性层的权重也有助于可视化,但参数数量(4,608x64)的视觉化分析看起来实在太多了。因此,在本节中,我们将仅限于卷积权重的视觉分析。

幸运的是,我们有更复杂的方法来解释模型预测,而不需要查看如此多的参数。在下一节中,我们将探讨 Captum,这是一个与 PyTorch 配合使用的机器学习模型解释工具包,可以在几行代码内帮助我们解释模型决策。

使用 Captum 解释模型

Captum [13.2] 是由 Facebook 在 PyTorch 上构建的开源模型解释库,目前(撰写本文时)正在积极开发中。在本节中,我们将使用前面章节中训练过的手写数字分类模型。我们还将使用 Captum 提供的一些模型解释工具来解释该模型所做的预测。此练习的完整代码可以在我们的 github 代码库 [13.3] 中找到。

设置 Captum

模型训练代码类似于“训练手写数字分类器 – 总结”部分中显示的代码。在接下来的步骤中,我们将使用训练好的模型和一个样本图像,来理解模型在为给定图像进行预测时内部发生了什么:

  1. 有几个与 Captum 相关的额外导入,我们需要执行,以便使用 Captum 的内置模型解释功能:
from captum.attr import IntegratedGradients
from captum.attr import Saliency
from captum.attr import DeepLift
from captum.attr import visualization as viz
  1. 要对输入图像进行模型的前向传递,我们将输入图像重塑为与模型输入大小相匹配:
captum_input = sample_data[0].unsqueeze(0)
captum_input.requires_grad = True
  1. 根据 Captum 的要求,输入张量(图像)需要参与梯度计算。因此,我们将输入的 requires_grad 标志设置为 True

  2. 接下来,我们准备样本图像,以便通过模型解释方法进行处理,使用以下代码:

orig_image = np.tile(np.transpose((sample_data[0].cpu().detach().numpy() / 2) + 0.5, (1, 2, 0)), (1,1,3))
_ = viz.visualize_image_attr(None, orig_image, cmap='gray', method="original_image", title="Original Image")

这应该输出以下内容:

图 13.9 – 原始图像

图 13.9 – 原始图像

我们已经在深度维度上平铺了灰度图像,以便 Captum 方法能够处理,这些方法期望一个 3 通道图像。

接下来,我们将实际应用一些 Captum 的解释性方法,通过预训练的手写数字分类模型对准备的灰度图像进行前向传递。

探索 Captum 的可解释性工具

在本节中,我们将探讨 Captum 提供的一些模型可解释性方法。

解释模型结果的最基本方法之一是观察显著性,它表示输出(在本例中是类别 0)关于输入(即输入图像像素)的梯度。对于特定输入,梯度越大,该输入越重要。您可以在原始的显著性论文[13.4]中详细了解这些梯度的计算方式。Captum 提供了显著性方法的实现:

  1. 在以下代码中,我们使用 Captum 的 Saliency 模块计算梯度:
saliency = Saliency(model)
gradients = saliency.attribute(captum_input, target=sample_targets[0].item())
gradients = np.reshape(gradients.squeeze().cpu().detach().numpy(), (28, 28, 1))
_ = viz.visualize_image_attr(gradients, orig_image, method="blended_heat_map", sign="absolute_value",
show_colorbar=True, title="Overlayed Gradients")

这应该输出如下结果:

图 13.10 – 叠加梯度

图 13.10 – 叠加梯度

在前面的代码中,我们将获得的梯度重塑为 (28,28,1) 的大小,以便在原始图像上叠加显示,如前面的图示所示。Captum 的 viz 模块为我们处理了可视化。我们还可以使用以下代码仅可视化梯度,而不显示原始图像:

plt.imshow(np.tile(gradients/(np.max(gradients)), (1,1,3)));

我们将获得以下输出:

图 13.11 – 梯度

图 13.11 – 梯度

正如你所看到的,梯度分布在图像中那些可能包含数字0的像素区域。

  1. 接下来,我们将采用类似的代码方式,研究另一种可解释性方法 - 综合梯度。通过这种方法,我们将寻找特征归因特征重要性。也就是说,我们将寻找在进行预测时使用的哪些像素是重要的。在综合梯度技术下,除了输入图像外,我们还需要指定一个基线图像,通常将其设置为所有像素值均为零的图像。

然后,沿着从基线图像到输入图像的路径计算梯度的积分。关于综合梯度技术的实现细节可以在原始论文[13.5]中找到。以下代码使用 Captum 的 IntegratedGradients 模块推导每个输入图像像素的重要性:

integ_grads = IntegratedGradients(model)
attributed_ig, delta=integ_grads.attribute(captum_input, target=sample_targets[0], baselines=captum_input * 0, return_convergence_delta=True)
attributed_ig = np.reshape(attributed_ig.squeeze().cpu().detach().numpy(), (28, 28, 1))
_ = viz.visualize_image_attr(attributed_ig, orig_image, method="blended_heat_map",sign="all",show_colorbar=True, title="Overlayed Integrated Gradients")

这应该输出如下结果:

图 13.12 – 叠加的综合梯度

图 13.12 – 叠加的综合梯度

如预期的那样,梯度在包含数字0的像素区域中较高。

  1. 最后,我们将研究另一种基于梯度的归因技术,称为深度提升。除了输入图像外,深度提升还需要一个基线图像。再次,我们使用所有像素值设置为零的图像作为基线图像。深度提升计算非线性激活输出相对于从基线图像到输入图像的输入变化的梯度(图 13.9)。以下代码使用 Captum 提供的 DeepLift 模块计算梯度,并将这些梯度叠加显示在原始输入图像上:
deep_lift = DeepLift(model)
attributed_dl = deep_lift.attribute(captum_input, target=sample_targets[0], baselines=captum_input * 0, return_convergence_delta=False)
attributed_dl = np.reshape(attributed_dl.squeeze(0).cpu().detach().numpy(), (28, 28, 1))
_ = viz.visualize_image_attr(attributed_dl, orig_image, method="blended_heat_map",sign="all",show_colorbar=True, title="Overlayed DeepLift")

你应该看到以下输出:

图 13.13 – 覆盖的 deeplift

图 13.13 – 覆盖的 deeplift

再次强调,梯度值在包含数字0的像素周围是极端的。

这就结束了本练习和本节。Captum 提供了更多的模型解释技术,例如LayerConductanceGradCAMSHAP [13.6]。模型解释性是一个活跃的研究领域,因此像 Captum 这样的库可能会迅速发展。在不久的将来,可能会开发出更多类似的库,这些库将使模型解释成为机器学习生命周期的标准组成部分。

摘要

在本章中,我们简要探讨了如何使用 PyTorch 解释或解读深度学习模型所做决策的方法。

在本书的下一章中,我们将学习如何在 PyTorch 上快速训练和测试机器学习模型——这是一个用于快速迭代各种机器学习想法的技能。我们还将讨论一些能够使用 PyTorch 进行快速原型设计的深度学习库和框架。