Python 环境之外部署机器学习模型曾经很困难。当目标平台是浏览器时,提供预测服务的事实标准是对服务器端推理引擎的 API 调用。由于许多原因,服务器端推理 API 不是最佳解决方案,并且机器学习模型更经常在本地部署。TensorFlow通过提供跨平台 API 在支持这一运动方面做得很好,但是我们中的许多人不想嫁给一个单一的生态系统。

Open Neural Network Exchange(ONNX) 项目自被 Microsoft 接手以来,一直在进行大规模的开发工作,并接近稳定状态。现在部署机器学习模型比以往任何时候都容易;使用您选择的机器学习框架在您选择的平台上进行训练,开箱即用的硬件加速。

今年 4 月,引入了onnxruntime-web(参见这个Pull Request)。onnxruntime-web使用 WebAssemblyonnxruntime推理引擎编译为wasm格式——是时候 WebAssembly 开始大展拳脚了。尤其是与 _WebGL 搭配使用时,我们突然在浏览器中拥有了 GPU 驱动的机器学习,非常酷。

在本教程中,我们将通过将预训练的 PyTorch 模型部署到浏览器来深入研究onnxruntime-web。我们将使用 AlexNet 作为我们的部署目标。 AlexNet 已在ImageNet 数据集上被训练为图像分类器,因此我们将构建一个图像分类器 - 没有比重新发明轮子更好的了。在本教程结束时,我们将构建一个捆绑的 Web 应用程序,它可以作为独立的静态网页运行,也可以集成到您选择的 JavaScript 框架中。

![](data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20version=%271.1%27%20width=%27800%27%20height=%27600%27/%3e)在浏览器中本机运行的 ImageNet 分类器

<img altu003d"ImageNet 分类器在浏览器中本地运行" srcsetu003d"/_next/image?urlu003dhttps%3A%2F%2Fcdn.hackernoon.com%2Fimages%2FR7Sat3yK8hMTNJSnRDmXnbNCkbI3-2da6267j.png&wu003d828&qu003d75 1x, / _next/image?urlu003dhttps%3A%2F%2Fcdn.hackernoon.com%2Fimages%2FR7Sat3yK8hMTNJSnRDmXnbNCkbI3-2da6267j.png&wu003d1920&qu003d75 2x" srcu003d"/_next/image?urlu003dhttps%3A%2F%2Fcdn .hackernoon.com%2Fimages%2FR7Sat3yK8hMTNJSnRDmXnbNCkbI3-2da6267j.png&wu003d1920&qu003d75" 解码u003d"async" data-nimgu003d"intrinsic" styleu003d"position:absolute;top:0;left:0;bottom:0;right: 0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-高度:100%;最大高度:100%;对象拟合:包含“类u003d“图像”加载u003d“懒惰”>

在浏览器中本地运行的 ImageNet 分类器

跳转到代码 →onnxruntime-web-tutorial

先决条件

您将需要导出为 ONNX 二进制 protobuf 文件的经过训练的机器学习模型。使用许多不同的深度学习框架有很多方法可以实现这一目标。在本教程中,我将使用PyTorch 文档中的 AlexNet 示例中导出的模型,下面的 python 代码片段将帮助您生成自己的模型。您也可以按照文档导出自己的 PyTorch 模型。如果您来自 Tensorflow,本教程将帮助您将模型导出到 ONNX。最后,ONNX 不仅以跨平台部署为荣,还以允许从所有主要深度学习框架导出。那些使用另一个深度学习框架的人应该能够在框架的文档中找到对导出到 ONNX 的支持。

import torch
import torchvision

dummy_input = torch.randn(1, 3, 224, 224)
model = torchvision.models.alexnet(pretrained=True)

input_names = ["input1"]
output_names = ["output1"]

torch.onnx.export(
  model, 
  dummy_input, 
  "alexnet.onnx", 
  verbose=True, 
  input_names=input_names,
  output_names=output_names
)

