WebNN API 教程

有关 WebNN 的简介,包括有关操作系统支持、模型支持等的信息,请访问 WebNN 概述

本教程介绍如何使用 WebNN API 在 Web 上构建图像分类系统,该系统使用设备 GPU 进行硬件加速。 我们将利用 MobileNetv2 模型,它是用于对图像进行分类的 Hugging Face 上的开放源模型。

如果要查看并运行本教程的最终代码,可以在 WebNN 开发人员预览版 GitHub 上找到它。

注意

WebNN API 是一个 W3C 候选推荐版本,处于开发人员预览版早期阶段。 一些功能会受到限制。 我们有当前支持和实施状态的列表。

要求和设置:

设置 Windows

确保拥有如 WebNN 要求部分详细介绍的 Edge、Windows 和硬件驱动程序的正确版本。

设置 Edge

  1. 下载并安装 Microsoft Edge Dev

  2. 启动 Edge Beta,然后在地址栏中导航到 about:flags

  3. 搜索“WebNN API”,单击下拉列表,并设置为“已启用”。

  4. 按提示重启 Edge。

Edge beta 中已启用的 WebNN 图像

设置开发人员环境

  1. 下载并安装 Visual Studio Code (VSCode)

  2. 启动 VSCode。

  3. 在 VSCode 中下载并安装 VSCode 的 Live Server 扩展

  4. 选择 File --> Open Folder,并在所需位置创建一个空白文件夹。

步骤 1:初始化 Web 应用

  1. 若要开始,请创建新 index.html 页面。 将以下样本代码添加到新页面:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Website</title>
  </head>
  <body>
    <main>
        <h1>Welcome to My Website</h1>
    </main>
  </body>
</html>
  1. 通过选择 VSCode 右下侧的 Go Live 按钮来验证样本代码和开发人员设置是否正常工作。 这应在 Edge Beta 中启动一个运行样本代码的本地服务器。
  2. 现在,创建名为 main.js 的新文件。 此文件将包含应用程序的 JavaScript 代码。
  3. 接下来,在名为 images 的根目录下创建子文件夹。 在文件夹中下载并保存图像。 对于此演示,我们将使用默认名称 image.jpg
  4. ONNX Model Zoo 下载 mobilenet 模型。 在本教程中,我们将使用 mobilenet2-10.onnx 文件。 将此模型保存到 Web 应用的根文件夹。
  5. 最后,下载并保存此图像类文件imagenetClasses.js。 这为模型提供了 1000 种常用图像分类供使用。

步骤 2:添加 UI 元素和父级函数

  1. 在上一步中添加的 <main> html 标记的 body 中,将现有代码替换为以下元素。 这将创建一个按钮并显示默认图像。
<h1>Image Classification Demo!</h1> 
<div><img src="./images/image.jpg"></div> 
<button onclick="classifyImage('./images/image.jpg')"  type="button">Click Me to Classify Image!</button> 
<h1 id="outputText"> This image displayed is ... </h1>
  1. 现在,将 ONNX Runtime Web 添加到你的页面,这是一个 JavaScript 库,用于访问 WebNN API。 在 <head> html 标记的正文中,添加以下 javascript 源链接。
<script src="./main.js"></script> 
<script src="imagenetClasses.js"></script>
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.18.0-dev.20240311-5479124834/dist/ort.webgpu.min.js"></script> 
  1. 打开 main.js 文件并添加以下代码片段。
async function classifyImage(pathToImage){ 
  var imageTensor = await getImageTensorFromPath(pathToImage); // Convert image to a tensor
  var predictions = await runModel(imageTensor); // Run inference on the tensor
  console.log(predictions); // Print predictions to console
  document.getElementById("outputText").innerHTML += predictions[0].name; // Display prediction in HTML
} 

步骤 3:预处理数据

  1. 刚刚添加的函数会调用 getImageTensorFromPath,它是必须实施的另一个函数。 将在下面添加它,以及它调用的用来检索图像本身的另一个异步函数。
  async function getImageTensorFromPath(path, width = 224, height = 224) {
    var image = await loadImagefromPath(path, width, height); // 1. load the image
    var imageTensor = imageDataToTensor(image); // 2. convert to tensor
    return imageTensor; // 3. return the tensor
  } 

  async function loadImagefromPath(path, resizedWidth, resizedHeight) {
    var imageData = await Jimp.read(path).then(imageBuffer => { // Use Jimp to load the image and resize it.
      return imageBuffer.resize(resizedWidth, resizedHeight);
    });

    return imageData.bitmap;
  }
  1. 还需要添加上面引用的 imageDataToTensor 函数,该函数会将已加载的图像呈现为张量格式,该格式将用于 ONNX 模型。 这是一个更相关的函数,但如果你以前曾使用过类似的图像分类应用,它看起来可能很熟悉。 有关扩展说明,可以查看此 ONNX 教程
  function imageDataToTensor(image) {
    var imageBufferData = image.data;
    let pixelCount = image.width * image.height;
    const float32Data = new Float32Array(3 * pixelCount); // Allocate enough space for red/green/blue channels.

    // Loop through the image buffer, extracting the (R, G, B) channels, rearranging from
    // packed channels to planar channels, and converting to floating point.
    for (let i = 0; i < pixelCount; i++) {
      float32Data[pixelCount * 0 + i] = imageBufferData[i * 4 + 0] / 255.0; // Red
      float32Data[pixelCount * 1 + i] = imageBufferData[i * 4 + 1] / 255.0; // Green
      float32Data[pixelCount * 2 + i] = imageBufferData[i * 4 + 2] / 255.0; // Blue
      // Skip the unused alpha channel: imageBufferData[i * 4 + 3].
    }
    let dimensions = [1, 3, image.height, image.width];
    const inputTensor = new ort.Tensor("float32", float32Data, dimensions);
    return inputTensor;
  }

