PyTorch 深度学习(GPT 重译)(六)(4)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: PyTorch 深度学习(GPT 重译)(六)

PyTorch 深度学习(GPT 重译)(六)(3)https://developer.aliyun.com/article/1485255

15.3.4 脚本化追踪的间隙

在更复杂的模型中,例如用于检测的 Fast R-CNN 系列或用于自然语言处理的循环网络,像for循环这样的控制流位需要进行脚本化。同样,如果我们需要灵活性,我们会找到追踪器警告的代码片段。

代码清单 15.8 来自 utils/unet.py

class UNetUpBlock(nn.Module):
    ...
    def center_crop(self, layer, target_size):
        _, _, layer_height, layer_width = layer.size()
        diff_y = (layer_height - target_size[0]) // 2
        diff_x = (layer_width - target_size[1]) // 2
        return layer[:, :, diff_y:(diff_y + target_size[0]), diff_x:(diff_x + target_size[1])]                            # ❶
    def forward(self, x, bridge):
        ...
        crop1 = self.center_crop(bridge, up.shape[2:])
 ...

❶ 追踪器在这里发出警告。

发生的情况是,JIT 神奇地用包含相同信息的 1D 整数张量替换了形状元组up.shape。现在切片[2:]和计算diff_xdiff_y都是可追踪的张量操作。然而,这并不能拯救我们,因为切片然后需要 Python int;在那里,JIT 的作用范围结束,给我们警告。

但是我们可以通过一种简单直接的方式解决这个问题:我们对center_crop进行脚本化。我们通过将up传递给脚本化的center_crop并在那里提取大小来略微更改调用者和被调用者之间的切割。除此之外,我们所需的只是添加@torch.jit.script装饰器。结果是以下代码,使 U-Net 模型可以无警告地进行追踪。

代码清单 15.9 从 utils/unet.py 重写的节选

@torch.jit.script
def center_crop(layer, target):                         # ❶
    _, _, layer_height, layer_width = layer.size()
    _, _, target_height, target_width = target.size()   # ❷
    diff_y = (layer_height - target_height) // 2
    diff_x = (layer_width - target_width]) // 2
    return layer[:, :, diff_y:(diff_y + target_height),  diff_x:(diff_x + target_width)]                     # ❸
class UNetUpBlock(nn.Module):
    ...
    def forward(self, x, bridge):
        ...
        crop1 = center_crop(bridge, up)                 # ❹
  ...

❶ 更改签名,接受目标而不是目标大小

❷ 在脚本化部分内获取大小

❸ 索引使用我们得到的大小值。

❹ 我们调整我们的调用以传递上而不是大小。

我们可以选择的另一个选项–但我们这里不会使用–是将不可脚本化的内容移入在 C++ 中实现的自定义运算符中。TorchVision 库为 Mask R-CNN 模型中的一些特殊操作执行此操作。

15.4 LibTorch:在 C++ 中使用 PyTorch

我们已经看到了各种导出模型的方式,但到目前为止,我们使用了 Python。现在我们将看看如何放弃 Python 直接使用 C++。

让我们回到从马到斑马的 CycleGAN 示例。我们现在将从第 15.2.3 节中获取 JITed 模型,并在 C++ 程序中运行它。

15.4.1 从 C++ 运行 JITed 模型

在 C++ 中部署 PyTorch 视觉模型最困难的部分是选择一个图像库来选择数据。⁸ 在这里,我们选择了非常轻量级的库 CImg (cimg.eu)。如果你非常熟悉 OpenCV,你可以调整代码以使用它;我们只是觉得 CImg 对我们的阐述最容易。

运行 JITed 模型非常简单。我们首先展示图像处理;这并不是我们真正想要的,所以我们会很快地完成这部分。⁹

代码清单 15.10 cyclegan_jit.cpp

#include "torch/script.h"                                       # ❶
#define cimg_use_jpeg
#include "CImg.h"
using namespace cimg_library;
int main(int argc, char **argv) {
  CImg<float> image(argv[2]);                                   # ❷
  image = image.resize(227, 227);                               # ❸
  // ...here we need to produce an output tensor from input
  CImg<float> out_img(output.data_ptr<float>(), output.size(2), # ❹
                      output.size(3), 1, output.size(1));
  out_img.save(argv[3]);                                        # ❺
  return 0;
}

❶ 包括 PyTorch 脚本头文件和具有本地 JPEG 支持的 CImg

❷ 将图像加载并解码为浮点数组

❸ 调整为较小的尺寸

❹ 方法 data_ptr() 给我们一个指向张量存储的指针。有了它和形状信息,我们可以构建输出图像。

❺ 保存图像

