Como treinar um modelo em nodejs (tensorflow.js)?


29

Quero criar um classificador de imagens, mas não conheço python. O Tensorflow.js trabalha com javascript, com o qual estou familiarizado. Os modelos podem ser treinados com ele e quais seriam as etapas para fazer isso? Sinceramente, não tenho idéia por onde começar.

A única coisa que descobri é como carregar o "mobilenet", que aparentemente é um conjunto de modelos pré-treinados, e classificar imagens com ele:

const tf = require('@tensorflow/tfjs'),
      mobilenet = require('@tensorflow-models/mobilenet'),
      tfnode = require('@tensorflow/tfjs-node'),
      fs = require('fs-extra');

const imageBuffer = await fs.readFile(......),
      tfimage = tfnode.node.decodeImage(imageBuffer),
      mobilenetModel = await mobilenet.load();  

const results = await mobilenetModel.classify(tfimage);

que funciona, mas não adianta para mim, porque quero treinar meu próprio modelo usando minhas imagens com rótulos criados por mim.

=======================

Digamos que eu tenha várias imagens e rótulos. Como os uso para treinar um modelo?

const myData = JSON.parse(await fs.readFile('files.json'));

for(const data of myData){
  const image = await fs.readFile(data.imagePath),
        labels = data.labels;

  // how to train, where to pass image and labels ?

}

onde você está enfrentando o problema. se você tem tensorflow carregado, você pode treinar seu próprio modelo
— Abhishek Anand

2
Parece que você pode treinar modelos com tensorflow.js tensorflow.org/js/guide/train_models Eu usei o TensorFlow com python. Se o TensorFlow.js não estiver usando GPU, o treinamento poderá demorar. Para mim, o colab.research.google.com foi um recurso útil, pois é gratuito e fornece 11 GB de GPU.
— canbax

11
Essa é uma pergunta muito ampla ... Como indicado nos documentos , você pode usar o ml5 para treinar um modelo ou usar o TF.js diretamente, como neste exemplo do Node.js. (expanda o código de exemplo para ver um exemplo de treinamento).
— jdehesa

Mas não vejo em nenhum lugar desse código como passar as imagens e os rótulos?
— 20919 Alex

@Alex Eles são passados ​​para o fitmétodo ou no conjunto de dados passados ​​para fitDataset, como mostrado nos exemplos.
— jdehesa

Respostas:


22

Antes de tudo, as imagens precisam ser convertidas em tensores. A primeira abordagem seria criar um tensor contendo todos os recursos (respectivamente um tensor contendo todos os rótulos). Este deve ser o caminho a seguir somente se o conjunto de dados contiver poucas imagens.

  const imageBuffer = await fs.readFile(feature_file);
  tensorFeature = tfnode.node.decodeImage(imageBuffer) // create a tensor for the image

  // create an array of all the features
  // by iterating over all the images
  tensorFeatures = tf.stack([tensorFeature, tensorFeature2, tensorFeature3])

Os rótulos seriam uma matriz indicando o tipo de cada imagem

 labelArray = [0, 1, 2] // maybe 0 for dog, 1 for cat and 2 for birds

É preciso agora criar uma codificação quente das etiquetas

 tensorLabels = tf.oneHot(tf.tensor1d(labelArray, 'int32'), 3);

Uma vez que existem os tensores, seria necessário criar o modelo para o treinamento. Aqui está um modelo simples.

const model = tf.sequential();
model.add(tf.layers.conv2d({
  inputShape: [height, width, numberOfChannels], // numberOfChannels = 3 for colorful images and one otherwise
  filters: 32,
  kernelSize: 3,
  activation: 'relu',
}));
model.add(tf.layers.flatten()),
model.add(tf.layers.dense({units: 3, activation: 'softmax'}));

Então o modelo pode ser treinado

model.fit(tensorFeatures, tensorLabels)

Se o conjunto de dados contiver muitas imagens, será necessário criar um tfDataset. Esta resposta discute o porquê.

const genFeatureTensor = image => {
      const imageBuffer = await fs.readFile(feature_file);
      return tfnode.node.decodeImage(imageBuffer)
}

const labelArray = indice => Array.from({length: numberOfClasses}, (_, k) => k === indice ? 1 : 0)

function* dataGenerator() {
  const numElements = numberOfImages;
  let index = 0;
  while (index < numFeatures) {
    const feature = genFeatureTensor(imagePath) ;
    const label = tf.tensor1d(labelArray(classImageIndex))
    index++;
    yield {xs: feature, ys: label};
  }
}

const ds = tf.data.generator(dataGenerator);

E use model.fitDataset(ds)para treinar o modelo