运行此脚本会创建一个文件alexnet.onnx,这是一个二进制 protobuf 文件,其中包含您导出的模型的网络结构和参数(在本例中为 AlexNet)。

ONNX 运行时网络

ONNX Runtime Web 是一个 JavaScript 库,用于在浏览器和 Node.js 上运行 ONNX 模型。 ONNX Runtime Web 采用了 WebAssemblyWebGL 技术,为 CPU 和 GPU 提供优化的 ONNX 模型推理运行时。

官方包托管在 npm 上,名称为onnxruntime-web。当使用捆绑器或工作服务器端时,可以使用npm install安装此包。但是,也可以使用脚本标签通过 CDN 交付代码。捆绑过程有点复杂,所以我们将从脚本标记方法开始,稍后再使用 npm 包。

推理会话

让我们从核心应用逻辑开始:模型推理。onnxruntime公开了一个名为InferenceSession的运行时对象,其方法.run()用于使用所需的输入启动前向传递。InferenceSessesion构造函数和附带的.run()方法都返回Promise,因此我们将在async上下文中运行整个过程。在实现任何浏览器元素之前,我们将检查我们的模型是否使用虚拟输入张量运行,记住我们之前在导出模型时定义的输入和输出名称和大小。

async function run() {
  try {
    // create a new session and load the AlexNet model.
    const session = await ort.InferenceSession.create('./alexnet.onnx');

    // prepare dummy input data
    const dims = [1, 3, 224, 224];
    const size = dims[0] * dims[1] * dims[2] * dims[3];
    const inputData = Float32Array.from({ length: size }, () => Math.random());

    // prepare feeds. use model input names as keys.
    const feeds = { input1: new ort.Tensor('float32', inputData, dims) };

    // feed inputs and run
    const results = await session.run(feeds);
    console.log(results.output1.data);
  } catch (e) {
    console.log(e);
  }
}
run();

然后我们实现一个简单的 HTML 模板index.html,它应该加载预编译的onnxruntime-web包和包含我们的代码的main.js

<!DOCTYPE html>
<html>
  <header>
    <title>ONNX Runtime Web - Tutorial</title>
  </header>
  <body>
    <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js">
    </script>
    <script src="main.js"></script>
  </body>
</html>

要运行它,我们可以使用light-server。如果您现在还没有启动npm项目,请在当前工作目录中运行npm init。完成设置后,安装 live-server (npm install light-server) 并使用npx light-server -s . -p 8080提供静态 HTML 页面。

您现在正在浏览器本地运行机器学习模型!要检查一切是否正常,请转到您的 Web 控制台并确保记录输出张量(AlexNet 很庞大,因此推断需要几秒钟是正常的)。

捆绑部署

接下来,我们将使用webpack来捆绑我们的依赖项,就像我们想将模型部署在由 ReactVue 等框架支持的 Javascript 应用程序中一样。通常捆绑是一个相对简单的过程,但是onnxruntime-web需要稍微复杂的webpack配置 - 这是因为 WebAssembly 用于提供本地组装的运行时。

浏览器支持,经典的陷阱,尤其是在使用尖端网络技术时。如果您的目标用户没有使用四种主要浏览器(Chrome、Edge、Firefox、Safari)之一,您可能希望推迟集成 WebAssembly 组件。有关 WebAssembly 支持和路线图的更多信息,请参见此处。

以下步骤基于官方_ONNX_文档提供的示例。我们假设您已经启动了一个 npm 项目。

1.安装依赖项。

npm install onnxruntime-web && npm install -D webpack webpack-cli copy-webpack

2.与其通过 CDN 加载onnxruntime-web模块,不如将脚本顶部的包更新为main.jsrequire

const ort = require('onnxruntime-web');

3.将_ONNX_Runtime团队定义的配置文件保存为webpack.config.js

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");