对于 PyTorch 部分,我们包含了一个 C++ 头文件 torch/script.h。然后我们需要设置并包含 CImg 库。在 main 函数中,我们从命令行中加载一个文件中的图像并调整大小(在 CImg 中)。所以现在我们有一个 CImg 变量 image 中的 227 × 227 图像。在程序的末尾,我们将从我们的形状为 (1, 3, 277, 277) 的张量创建一个相同类型的 out_img 并保存它。

不要担心这些细节。它们不是我们想要学习的 PyTorch C++,所以我们可以直接接受它们。

实际的计算也很简单。我们需要从图像创建一个输入张量,加载我们的模型,并将输入张量通过它运行。

代码清单 15.11 cyclegan_jit.cpp

auto input_ = torch::tensor(
    torch::ArrayRef<float>(image.data(), image.size()));  # ❶
  auto input = input_.reshape({1, 3, image.height(),
                   image.width()}).div_(255);             # ❷
  auto module = torch::jit::load(argv[1]);                # ❸
  std::vector<torch::jit::IValue> inputs;                 # ❹
  inputs.push_back(input);
  auto output_ = module.forward(inputs).toTensor();       # ❺
  auto output = output_.contiguous().mul_(255);           # ❻

❶ 将图像数据放入张量中

❷ 重新调整和重新缩放以从 CImg 约定转换为 PyTorch 的

❸ 从文件加载 JITed 模型或函数

❹ 将输入打包成一个(单元素)IValues 向量

❺ 调用模块并提取结果张量。为了效率,所有权被移动,所以如果我们保留了 IValue,之后它将为空。

❻ 确保我们的结果是连续的

从第三章中回想起,PyTorch 将张量的值保存在特定顺序的大块内存中。CImg 也是如此,我们可以使用 image.data() 获取指向此内存块的指针(作为 float 数组),并使用 image.size() 获取元素的数量。有了这两个,我们可以创建一个稍微更智能的引用:一个 torch::ArrayRef(这只是指针加大小的简写;PyTorch 在 C++ 级别用于数据但也用于返回大小而不复制)。然后我们可以将其解析到 torch::tensor 构造函数中,就像我们对列表做的那样。

提示 有时候你可能想要使用类似工作的 torch::from_blob 而不是 torch::tensor。区别在于 tensor 会复制数据。如果你不想复制,可以使用 from_blob,但是你需要确保在张量的生命周期内底层内存是可用的。

我们的张量只有 1D,所以我们需要重新调整它。方便的是,CImg 使用与 PyTorch 相同的顺序(通道、行、列)。如果不是这样,我们需要调整重新调整并排列轴,就像我们在第四章中所做的那样。由于 CImg 使用 0…255 的范围,而我们使我们的模型使用 0…1,所以我们在这里除以后面再乘以。当然,这可以被吸收到模型中,但我们想重用我们的跟踪模型。

避免的一个常见陷阱:预处理和后处理

当从一个库切换到另一个库时,很容易忘记检查转换步骤是否兼容。除非我们查看 PyTorch 和我们使用的图像处理库的内存布局和缩放约定,否则它们是不明显的。如果我们忘记了,我们将因为没有得到预期的结果而感到失望。

在这里,模型会变得疯狂,因为它接收到非常大的输入。然而,最终,我们模型的输出约定是在 0 到 1 的范围内给出 RGB 值。如果我们直接将其与 CImg 一起使用,结果看起来会全是黑色。

其他框架有其他约定:例如 OpenCV 喜欢将图像存储为 BGR 而不是 RGB,需要我们翻转通道维度。我们始终要确保在部署中向模型提供的输入与我们在 Python 中输入的相同。

使用 torch::jit::load 加载跟踪模型非常简单。接下来,我们必须处理 PyTorch 引入的一个在 Python 和 C++ 之间桥接的抽象:我们需要将我们的输入包装在一个 IValue(或多个 IValue)中,这是任何值的通用数据类型。 JIT 中的一个函数接收一个 IValue 向量,所以我们声明这个向量,然后 push_back 我们的输入张量。这将自动将我们的张量包装成一个 IValue。我们将这个 IValue 向量传递给前向并得到一个返回的单个 IValue。然后我们可以使用 .toTensor 解包结果 IValue 中的张量。

这里我们了解一下 IValue:它们有一个类型(这里是 Tensor),但它们也可以持有 int64_tdouble 或一组张量。例如,如果我们有多个输出,我们将得到一个持有张量列表的 IValue,这最终源自于 Python 的调用约定。当我们使用 .toTensorIValue 中解包张量时,IValue 将转移所有权(变为无效)。但让我们不要担心这个;我们得到了一个张量。因为有时模型可能返回非连续数据(从第三章的存储中存在间隙),但 CImg 合理地要求我们提供一个连续的块,我们调用 contiguous。重要的是,我们将这个连续的张量分配给一个在使用底层内存时处于作用域内的变量。就像在 Python 中一样,如果 PyTorch 发现没有张量在使用内存,它将释放内存。