步骤 4:调用 WebNN

  1. 你现在已经添加了所有需要的函数来检索图像并将其呈现为张量。 现在,使用上面加载的 ONNX Runtime Web 库来运行你的模型。 请注意,要在此处使用 WebNN,只需指定 executionProvider = "webnn" - ONNX Runtime 的支持使得启用 WebNN 非常简单。
  async function runModel(preprocessedData) { 
    // Set up environment.
    ort.env.wasm.numThreads = 1; 
    ort.env.wasm.simd = true; 
    ort.env.wasm.proxy = true; 
    ort.env.logLevel = "verbose";  
    ort.env.debug = true; 

    // Configure WebNN.
    const executionProvider = "webnn"; // Other options: webgpu 
    const modelPath = "./mobilenetv2-7.onnx" 
    const options = {
	    executionProviders: [{ name: executionProvider, deviceType: "gpu", powerPreference: "default" }],
      freeDimensionOverrides: {"batch": 1, "channels": 3, "height": 224, "width": 224}
    };
    modelSession = await ort.InferenceSession.create(modelPath, options); 

    // Create feeds with the input name from model export and the preprocessed data. 
    const feeds = {}; 
    feeds[modelSession.inputNames[0]] = preprocessedData; 
    // Run the session inference.
    const outputData = await modelSession.run(feeds); 
    // Get output results with the output name from the model export. 
    const output = outputData[modelSession.outputNames[0]]; 
    // Get the softmax of the output data. The softmax transforms values to be between 0 and 1.
    var outputSoftmax = softmax(Array.prototype.slice.call(output.data)); 
    // Get the top 5 results.
    var results = imagenetClassesTopK(outputSoftmax, 5);

    return results; 
  } 

步骤 5:数据后期处理

  1. 最后,添加一个 softmax 函数,然后添加最终函数以返回最有可能的图像分类。 softmax 会将你的值转换为 0 到 1 之间的值,这是此最终分类所需的概率形式。

首先,在 main.js 的 head 标记中添加以下帮助程序库 JimpLodash 的源文件。

<script src="https://cdnjs.cloudflare.com/ajax/libs/jimp/0.22.12/jimp.min.js" integrity="sha512-8xrUum7qKj8xbiUrOzDEJL5uLjpSIMxVevAM5pvBroaxJnxJGFsKaohQPmlzQP8rEoAxrAujWttTnx3AMgGIww==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

现在,将以下函数添加到 main.js

// The softmax transforms values to be between 0 and 1.
function softmax(resultArray) {
  // Get the largest value in the array.
  const largestNumber = Math.max(...resultArray);
  // Apply the exponential function to each result item subtracted by the largest number, using reduction to get the
  // previous result number and the current number to sum all the exponentials results.
  const sumOfExp = resultArray 
    .map(resultItem => Math.exp(resultItem - largestNumber)) 
    .reduce((prevNumber, currentNumber) => prevNumber + currentNumber);

  // Normalize the resultArray by dividing by the sum of all exponentials.
  // This normalization ensures that the sum of the components of the output vector is 1.
  return resultArray.map((resultValue, index) => {
    return Math.exp(resultValue - largestNumber) / sumOfExp
  });
}

function imagenetClassesTopK(classProbabilities, k = 5) { 
  const probs = _.isTypedArray(classProbabilities)
    ? Array.prototype.slice.call(classProbabilities)
    : classProbabilities;

  const sorted = _.reverse(
    _.sortBy(
      probs.map((prob, index) => [prob, index]),
      probIndex => probIndex[0]
    )
  );

  const topK = _.take(sorted, k).map(probIndex => {
    const iClass = imagenetClasses[probIndex[1]]
    return {
      id: iClass[0],
      index: parseInt(probIndex[1].toString(), 10),
      name: iClass[1].replace(/_/g, " "),
      probability: probIndex[0]
    }
  });
  return topK;
}
  1. 现在,你已在基本 Web 应用中添加了使用 WebNN 来运行图像分类所需的所有脚本。 使用适用于 VS Code 的 Live Server 扩展,现在可以启动应用内的基本网页,并自行查看分类结果。