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 ?

}
Alex
fonte
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.

edkeveked
fonte
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);
  }

}
Grodzi
fonte
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

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

mico
fonte