所以让我们编译这个!在 Debian 或 Ubuntu 上,你需要安装 cimg-devlibjpeg-devlibx11-dev 来使用 CImg

你可以从 PyTorch 页面下载一个 PyTorch 的 C++ 库。但考虑到我们已经安装了 PyTorch,¹⁰我们可能会选择使用它;它已经包含了我们在 C++ 中所需的一切。我们需要知道我们的 PyTorch 安装位置在哪里,所以打开 Python 并检查 torch.__file__,它可能会显示 /usr/local/lib/python3.7/dist-packages/ torch/init.py。这意味着我们需要的 CMake 文件在 /usr/local/lib/python3.7/dist-packages/torch/share/cmake/ 中。

尽管对于一个单个源文件项目来说使用 CMake 似乎有点大材小用,但链接到 PyTorch 有点复杂;因此我们只需使用以下内容作为一个样板 CMake 文件。¹¹

列表 15.12 CMakeLists.txt

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(cyclegan-jit)                                         # ❶
find_package(Torch REQUIRED)                                  # ❷
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}")
add_executable(cyclegan-jit cyclegan_jit.cpp)                 # ❸
target_link_libraries(cyclegan-jit pthread jpeg X11)          # ❹
target_link_libraries(cyclegan-jit "${TORCH_LIBRARIES}")
set_property(TARGET cyclegan-jit PROPERTY CXX_STANDARD 14)

❶ 项目名称。用你自己的项目名称替换这里和其他行。

❷ 我们需要 Torch。

❸ 我们想要从 cyclegan_jit.cpp 源文件编译一个名为 cyclegan-jit 的可执行文件。

❹ 链接到 CImg 所需的部分。CImg 本身是全包含的,所以这里不会出现。

最好在源代码所在的子目录中创建一个构建目录,然后在其中运行 CMake,如¹² CMAKE_PREFIX_PATH=/usr/local/lib/python3.7/ dist-packages/torch/share/cmake/ cmake ..,最后 make。这将构建 cyclegan-jit 程序,然后我们可以运行如下:

./cyclegan-jit ../traced_zebra_model.pt  ../../data/p1ch2/horse.jpg /tmp/z.jpg

我们刚刚在没有 Python 的情况下运行了我们的 PyTorch 模型。太棒了!如果你想发布你的应用程序,你可能想将 /usr/local/lib/python3.7/dist-packages/torch/lib 中的库复制到可执行文件所在的位置,这样它们就会始终被找到。

15.4.2 从头开始的 C++:C++ API

C++ 模块化 API 旨在感觉很像 Python 的 API。为了体验一下,我们将把 CycleGAN 生成器翻译成在 C++ 中本地定义的模型,但没有 JIT。但是,我们需要预训练的权重,因此我们将保存模型的跟踪版本(在这里重要的是跟踪模型而不是函数)。

我们将从一些行政细节开始:包括和命名空间。

列表 15.13 cyclegan_cpp_api.cpp

#include <torch/torch.h>   # ❶
#define cimg_use_jpeg
#include <CImg.h>
using torch::Tensor;       # ❷

❶ 导入一站式 torch/torch.h 头文件和 CImg

❷ 拼写torch::Tensor可能很繁琐,因此我们将名称导入主命名空间。

当我们查看文件中的源代码时,我们发现ConvTransposed2d是临时定义的,理想情况下应该从标准库中获取。问题在于 C++ 模块化 API 仍在开发中;并且在 PyTorch 1.4 中,预制的ConvTranspose2d模块无法在Sequential中使用,因为它需要一个可选的第二个参数。通常我们可以像我们为 Python 所做的那样留下Sequential,但我们希望我们的模型具有与第二章 Python CycleGAN 生成器相同的结构。

接下来,让我们看看残差块。

列表 15.14 cyclegan_cpp_api.cpp 中的残差块

struct ResNetBlock : torch::nn::Module {
  torch::nn::Sequential conv_block;
  ResNetBlock(int64_t dim)
      : conv_block(                                   # ❶
           torch::nn::ReflectionPad2d(1),
           torch::nn::Conv2d(torch::nn::Conv2dOptions(dim, dim, 3)),
           torch::nn::InstanceNorm2d(
           torch::nn::InstanceNorm2dOptions(dim)),
           torch::nn::ReLU(/*inplace=*/true),
        torch::nn::ReflectionPad2d(1),
           torch::nn::Conv2d(torch::nn::Conv2dOptions(dim, dim, 3)),
           torch::nn::InstanceNorm2d(
           torch::nn::InstanceNorm2dOptions(dim))) {
    register_module("conv_block", conv_block);        # ❷
  }
  Tensor forward(const Tensor &inp) {
    return inp + conv_block->forward(inp);            # ❸
  }
};.

❶ 初始化 Sequential,包括其子模块

❷ 始终记得注册您分配的模块,否则会发生糟糕的事情!