O acima é para treinamento em nodejs. Para fazer esse processamento no navegador, genFeatureTensorpode ser escrito da seguinte maneira:

function load(url){
  return new Promise((resolve, reject) => {
    const im = new Image()
        im.crossOrigin = 'anonymous'
        im.src = 'url'
        im.onload = () => {
          resolve(im)
        }
   })
}

genFeatureTensor = image => {
  const img = await loadImage(image);
  return tf.browser.fromPixels(image);
}

Uma palavra de cautela é que o processamento pesado pode bloquear o encadeamento principal no navegador. É aqui que os trabalhadores da web entram em cena.


a largura e a altura do inputShape devem corresponder à largura e altura das imagens? Então, eu não posso passar imagens com dimensões diferentes?
— Alex #

Sim, eles devem corresponder. Se você tem imagens de diferente largura e altura do inputShape do modelo, será necessário redimensionar a imagem usandotf.image.resizeBilinear
— edkeveked

Bem, isso realmente não funciona. Eu recebo erros
— Alex

11
@Alex Você poderia atualizar sua pergunta com o resumo do modelo e o formato da imagem que está carregando? Todas as imagens precisam ter a mesma forma ou a imagem teria de ser redimensionada para a formação
— edkeveked

11
oi @edkeveked, eu estou falando sobre detecção de objetos, eu adicionei uma nova pergunta aqui, por favor dê uma olhada stackoverflow.com/questions/59322382/…
— Pranoy Sarkar

10

Considere o exemplo https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/#0

O que eles fazem é:

  • obter uma imagem PNG GRANDE (uma concatenação vertical de imagens)
  • pegue alguns rótulos
  • criar o conjunto de dados (data.js)

então treine

A construção do conjunto de dados é a seguinte:

  1. imagens

A imagem grande é dividida em n pedaços verticais. (n sendo chunkSize)

Considere um pedaço de tamanho 2.

Dada a matriz de pixels da imagem 1:

  1 2 3
  4 5 6

Dada a matriz de pixels da imagem 2 é

  7 8 9
  1 2 3

A matriz resultante seria 1 2 3 4 5 6 7 8 9 1 2 3 (de alguma forma a concatenação 1D)

Então, basicamente, no final do processamento, você tem um grande buffer representando

[...Buffer(image1), ...Buffer(image2), ...Buffer(image3)]

  1. etiquetas

Esse tipo de formatação é feito muito para problemas de classificação. Em vez de classificar com um número, eles usam uma matriz booleana. Para prever 7 de 10 aulas, consideraríamos [0,0,0,0,0,0,0,1,0,0] // 1 in 7e position, array 0-indexed

O que você pode fazer para começar

  • Tire sua imagem (e seu rótulo associado)
  • Carregue sua imagem na tela
  • Extraia seu buffer associado
  • Concatene todo o buffer da sua imagem como um buffer grande. É isso para xs.
  • Pegue todos os rótulos associados, mapeie-os como uma matriz booleana e concatene-os.

