WebNN API Tutorial

Para obter uma introdução ao WebNN, incluindo informações sobre suporte ao sistema operacional, suporte a modelos e muito mais, visite a Visão geral da WebNN.

Este tutorial mostrará como usar o WebNN com o ONNX Runtime Web para criar um sistema de classificação de imagens na Web que seja acelerado por hardware usando a GPU no dispositivo. Aproveitaremos o modelo MobileNetV2 , que é um modelo de software livre no Hugging Face usado para classificar imagens.

Se você quiser exibir e executar o código final deste tutorial, poderá encontrá-lo em nosso GitHub de Visualização do Desenvolvedor da WebNN.

Observação

A API WebNN é uma Recomendação de Candidato do W3C e está em estágios iniciais de uma versão prévia do desenvolvedor. Algumas funcionalidades são limitadas. Temos uma lista do status atual de suporte e implementação.

Requisitos e configuração:

Configurando o Windows

Verifique se você tem as versões corretas do Edge, do Windows e dos drivers de hardware, conforme detalhado na seção Requisitos da WebNN.

Configurando o Edge

  1. Baixe e instale o Microsoft Edge Dev.

  2. Inicie o Edge Beta e, na barra de endereços, navegue até about:flags.

  3. Pesquise por "API WebNN", clique na lista suspensa e defina como "Habilitado".

  4. Reinicie o Edge, conforme solicitado.

Uma imagem do WebNN habilitada na versão beta do Edge

Configurando o ambiente do desenvolvedor

  1. Baixe e instale o VSCode (Visual Studio Code).

  2. Inicie o VSCode.

  3. Baixe e instale a extensão do Live Server para VSCode no VSCode.

  4. Selecione File --> Open Foldere crie uma pasta em branco no local desejado.

Etapa 1: inicializar o aplicativo Web

  1. Para começar, crie uma nova index.html página. Adicione o seguinte código clichê à nova página:
<!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. Verifique se o código clichê e a configuração do desenvolvedor funcionaram selecionando o botão Go Live no lado inferior direito do VSCode. Isso deve iniciar um servidor local no Edge Beta executando o código clichê.
  2. Agora, crie um novo arquivo chamado main.js. Isso conterá o código javascript para seu aplicativo.
  3. Em seguida, crie uma subpasta do diretório raiz chamada images. Baixe e salve qualquer imagem dentro da pasta. Para essa demonstração, usaremos o nome padrão de image.jpg.
  4. Baixe o modelo de mobilenet do ONNX Model Zoo. Para este tutorial, você usará o arquivo mobilenet2-10.onnx . Salve esse modelo na pasta raiz do aplicativo Web.
  5. Por fim, baixe e salve este arquivo de classes de imagem. imagenetClasses.js Isso fornece 1.000 classificações comuns de imagens para seu modelo usar.

Etapa 2: Adicionar elementos da interface do usuário (UI) e função parente

  1. Dentro do corpo das <main> marcas html que você adicionou na etapa anterior, substitua o código existente pelos seguintes elementos. Eles criarão um botão e exibirão uma imagem padrão.
<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. Agora, você adicionará o ONNX Runtime Web à sua página, que é uma biblioteca JavaScript que você usará para acessar a API WebNN. Dentro do corpo das <head> etiquetas html, adicione os seguintes links de origem do 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. Abra o main.js arquivo e adicione o snippet de código a seguir.
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
} 

Etapa 3: Pré-processar dados

  1. A função que você acabou de adicionar chama getImageTensorFromPath, outra função que você precisa implementar. Você a adicionará abaixo, bem como outra função assíncrona que ele chama para recuperar a imagem em si.
  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. Você também precisa adicionar a imageDataToTensor função referenciada acima, que renderizará a imagem carregada em um formato tensor que funcionará com nosso modelo ONNX. Essa é uma função mais envolvida, embora possa parecer familiar se você já trabalhou com aplicativos de classificação de imagem semelhantes antes. Para obter uma explicação estendida, você pode exibir este tutorial do 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;
  }

Etapa 4: Chamar o ONNX Runtime Web

  1. Agora você adicionou todas as funções necessárias para recuperar sua imagem e renderizá-la como um tensor. Agora, usando a biblioteca Web do ONNX Runtime que você carregou acima, você executará seu modelo. Observe que, para usar o WebNN aqui, basta especificar executionProvider = "webnn" : o suporte do ONNX Runtime torna muito simples habilitar o WebNN.
  async function runModel(preprocessedData) { 
    // Set up environment.
    ort.env.wasm.numThreads = 1; 
    ort.env.wasm.simd = true; 
    // Uncomment for additional information in debug builds:
    // ort.env.wasm.proxy = true; 
    // ort.env.logLevel = "verbose";  
    // ort.env.debug = true; 

    // Configure WebNN.
    const modelPath = "./mobilenetv2-10.onnx";
    const devicePreference = "gpu"; // Other options include "npu" and "cpu".
    const options = {
        executionProviders: [{ name: "webnn", deviceType: devicePreference, powerPreference: "default" }],
      freeDimensionOverrides: {"batch": 1, "channels": 3, "height": 224, "width": 224}
      // The key names in freeDimensionOverrides should map to the real input dim names in the model.
      // For example, if a model's only key is batch_size, you only need to set
      // freeDimensionOverrides: {"batch_size": 1}
    };
    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; 
  } 

Etapa 5: Dados pós-processo

  1. Por fim, você adicionará uma função softmax e adicionará sua função final para retornar a classificação de imagem mais provável. Transforma softmax seus valores entre 0 e 1, que é a forma de probabilidade necessária para essa classificação final.

Primeiro, adicione os seguintes arquivos de origem para bibliotecas auxiliares Jimp e Lodash na marca de anotação de main.js.

<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>

Agora, adicione essas funções a seguir a 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. Agora você adicionou todos os scripts necessários para executar a classificação de imagens com WebNN em seu aplicativo Web básico. Usando a extensão do Live Server para VS Code, agora você pode iniciar sua página da Web básica no aplicativo para ver os resultados da classificação por conta própria.