❸ 正如我们所预期的那样,我们的前向函数非常简单。

就像我们在 Python 中所做的那样,我们注册torch::nn::Module的子类。我们的残差块有一个顺序的conv_block子模块。

就像我们在 Python 中所做的那样,我们需要初始化我们的子模块,特别是Sequential。我们使用 C++ 初始化语句来做到这一点。这类似于我们在 Python 中在__init__构造函数中构造子模块的方式。与 Python 不同,C++ 没有启发式和挂钩功能,使得将__setattr__重定向以结合对成员的赋值和注册成为可能。

由于缺乏关键字参数使得带有默认参数的参数规范变得笨拙,模块(如张量工厂函数)通常需要一个options参数。Python 中的可选关键字参数对应于我们可以链接的选项对象的方法。例如,我们需要转换的 Python 模块nn.Conv2d(in_channels, out_channels, kernel_size, stride=2, padding=1)对应于torch::nn::Conv2d(torch::nn::Conv2dOptions (in_channels, out_channels, kernel_size).stride(2).padding(1))。这有点繁琐,但您正在阅读这篇文章是因为您热爱 C++,并且不会被它让您跳过的环节吓倒。

我们应始终确保注册和分配给成员的同步,否则事情将不会按预期进行:例如,在训练期间加载和更新参数将发生在注册的模块上,但实际被调用的模块是一个成员。这种同步在 Python 的 nn.Module 类后台完成,但在 C++ 中不是自动的。未能这样做将给我们带来许多头痛。

与我们在 Python 中所做的(应该!)相反,我们需要为我们的模块调用m->forward(...)。一些模块也可以直接调用,但对于Sequential,目前不是这种情况。

最后关于调用约定的评论是:根据您是否修改传递给函数的张量,张量参数应始终作为const Tensor&传递,对于不会更改的张量,或者如果它们被更改,则传递Tensor。应返回张量作为Tensor。错误的参数类型,如非 const 引用(Tensor&),将导致无法解析的编译器错误。

在主生成器类中,我们将更加密切地遵循 C++ API 中的典型模式,通过将我们的类命名为 ResNetGeneratorImpl 并使用 TORCH_MODULE 宏将其提升为 torch 模块 ResNetGenerator。背景是我们希望大部分处理模块作为引用或共享指针。包装类实现了这一点。

列表 15.15 cyclegan_cpp_api.cpp 中的 ResNetGenerator

struct ResNetGeneratorImpl : torch::nn::Module {
  torch::nn::Sequential model;
  ResNetGeneratorImpl(int64_t input_nc = 3, int64_t output_nc = 3,
                      int64_t ngf = 64, int64_t n_blocks = 9) {
    TORCH_CHECK(n_blocks >= 0);
    model->push_back(torch::nn::ReflectionPad2d(3));    # ❶
    ...                                                 # ❷
      model->push_back(torch::nn::Conv2d(
          torch::nn::Conv2dOptions(ngf * mult, ngf * mult * 2, 3)
              .stride(2)
              .padding(1)));                            # ❸
    ...
    register_module("model", model);
  }
  Tensor forward(const Tensor &inp) { return model->forward(inp); }
};
TORCH_MODULE(ResNetGenerator);                          # ❹

❶ 在构造函数中向 Sequential 容器添加模块。这使我们能够在 for 循环中添加可变数量的模块。

❷ 使我们免于重复一些繁琐的事情

❸ Options 的一个示例

❹ 在我们的 ResNetGeneratorImpl 类周围创建一个包装器 ResNetGenerator。尽管看起来有些过时,但匹配的名称在这里很重要。

就是这样–我们定义了 Python ResNetGenerator 模型的完美 C++ 对应物。现在我们只需要一个 main 函数来加载参数并运行我们的模型。加载图像使用 CImg 并将图像转换为张量,再将张量转换回图像与上一节中相同。为了增加一些变化,我们将显示图像而不是将其写入磁盘。

列表 15.16 cyclegan_cpp_api.cpp main

ResNetGenerator model;                                                    # ❶
  ...
  torch::load(model, argv[1]);                                            # ❷
  ...
  cimg_library::CImg<float> image(argv[2]);
  image.resize(400, 400);
  auto input_ =
      torch::tensor(torch::ArrayRef<float>(image.data(), image.size()));
  auto input = input_.reshape({1, 3, image.height(), image.width()});
  torch::NoGradGuard no_grad;                                             # ❸
  model->eval();                                                          # ❹
  auto output = model->forward(input);                                    # ❺
  ...
  cimg_library::CImg<float> out_img(output.data_ptr<float>(),
                    output.size(3), output.size(2),
                    1, output.size(1));
  cimg_library::CImgDisplay disp(out_img, "See a C++ API zebra!");        # ❻
  while (!disp.is_closed()) {
    disp.wait();
  }

❶ 实例化我们的模型

❷ 加载参数