module.exports = () => {
    return {
        target: ['web'],
        entry: path.resolve(__dirname, 'main.js'),
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'bundle.min.js',
            library: {
                type: 'umd'
            }
        },
        plugins: [new CopyPlugin({
            // Use copy plugin to copy *.wasm to output folder.
            patterns: [{ from: 'node_modules/onnxruntime-web/dist/*.wasm', to: '[name][ext]' }]
        })],
        mode: 'production'
    }
};

4.运行npx webpack以编译捆绑包。

5\。最后,在重新加载服务器之前,我们需要更新index.html

  • 去掉ort.min.js脚本标签,停止从 CDN 加载编译好的包。

  • bundle.min.js加载代码依赖项(其中包含我们所有的依赖项捆绑并由webpack缩小)而不是main.js

index.html现在应该看起来像这样。

<!DOCTYPE html>
<html>
  <header>
    <title>ONNX Runtime Web Tutorial</title>
  </header>
  <body>
    <script src="bundle.min.js.js"></script>
  </body>
</html>

为了使构建和启动实时服务器更容易,您可以在package.json中定义buildserve脚本

"scripts": {
    "build": "npx webpack",
    "serve": "npm run build && npx light-server -s . -p 8080"
  }

图像分类器

让我们使用这个模型并实现图像分类管道。

我们将需要一些实用函数来加载、调整大小和显示图像 -canvas对象非常适合这一点。此外,图像分类系统通常在预处理管道中内置了很多魔法,使用numpy等框架在 Python 中实现这一点非常简单,不幸的是,JavaScript 并非如此。因此,我们必须从头开始执行预处理,将图像数据转换为正确的输入格式。

1. DOM 元素

我们将需要一些 HTML 元素来与数据交互并显示数据。

文件输入,用于从磁盘加载文件。

<label for="fileIn"><h2>What am I?</h2></label>
<input type="file" id="file-in" name="file-in">

图像显示,我们要显示输入和重新缩放的图像。

<img id="input-image" class="input-image"></img>
<img id="scaled-image" class="scaled-image"></img>

分类目标,展示我们的推理结果。

<h3 id="target"></h3>

2.图像加载和显示

我们想从文件中加载图像并显示它。回到main.js,我们将从 DOM 中获取文件输入元素,并使用FileReader将数据读入内存。在此之后,图像数据将被传递到handleImage,它将使用 2Dcanvas上下文绘制图像。

const canvas = document.createElement("canvas"),
  ctx = canvas.getContext("2d");

document.getElementById("file-in").onchange = function (evt) {
  let target = evt.target || window.event.src,
    files = target.files;

  if (FileReader && files && files.length) {
      var fileReader = new FileReader();
      fileReader.onload = () => onLoadImage(fileReader);
      fileReader.readAsDataURL(files[0]);
  }
}

function onLoadImage(fileReader) {
    var img = document.getElementById("input-image");
    img.onload = () => handleImage(img);
    img.src = fileReader.result;
}

function handleImage(img) {
  ctx.drawImage(img, 0, 0)
}

2.预处理并将图像转换为张量

现在我们可以加载和显示图像,我们要开始提取和处理数据。请记住,我们的模型采用形状为[1, 3, 224, 224]的矩阵,这意味着我们必须调整图像的大小以支持任何输入图像,并且可能还需要根据我们提取图像数据的方式来转置尺寸。

为了调整和提取图像数据,我们将再次使用canvas上下文。让我们定义一个执行此操作的函数processImageprocessImage具有立即绘制缩放图像的必要元素,因此我们也将在此处执行此操作。

function processImage(img, width) {
  const canvas = document.createElement("canvas"),
    ctx = canvas.getContext("2d")

  // resize image
  canvas.width = width;
  canvas.height = canvas.width * (img.height / img.width);

  // draw scaled image
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  document.getElementById("scaled-image").src = canvas.toDataURL();

  // return data
  return ctx.getImageData(0, 0, width, width).data;
}

我们现在可以在调用processImage的函数handleImage中添加一行。

const resizedImageData = processImage(img, targetWidth);