Abaixo, eu subclasse MNistData::load (o restante pode ser deixado como está (exceto em script.js, onde você precisa instanciar sua própria classe)

Ainda gero imagens 28x28, escrevo um dígito e obtenho uma precisão perfeita, pois não incluo ruídos ou etiquetas voluntariamente erradas.


import {MnistData} from './data.js'

const IMAGE_SIZE = 784;// actually 28*28...
const NUM_CLASSES = 10;
const NUM_DATASET_ELEMENTS = 5000;
const NUM_TRAIN_ELEMENTS = 4000;
const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;


function makeImage (label, ctx) {
  ctx.fillStyle = 'black'
  ctx.fillRect(0, 0, 28, 28) // hardcoded, brrr
  ctx.fillStyle = 'white'
  ctx.fillText(label, 10, 20) // print a digit on the canvas
}

export class MyMnistData extends MnistData{
  async load() { 
    const canvas = document.createElement('canvas')
    canvas.width = 28
    canvas.height = 28
    let ctx = canvas.getContext('2d')
    ctx.font = ctx.font.replace(/\d+px/, '18px')
    let labels = new Uint8Array(NUM_DATASET_ELEMENTS*NUM_CLASSES)

    // in data.js, they use a batch of images (aka chunksize)
    // let's even remove it for simplification purpose
    const datasetBytesBuffer = new ArrayBuffer(NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
    for (let i = 0; i < NUM_DATASET_ELEMENTS; i++) {

      const datasetBytesView = new Float32Array(
          datasetBytesBuffer, i * IMAGE_SIZE * 4, 
          IMAGE_SIZE);

      // BEGIN our handmade label + its associated image
      // notice that you could loadImage( images[i], datasetBytesView )
      // so you do them by bulk and synchronize after your promises after "forloop"
      const label = Math.floor(Math.random()*10)
      labels[i*NUM_CLASSES + label] = 1
      makeImage(label, ctx)
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      // END you should be able to load an image to canvas :)

      for (let j = 0; j < imageData.data.length / 4; j++) {
        // NOTE: you are storing a FLOAT of 4 bytes, in [0;1] even though you don't need it
        // We could make it with a uint8Array (assuming gray scale like we are) without scaling to 1/255
        // they probably did it so you can copy paste like me for color image afterwards...
        datasetBytesView[j] = imageData.data[j * 4] / 255;
      }
    }
    this.datasetImages = new Float32Array(datasetBytesBuffer);
    this.datasetLabels = labels

    //below is copy pasted
    this.trainIndices = tf.util.createShuffledIndices(NUM_TRAIN_ELEMENTS);
    this.testIndices = tf.util.createShuffledIndices(NUM_TEST_ELEMENTS);
    this.trainImages = this.datasetImages.slice(0, IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
    this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
    this.trainLabels =
        this.datasetLabels.slice(0, NUM_CLASSES * NUM_TRAIN_ELEMENTS);// notice, each element is an array of size NUM_CLASSES
    this.testLabels =
        this.datasetLabels.slice(NUM_CLASSES * NUM_TRAIN_ELEMENTS);
  }

}

8

Eu encontrei um tutorial [1] sobre como usar o modelo existente para treinar novas classes. Principais partes do código aqui:

index.html head:

   <script src="https://unpkg.com/@tensorflow-models/knn-classifier"></script>

index.html body:

    <button id="class-a">Add A</button>
    <button id="class-b">Add B</button>
    <button id="class-c">Add C</button>

index.js:

    const classifier = knnClassifier.create();

    ....

    // Reads an image from the webcam and associates it with a specific class
    // index.
    const addExample = async classId => {
           // Capture an image from the web camera.
           const img = await webcam.capture();

           // Get the intermediate activation of MobileNet 'conv_preds' and pass that
           // to the KNN classifier.
           const activation = net.infer(img, 'conv_preds');

           // Pass the intermediate activation to the classifier.
           classifier.addExample(activation, classId);

           // Dispose the tensor to release the memory.
          img.dispose();
     };

     // When clicking a button, add an example for that class.
    document.getElementById('class-a').addEventListener('click', () => addExample(0));
    document.getElementById('class-b').addEventListener('click', () => addExample(1));
    document.getElementById('class-c').addEventListener('click', () => addExample(2));

    ....

A idéia principal é usar a rede existente para fazer sua previsão e, em seguida, substituir o rótulo encontrado pelo seu.

O código completo está no tutorial. Outro promissor, mais avançado em [2]. Ele precisa de pré-processamento rigoroso, então deixo apenas aqui, quero dizer que é muito mais avançado.

Fontes:

[1] https://codelabs.developers.google.com/codelabs/tensorflowjs-teachablemachine-codelab/index.html#6

[2] https://towardsdatascience.com/training-custom-image-classification-model-on-the-browser-with-tensorflow-js-and-angular-f1796ed24934


Por favor, dê uma olhada na minha segunda resposta, é muito mais próxima da realidade, por onde começar.
— Mico

Por que não colocar as duas respostas em uma?
— edkeveked

Eles têm uma abordagem tão diferente da mesma coisa. Este acima, onde eu comento agora é realmente uma solução alternativa, o outro está começando do básico, que acho que agora mais tarde é mais apropriado para a configuração de perguntas.
— Mico

3

TL; DR

MNIST é o reconhecimento de imagem Hello World. Depois de aprender de cor, essas perguntas são fáceis de resolver.


Configuração da pergunta:

Sua principal pergunta escrita é

 // how to train, where to pass image and labels ?

dentro do seu bloco de código. Para aqueles que encontrei a resposta perfeita dos exemplos da seção de exemplos do Tensorflow.js: Exemplo do MNIST. Meus links abaixo têm versões javascript e node.js puras e explicação da Wikipedia. Vou examiná-los no nível necessário para responder à pergunta principal em sua mente e acrescentarei também perspectivas de como suas próprias imagens e rótulos têm alguma coisa a ver com o conjunto de imagens MNIST e os exemplos usando-os.

Primeiras coisas primeiro:

Partes de codigo.

onde transmitir imagens (amostra do Node.js.)

async function loadImages(filename) {
  const buffer = await fetchOnceAndSaveToDiskWithBuffer(filename);

  const headerBytes = IMAGE_HEADER_BYTES;
  const recordBytes = IMAGE_HEIGHT * IMAGE_WIDTH;

  const headerValues = loadHeaderValues(buffer, headerBytes);
  assert.equal(headerValues[0], IMAGE_HEADER_MAGIC_NUM);
  assert.equal(headerValues[2], IMAGE_HEIGHT);
  assert.equal(headerValues[3], IMAGE_WIDTH);

  const images = [];
  let index = headerBytes;
  while (index < buffer.byteLength) {
    const array = new Float32Array(recordBytes);
    for (let i = 0; i < recordBytes; i++) {
      // Normalize the pixel values into the 0-1 interval, from
      // the original 0-255 interval.
      array[i] = buffer.readUInt8(index++) / 255;
    }
    images.push(array);
  }

  assert.equal(images.length, headerValues[1]);
  return images;
}

Notas:

O conjunto de dados MNIST é uma imagem enorme, onde em um arquivo existem várias imagens como blocos no quebra-cabeça, todas com o mesmo tamanho, lado a lado, como caixas na tabela de coordenação xey. Cada caixa tem uma amostra e x e y correspondentes na matriz de rótulos têm o rótulo. Neste exemplo, não é grande coisa transformá-lo em vários formatos de arquivo, de modo que, na verdade, apenas uma foto de cada vez seja dada ao loop while para lidar.

Marcadores:

async function loadLabels(filename) {
  const buffer = await fetchOnceAndSaveToDiskWithBuffer(filename);

  const headerBytes = LABEL_HEADER_BYTES;
  const recordBytes = LABEL_RECORD_BYTE;

  const headerValues = loadHeaderValues(buffer, headerBytes);
  assert.equal(headerValues[0], LABEL_HEADER_MAGIC_NUM);

  const labels = [];
  let index = headerBytes;
  while (index < buffer.byteLength) {
    const array = new Int32Array(recordBytes);
    for (let i = 0; i < recordBytes; i++) {
      array[i] = buffer.readUInt8(index++);
    }
    labels.push(array);
  }

  assert.equal(labels.length, headerValues[1]);
  return labels;
}

Notas:

Aqui, os rótulos também são dados de bytes em um arquivo. No mundo Javascript, e com a abordagem que você tem no seu ponto de partida, os rótulos também podem ser uma matriz json.

treinar o modelo:

await data.loadData();

  const {images: trainImages, labels: trainLabels} = data.getTrainData();
  model.summary();

  let epochBeginTime;
  let millisPerStep;
  const validationSplit = 0.15;
  const numTrainExamplesPerEpoch =
      trainImages.shape[0] * (1 - validationSplit);
  const numTrainBatchesPerEpoch =
      Math.ceil(numTrainExamplesPerEpoch / batchSize);
  await model.fit(trainImages, trainLabels, {
    epochs,
    batchSize,
    validationSplit
  });

Notas:

Aqui model.fitestá a linha de código real que faz a coisa: treina o modelo.

Resultados da coisa toda:

  const {images: testImages, labels: testLabels} = data.getTestData();
  const evalOutput = model.evaluate(testImages, testLabels);

  console.log(
      `\nEvaluation result:\n` +
      `  Loss = ${evalOutput[0].dataSync()[0].toFixed(3)}; `+
      `Accuracy = ${evalOutput[1].dataSync()[0].toFixed(3)}`);

Nota:

Em Data Science, também desta vez aqui, a parte mais fascinante é saber até que ponto o modelo sobrevive ao teste de novos dados e sem rótulos, pode rotulá-los ou não? Pois essa é a parte da avaliação que agora nos imprime alguns números.

Perda e precisão: [4]

Quanto menor a perda, melhor o modelo (a menos que o modelo tenha se ajustado demais aos dados de treinamento). A perda é calculada no treinamento e na validação e sua interoperação é o desempenho do modelo nesses dois conjuntos. Diferentemente da precisão, a perda não é uma porcentagem. É uma soma dos erros cometidos para cada exemplo nos conjuntos de treinamento ou validação.

..

A precisão de um modelo é geralmente determinada depois que os parâmetros do modelo são aprendidos e corrigidos e nenhum aprendizado está ocorrendo. Em seguida, as amostras de teste são fornecidas ao modelo e o número de erros (perda zero e um) que o modelo comete é registrado, após comparação com os verdadeiros alvos.


Mais Informações:

Nas páginas do github, no arquivo README.md, há um link para o tutorial, onde tudo no exemplo do github é explicado em mais detalhes.


[1] https://github.com/tensorflow/tfjs-examples/tree/master/mnist

[2] https://github.com/tensorflow/tfjs-examples/tree/master/mnist-node

[3] https://en.wikipedia.org/wiki/MNIST_database

[4] Como interpretar "perda" e "precisão" para um modelo de aprendizado de máquina

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.