❸ 声明一个守卫变量相当于 torch.no_grad() 上下文。如果需要限制关闭梯度的时间,可以将其放在 { … } 块中。

❹ 就像在 Python 中一样,打开 eval 模式(对于我们的模型来说可能并不严格相关)。

❺ 再次调用 forward 而不是 model。

❻ 显示图像时,我们需要等待按键而不是立即退出程序。

有趣的变化在于我们如何创建和运行模型。正如预期的那样,我们通过声明模型类型的变量来实例化模型。我们使用 torch::load 加载模型(这里重要的是我们包装了模型)。虽然这看起来对于 PyTorch 从业者来说非常熟悉,但请注意它将在 JIT 保存的文件上工作,而不是 Python 序列化的状态字典。

运行模型时,我们需要相当于 with torch.no_grad(): 的功能。这是通过实例化一个类型为 NoGradGuard 的变量并在我们不希望梯度时保持其范围来实现的。就像在 Python 中一样,我们调用 model->eval() 将模型设置为评估模式。这一次,我们调用 model->forward 传入我们的输入张量并得到一个张量作为结果–不涉及 JIT,因此我们不需要 IValue 的打包和解包。

哎呀。对于我们这些 Python 粉丝来说,在 C++ 中编写这个是很费力的。我们很高兴我们只承诺在这里进行推理,但当然 LibTorch 也提供了优化器、数据加载器等等。使用 API 的主要原因当然是当你想要创建模型而 JIT 和 Python 都不合适时。

为了您的方便,CMakeLists.txt 中还包含了构建 cyclegan-cpp-api 的说明,因此构建就像在上一节中一样简单。

我们可以运行程序如下

./cyclegan_cpp_api ../traced_zebra_model.pt ../../data/p1ch2/horse.jpg

但我们知道模型会做什么,不是吗?

15.5 走向移动

作为部署模型的最后一个变体,我们将考虑部署到移动设备。当我们想要将我们的模型带到移动设备时,通常会考虑 Android 和/或 iOS。在这里,我们将专注于 Android。

PyTorch 的 C++ 部分–LibTorch–可以编译为 Android,并且我们可以通过使用 Android Java Native Interface (JNI) 编写的应用程序从 Java 中访问它。但实际上我们只需要从 PyTorch 中使用少量函数–加载 JIT 模型,将输入转换为张量和 IValue,通过模型运行它们,并将结果返回。为了避免使用 JNI 的麻烦,PyTorch 开发人员将这些函数封装到一个名为 PyTorch Mobile 的小型库中。

在 Android 中开发应用程序的标准方式是使用 Android Studio IDE,我们也将使用它。但这意味着有几十个管理文件–这些文件也会随着 Android 版本的更改而改变。因此,我们专注于将 Android Studio 模板(具有空活动的 Java 应用程序)转换为一个拍照、通过我们的斑马 CycleGAN 运行图片并显示结果的应用程序的部分。遵循本书的主题,我们将在示例应用程序中高效处理 Android 部分(与编写 PyTorch 代码相比可能会更痛苦)。

要使模板生动起来,我们需要做三件事。首先,我们需要定义一个用户界面。为了尽可能简单,我们有两个元素:一个名为headlineTextView,我们可以点击以拍摄和转换图片;以及一个用于显示我们图片的ImageView,我们称之为image_view。我们将把拍照留给相机应用程序(在应用程序中可能会避免这样做以获得更流畅的用户体验),因为直接处理相机会模糊我们专注于部署 PyTorch 模型的焦点。

然后,我们需要将 PyTorch 作为依赖项包含进来。这是通过编辑我们应用程序的 build.gradle 文件并添加pytorch_androidpytorch_android_torchvision来完成的。

15.17 build.gradle 的添加部分

dependencies {                                                     # ❶
  ...
  implementation 'org.pytorch:pytorch_android:1.4.0'               # ❷
  implementation 'org.pytorch:pytorch_android_torchvision:1.4.0'   # ❸
}

❶ 依赖部分很可能已经存在。如果没有,请在底部添加。

❷ pytorch_android 库获取了文本中提到的核心内容。

❸ 辅助库 pytorch_android_torchvision–与其更大的 TorchVision 兄弟相比可能有点自负地命名–包含一些将位图对象转换为张量的实用程序,但在撰写本文时没有更多内容。

我们需要将我们的跟踪模型添加为资产。

最后,我们可以进入我们闪亮应用的核心部分:从活动派生的 Java 类,其中包含我们的主要代码。我们这里只讨论一个摘录。它以导入和模型设置开始。

15.18 MainActivity.java 第 1 部分