最后,让我们实现一个名为imageDataToTensor的函数,该函数应用所需的转换,以使图像数据准备好用作模型的输入。imageDataToTensor应该应用三个转换:

  • 过滤掉alpha通道,我们的输入张量应该包含3个通道对应RGB通道。

  • ctx.getImageData返回形状为[224, 224, 3]的数据,所以我们需要将数据转置为形状[3, 224, 224]

  • ctx.getImageData返回一个UInt8ClampedArray,其中int的值范围为 0 到 255,我们需要将这些值转换为float32并将它们存储在Float32Array中以构造我们的张量输入。

function imageDataToTensor(data, dims) {
  // 1a. Extract the R, G, and B channels from the data
  const [R, G, B] = [[], [], []]
  for (let i = 0; i < data.length; i += 4) {
    R.push(data[i]);
    G.push(data[i + 1]);
    B.push(data[i + 2]);
    // 2. skip data[i + 3] thus filtering out the alpha channel
  }
  // 1b. concatenate RGB ~= transpose [224, 224, 3] -> [3, 224, 224]
  const transposedData = R.concat(G).concat(B);

  // 3. convert to float32
  let i, l = transposedData.length; // length, we need this for the loop
  const float32Data = new Float32Array(3 * 224 * 224); // create the Float32Array for output
  for (i = 0; i < l; i++) {
    float32Data[i] = transposedData[i] / 255.0; // convert to float
  }

  const inputTensor = new ort.Tensor("float32", float32Data, dims);
  return inputTensor;
}

3.显示分类结果

快到了,让我们结束一些松散的事情,以启动并运行完整的推理管道。

首先将handleImageData中的图像处理和推理管道拼接起来。

function handleImage(img, targetWidth) {
  ctx.drawImage(img, 0, 0);
  const resizedImageData = processImage(img, targetWidth);
  const inputTensor = imageDataToTensor(resizedImageData, DIMS);
  run(inputTensor);
}

模型的输出是一个激活值列表,对应于在图像中识别出某个类别的概率。我们需要通过获取输出数据中最大值的索引来获得最可能的分类结果,这是使用argMax函数完成的。

function argMax(arr) {
  let max = arr[0];
  let maxIndex = 0;
  for (var i = 1; i < arr.length; i++) {
      if (arr[i] > max) {
          maxIndex = i;
          max = arr[i];
      }
  }
  return [max, maxIndex];
}

3.最后,需要对run()进行重构以接受张量输入。我们还需要使用最大索引从个 ImageNet 类的列表中实际检索结果。我已将此列表预先转换为 JSON,我们将使用require将其加载到我们的脚本中 - 您可以在教程开头和结尾处链接的代码存储库中找到 JSON 文件。

const classes = require("./imagenet_classes.json").data;

async function run(inputTensor) {
  try {
    const session = await ort.InferenceSession.create('./alexnet.onnx');

    const feeds = { input1: inputTensor };
    const results = await session.run(feeds);

    const [maxValue, maxIndex] = argMax(results.output1.data);
    target.innerHTML = `${classes[maxIndex]}`;
  } catch (e) {
    console.error(e);  // non-fatal error handling
  }
}

而已!剩下的就是重新构建我们的包,提供应用程序,并开始对一些图像进行分类。

当您测试应用程序时,您会注意到预测质量不如预期的那么好。这主要是因为当前的图像处理管道仍然相当初级,可以通过多种方式进行改进,例如我们可以实现改进的调整大小、中心裁剪和/或标准化。也许是下一个教程的食物,或者我就留给你去探索吧!

结论

就是这样,我们已经构建了一个带有机器学习模型的 Web 应用程序,该模型在浏览器中本地运行!您可以在 GitHub 上的这个代码库中找到完整的代码(包括样式和布局)。我感谢任何和所有反馈,因此请随时分享任何问题或星星。

感谢您的阅读!

也发表于:https://rekoil.io/blog/onnxruntime-web-tutorial

Logo

学AI,认准AI Studio!GPU算力,限时免费领,邀请好友解锁更多惊喜福利 >>>

更多推荐