...
import org.pytorch.IValue;                                                 # ❶
import org.pytorch.Module;
import org.pytorch.Tensor;
import org.pytorch.torchvision.TensorImageUtils;
...
public class MainActivity extends AppCompatActivity {
  private org.pytorch.Module model;                                        # ❷
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    try {                                                                  # ❸
      model = Module.load(assetFilePath(this, "traced_zebra_model.pt"));   # ❹
    } catch (IOException e) {
      Log.e("Zebraify", "Error reading assets", e);
      finish();
    }
    ...
  }
  ...
}

❶ 你喜欢导入吗?

❷ 包含我们的 JIT 模型

❸ 在 Java 中我们必须捕获异常。

❹ 从文件加载模块

我们需要从org.pytorch命名空间导入一些内容。在 Java 的典型风格中,我们导入IValueModuleTensor,它们的功能符合我们的预期;以及org.pytorch.torchvision.TensorImageUtils类,其中包含在张量和图像之间转换的实用函数。

首先,当然,我们需要声明一个变量来保存我们的模型。然后,在我们的应用启动时–在我们的活动的onCreate中–我们将使用Model.load方法从给定的位置加载模块。然而,有一个小复杂之处:应用程序的数据是由供应商提供的资产,这些资产不容易从文件系统中访问。因此,一个名为assetFilePath的实用方法(取自 PyTorch Android 示例)将资产复制到文件系统中的一个位置。最后,在 Java 中,我们需要捕获代码抛出的异常,除非我们想要(并且能够)依次声明我们编写的方法抛出异常。

当我们使用 Android 的Intent机制从相机应用程序获取图像时,我们需要运行它通过我们的模型并显示它。这发生在onActivityResult事件处理程序中。

15.19 MainActivity.java,第 2 部分

@Override
protected void onActivityResult(int requestCode, int resultCode,
                                Intent data) {
  if (requestCode == REQUEST_IMAGE_CAPTURE &&
      resultCode == RESULT_OK) {                                          # ❶
    Bitmap bitmap = (Bitmap) data.getExtras().get("data");
    final float[] means = {0.0f, 0.0f, 0.0f};                             # ❷
    final float[] stds = {1.0f, 1.0f, 1.0f};
    final Tensor inputTensor = TensorImageUtils.bitmapToFloat32Tensor(    # ❸
        bitmap, means, stds);
    final Tensor outputTensor = model.forward(                            # ❹
        IValue.from(inputTensor)).toTensor();
    Bitmap output_bitmap = tensorToBitmap(outputTensor, means, stds,
        Bitmap.Config.RGB_565);                                           # ❺
    image_view.setImageBitmap(output_bitmap);
  }
}

❶ 当相机应用程序拍照时执行此操作。

❷ 执行归一化,但默认情况下图像范围为 0…1,因此我们不需要转换:即具有 0 偏移和 1 的缩放除数。

❸ 从位图获取张量,结合 TorchVision 的 ToTensor 步骤(将其转换为介于 0 和 1 之间的浮点张量)和 Normalize

❹ 这看起来几乎和我们在 C++中做的一样。

❺ tensorToBitmap 是我们自己的创造。

将从 Android 获取的位图转换为张量由TensorImageUtils.bitmapToFloat32Tensor函数(静态方法)处理,该函数除了bitmap之外还需要两个浮点数组meansstds。在这里,我们指定输入数据(集)的均值和标准差,然后将其映射为具有零均值和单位标准差的数据,就像 TorchVision 的Normalize变换一样。Android 已经将图像给我们提供在 0…1 范围内,我们需要将其馈送到我们的模型中,因此我们指定均值为 0,标准差为 1,以防止归一化改变我们的图像。

在实际调用model.forward时,我们执行与在 C++中使用 JIT 时相同的IValue包装和解包操作,只是我们的forward接受一个IValue而不是一个向量。最后,我们需要回到位图。在这里,PyTorch 不会帮助我们,因此我们需要定义自己的tensorToBitmap(并向 PyTorch 提交拉取请求)。我们在这里不详细介绍,因为这些细节很繁琐且充满复制(从张量到float[]数组到包含 ARGB 值的int[]数组到位图),但事实就是如此。它被设计为bitmapToFloat32Tensor的逆过程。

图 15.5 我们的 CycleGAN 斑马应用

这就是我们需要做的一切,就可以将 PyTorch 引入 Android。使用我们在这里留下的最小代码补充来请求一张图片,我们就有了一个看起来像图 15.5 中所见的Zebraify Android 应用程序。干得好!¹⁶

我们应该注意到,我们在 Android 上使用了 PyTorch 的完整版本,其中包含所有操作。一般来说,这也会包括您在特定任务中不需要的操作,这就引出了一个问题,即我们是否可以通过将它们排除在外来节省一些空间。事实证明,从 PyTorch 1.4 开始,您可以构建一个定制版本的 PyTorch 库,其中只包括您需要的操作(参见pytorch.org/mobile/android/#custom-build)。

15.5.1 提高效率:模型设计和量化

如果我们想更详细地探索移动端,我们的下一步是尝试使我们的模型更快。当我们希望减少模型的内存和计算占用空间时,首先要看的是简化模型本身:也就是说,使用更少的参数和操作计算相同或非常相似的输入到输出的映射。这通常被称为蒸馏。蒸馏的细节各不相同–有时我们尝试通过消除小或无关的权重来缩小每个权重;在其他示例中,我们将网络的几层合并为一层(DistilBERT),甚至训练一个完全不同、更简单的模型来复制较大模型的输出(OpenNMT 的原始 CTranslate)。我们提到这一点是因为这些修改很可能是使模型运行更快的第一步。

另一种方法是减少每个参数和操作的占用空间:我们将模型转换为使用整数(典型选择是 8 位)而不是以浮点数的形式花费通常的 32 位每个参数。这就是量化。¹⁸

PyTorch 确实为此目的提供了量化张量。它们被公开为一组类似于torch.floattorch.doubletorch.long的标量类型(请参阅第 3.5 节)。最常见的量化张量标量类型是torch.quint8torch.qint8,分别表示无符号和有符号的 8 位整数。PyTorch 在这里使用单独的标量类型,以便使用我们在第 3.11 节简要介绍的分派机制。

使用 8 位整数而不是 32 位浮点数似乎能够正常工作可能会让人感到惊讶;通常结果会有轻微的降级,但不会太多。有两个因素似乎起到作用:如果我们将舍入误差视为基本上是随机的,并且将卷积和线性层视为加权平均,我们可能期望舍入误差通常会抵消。¹⁹ 这允许将相对精度从 32 位浮点数的 20 多位减少到有符号整数提供的 7 位。量化的另一件事(与使用 16 位浮点数进行训练相反)是从浮点数转换为固定精度(每个张量或通道)。这意味着最大值被解析为 7 位精度,而是最大值的八分之一的值仅为 7 - 3 = 4 位。但如果像 L1 正则化(在第八章中简要提到)这样的事情起作用,我们可能希望类似的效果使我们在量化时能够为权重中的较小值提供更少的精度。在许多情况下,确实如此。

量化功能于 PyTorch 1.3 首次亮相,但在 PyTorch 1.4 中在支持的操作方面仍有些粗糙。不过,它正在迅速成熟,我们建议如果您真的关心计算效率的部署,不妨试试看。

15.6 新兴技术:企业 PyTorch 模型服务

我们可能会问自己,迄今为止讨论的所有部署方面是否都需要像它们现在这样涉及大量编码。当然,有人编写所有这些代码是很常见的。截至 2020 年初,当我们忙于为这本书做最后的润色时,我们对不久的将来寄予厚望;但与此同时,我们感觉到部署领域将在夏季发生重大变化。

目前,RedisAI(github.com/RedisAI/redisai-py)中的一位作者正在等待将 Redis 的优势应用到我们的模型中。PyTorch 刚刚实验性发布了 TorchServe(在这本书完成后,请查看pytorch.org/ blog/pytorch-library-updates-new-model-serving-library/#torchserve-experimental)。

同样,MLflow(mlflow.org)正在不断扩展更多支持,而 Cortex(cortex.dev)希望我们使用它来部署模型。对于更具体的信息检索任务,还有 EuclidesDB(euclidesdb.readthedocs.io/ en/latest)来执行基于 AI 的特征数据库。

令人兴奋的时刻,但不幸的是,它们与我们的写作计划不同步。我们希望在第二版(或第二本书)中有更多内容可以告诉您!

15.7 结论

这结束了我们如何将我们的模型部署到我们想要应用它们的地方的简短介绍。虽然现成的 Torch 服务在我们撰写本文时还不够完善,但当它到来时,您可能会希望通过 JIT 导出您的模型–所以您会很高兴我们在这里经历了这一过程。与此同时,您现在知道如何将您的模型部署到网络服务、C++ 应用程序或移动设备上。我们期待看到您将会构建什么!

希望我们也实现了这本书的承诺:对深度学习基础知识有所了解,并对 PyTorch 库感到舒适。我们希望您阅读的过程和我们写作的过程一样愉快。²⁰

15.8 练习

当我们结束 使用 PyTorch 进行深度学习 时,我们为您准备了最后一个练习:

  1. 选择一个让您感到兴奋的项目。Kaggle 是一个很好的开始地方。开始吧。

您已经掌握了成功所需的技能并学会了必要的工具。我们迫不及待想知道接下来您会做什么;在书的论坛上给我们留言,让我们知道!

15.9 总结

  • 我们可以通过将 PyTorch 模型包装在 Python Web 服务器框架(如 Flask)中来提供 PyTorch 模型的服务。
  • 通过使用 JIT 模型,我们可以避免即使从 Python 调用它们时也避免 GIL,这对于服务是一个好主意。
  • 请求批处理和异步处理有助于有效利用资源,特别是在 GPU 上进行推理时。
  • 要将模型导出到 PyTorch 之外,ONNX 是一个很好的格式。ONNX Runtime 为许多目的提供后端支持,包括树莓派。
  • JIT 允许您轻松导出和运行任意 PyTorch 代码在 C++中或在移动设备上。
  • 追踪是获得 JIT 模型的最简单方法;对于一些特别动态的部分,您可能需要使用脚本。
  • 对于运行 JIT 和本地模型,C++(以及越来越多的其他语言)也有很好的支持。
  • PyTorch Mobile 让我们可以轻松地将 JIT 模型集成到 Android 或 iOS 应用程序中。
  • 对于移动部署,我们希望简化模型架构并在可能的情况下对模型进行量化。
  • 几个部署框架正在兴起,但标准尚不太明显。

¹ 为了安全起见,请勿在不受信任的网络上执行此操作。

² 或者对于 Python3 使用pip3。您可能还希望从 Python 虚拟环境中运行它。

³ 早期公开讨论 Flask 为 PyTorch 模型提供服务的不足之处之一是 Christian Perone 的“PyTorch under the Hood”,mng.bz/xWdW

⁴ 高级人士将这些异步函数称为生成器,有时更宽松地称为协程 en.wikipedia.org/wiki/Coroutine

⁵ 另一种选择可能是放弃计时器,只有在队列不为空时才运行。这可能会运行较小的“第一”批次,但对于大多数应用程序来说,整体性能影响可能不会太大。

⁶ 代码位于github.com/microsoft/onnxruntime,但请务必阅读隐私声明!目前,自行构建 ONNX Runtime 将为您提供一个不会向母公司发送信息的软件包。

⁷ 严格来说,这将模型追踪为一个函数。最近,PyTorch 获得了使用torch.jit.trace_module保留更多模块结构的能力,但对我们来说,简单的追踪就足够了。

⁸ 但 TorchVision 可能会开发一个方便的函数来加载图像。

⁹ 该代码适用于 PyTorch 1.4 及以上版本。在 PyTorch 1.3 之前的版本中,您需要使用data代替data_ptr

¹⁰ 我们希望您一直在尝试阅读的内容。

¹¹ 代码目录有一个稍长版本,以解决 Windows 问题。

¹² 您可能需要将路径替换为您的 PyTorch 或 LibTorch 安装位置。请注意,与 Python 相比,C++库在兼容性方面可能更挑剔:如果您使用的是支持 CUDA 的库,则需要安装匹配的 CUDA 头文件。如果您收到关于“Caffe2 使用 CUDA”的神秘错误消息,则需要安装一个仅支持 CPU 的库版本,但 CMake 找到了一个支持 CUDA 的库。

¹³ 这是对 PyTorch 1.3 的巨大改进,我们需要为 ReLU、ÌnstanceNorm2d和其他模块实现自定义模块。

¹⁴ 这有点模糊,因为你可以创建一个与输入共享内存并就地修改的新张量,但最好尽量避免这样做。

¹⁵ 我们对这个主题隐喻感到非常自豪。

¹⁶ 撰写时,PyTorch Mobile 仍然相对年轻,您可能会遇到一些问题。在 Pytorch 1.3 上,实际的 32 位 ARM 手机在模拟器中工作时颜色不正确。原因很可能是 ARM 上仅在使用的计算后端函数中存在错误。使用 PyTorch 1.4 和更新的手机(64 位 ARM)似乎效果更好。

¹⁷ 示例包括彩票假设和 WaveRNN。

¹⁸ 与量化相比,(部分)转向 16 位浮点数进行训练通常被称为减少或(如果某些位保持 32 位)混合精度训练。

¹⁹ 时髦的人们可能会在这里提到中心极限定理。确实,我们必须注意保持舍入误差的独立性(在统计意义上)。例如,我们通常希望零(ReLU 的一个显著输出)能够被精确表示。否则,所有的零将会在舍入中被完全相同的数量改变,导致误差累积而不是抵消。

²⁰ 实际上更多;写书真的很难!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(七)(4)
JavaScript 权威指南第七版(GPT 重译)(七)
24 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
33 0
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
42 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
60 0
|
13天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
93 2
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
13天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
55 4
|
13天前
|
XML 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(2)
JavaScript 权威指南第七版(GPT 重译)(六)
60 4
JavaScript 权威指南第七版(GPT 重译)(六)(2)
|
13天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(六)(1)
JavaScript 权威指南第七版(GPT 重译)(六)
28 3
JavaScript 权威指南第七版(GPT 重译)(六)(1)
|
13天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(五)(4)
JavaScript 权威指南第七版(GPT 重译)(五)
39 9
|
13天前
|
前端开发 JavaScript 程序员
JavaScript 权威指南第七版(GPT 重译)(五)(3)
JavaScript 权威指南第七版(GPT 重译)(五)
36 8