¿Cómo entrenar un modelo en nodejs (tensorflow.js)?

29

Quiero hacer un clasificador de imágenes, pero no sé python. Tensorflow.js funciona con javascript, con el que estoy familiarizado. ¿Pueden los modelos entrenarse con él y cuáles serían los pasos para hacerlo? Francamente, no tengo idea de por dónde empezar.

Lo único que descubrí es cómo cargar "mobilenet", que aparentemente es un conjunto de modelos previamente entrenados, y clasificar imágenes con él:

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

lo que funciona, pero no me sirve porque quiero entrenar mi propio modelo usando mis imágenes con etiquetas que creo.

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

Digamos que tengo un montón de imágenes y etiquetas. ¿Cómo los uso para entrenar a una 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
fuente
¿Dónde estás enfrentando el problema? si ha cargado tensorflow, puede entrenar a su propio modelo
Abhishek Anand
2
Parece que puedes entrenar modelos con tensorflow.js tensorflow.org/js/guide/train_models Utilicé TensorFlow con python. Si TensorFlow.js no utiliza GPU, la capacitación puede llevar mucho tiempo. Para mí, colab.research.google.com fue un recurso útil porque es gratuito y proporciona 11 GB de GPU.
Canbax
1
Esta es una pregunta demasiado amplia ... Como se señaló en los documentos , puede usar ml5 para entrenar un modelo o usar TF.js directamente, como en este ejemplo de Node.js (expanda el código de muestra para ver un ejemplo de entrenamiento).
jdehesa
Pero no veo en ningún lugar de ese código cómo pasar las imágenes y las etiquetas.
Alex
@Alex Se pasan al fitmétodo, o en el conjunto de datos pasado fitDataset, como se muestra en los ejemplos.
jdehesa

Respuestas:

22

En primer lugar, las imágenes deben convertirse en tensores. El primer enfoque sería crear un tensor que contenga todas las características (respectivamente, un tensor que contenga todas las etiquetas). Este debería ser el camino a seguir solo si el conjunto de datos contiene pocas imágenes.

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

Las etiquetas serían una matriz que indica el tipo de cada imagen.

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

Ahora se necesita crear una codificación activa de las etiquetas.

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

Una vez que están los tensores, sería necesario crear el modelo para el entrenamiento. Aquí hay un modelo simple.

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'}));

Entonces el modelo puede ser entrenado

model.fit(tensorFeatures, tensorLabels)

Si el conjunto de datos contiene muchas imágenes, sería necesario crear un conjunto de datos tf. Esta respuesta discute por qué.

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

Y usar model.fitDataset(ds)para entrenar al modelo


Lo anterior es para entrenar en nodejs. Para realizar dicho procesamiento en el navegador, genFeatureTensorpuede escribirse de la siguiente manera:

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

Una advertencia es que realizar un procesamiento pesado podría bloquear el hilo principal en el navegador. Aquí es donde los trabajadores web entran en juego.

edkeveked
fuente
el ancho y el alto de inputShape deben coincidir con el ancho y alto de las imágenes? ¿Entonces no puedo pasar imágenes con diferentes dimensiones?
Alex
Sí, deben coincidir. Si tiene imágenes de diferente ancho y alto de la entrada Forma del modelo, deberá cambiar el tamaño de la imagen usandotf.image.resizeBilinear
edkeveked
Bueno, realmente no funciona. Recibo errores
Alex
1
@ Alex ¿Podría actualizar su pregunta con el resumen del modelo y la forma de la imagen que está cargando? Todas las imágenes deben tener la misma forma o la imagen tendría que ser redimensionada para la capacitación
edkeveked
1
hola @edkeveked, estoy hablando de detección de objetos, he agregado una nueva pregunta aquí, por favor, eche un vistazo stackoverflow.com/questions/59322382/…
Pranoy Sarkar
10

Considere el ejemplo https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/#0

Lo que hacen es:

  • tomar una imagen PNG GRANDE (una concatenación vertical de imágenes)
  • toma algunas etiquetas
  • construir el conjunto de datos (data.js)

luego entrenar

La construcción del conjunto de datos es la siguiente:

  1. imágenes

La imagen grande se divide en n trozos verticales. (n siendo trozo)

Considere un trozo Tamaño del tamaño 2.

Dada la matriz de píxeles de la imagen 1:

  1 2 3
  4 5 6

Dada la matriz de píxeles de la imagen 2 es

  7 8 9
  1 2 3

La matriz resultante sería 1 2 3 4 5 6 7 8 9 1 2 3 (la concatenación 1D de alguna manera)

Básicamente, al final del procesamiento, tiene un gran búfer que representa

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

  1. etiquetas

Ese tipo de formato se hace mucho para problemas de clasificación. En lugar de clasificar con un número, toman una matriz booleana. Para predecir 7 de cada 10 clases, consideraríamos [0,0,0,0,0,0,0,1,0,0] // 1 in 7e position, array 0-indexed

Lo que puedes hacer para comenzar

  • Tome su imagen (y su etiqueta asociada)
  • Carga tu imagen en el lienzo
  • Extrae su búfer asociado
  • Concatena todo el búfer de tu imagen como un gran búfer. Eso es todo por xs.
  • Tome todas sus etiquetas asociadas, asígnelas como una matriz booleana y concatenelas.

A continuación, subclase MNistData::load(el resto se puede dejar como está (excepto en script.js donde necesita crear una instancia de su propia clase)

Todavía genero imágenes de 28x28, escribo un dígito en él y obtengo una precisión perfecta ya que no incluyo ruido o etiquetado incorrecto voluntariamente.


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

Encontré un tutorial [1] sobre cómo usar el modelo existente para entrenar nuevas clases. Principales partes del código aquí:

cabeza index.html:

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

cuerpo index.html:

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

    ....

La idea principal es utilizar la red existente para hacer su predicción y luego sustituir la etiqueta encontrada con la suya.

El código completo está en el tutorial. Otro prometedor, más avanzado en [2]. Necesita un procesamiento previo estricto, por lo que lo dejo solo aquí, quiero decir que es mucho más avanzado.

Fuentes:

[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
fuente
Por favor, eche un vistazo a mi segunda respuesta, está mucho más cerca de la realidad, por dónde empezar.
mico
¿Por qué no poner ambas respuestas en una?
edkeveked
Tienen un enfoque tan diferente de lo mismo. Este anterior, donde comento ahora es en realidad una solución, el otro está comenzando desde lo básico, que creo que más adelante es más apropiado para la configuración de preguntas.
mico
3

TL; DR

MNIST es el reconocimiento de imágenes Hello World. Después de aprenderlo de memoria, estas preguntas en su mente son fáciles de resolver.


Configuración de preguntas:

Su pregunta principal escrita es

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

dentro de su bloque de código. Para aquellos que encontré la respuesta perfecta de ejemplos de la sección de ejemplos de Tensorflow.js: ejemplo de MNIST. Mis enlaces a continuación tienen versiones de javascript y node.js puras y explicaciones de Wikipedia. Los examinaré en el nivel necesario para responder la pregunta principal en su mente y agregaré también perspectivas sobre cómo sus propias imágenes y etiquetas tienen algo que ver con el conjunto de imágenes MNIST y los ejemplos que lo usan.

Lo primero es lo primero:

Fragmentos de código.

dónde pasar las imágenes (muestra de 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:

El conjunto de datos MNIST es una imagen enorme, donde en un archivo hay varias imágenes como mosaicos en rompecabezas, todas y cada una con el mismo tamaño, una al lado de la otra, como cuadros en la tabla de coordinación x e y. Cada cuadro tiene una muestra y las correspondientes xey en la matriz de etiquetas tienen la etiqueta. A partir de este ejemplo, no es un gran problema convertirlo a varios formatos de archivo, por lo que en realidad solo se le da una imagen a la vez al ciclo while para manejar.

Etiquetas:

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:

Aquí, las etiquetas también son datos de bytes en un archivo. En el mundo Javascript, y con el enfoque que tiene en su punto de partida, las etiquetas también podrían ser una matriz json.

entrenar al 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:

Aquí model.fitestá la línea de código real que hace la cosa: entrena el modelo.

Resultados de todo el asunto:

  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:

En Data Science, también esta vez aquí, la parte más fascinante es saber qué tan bien el modelo sobrevive a la prueba de nuevos datos y sin etiquetas, ¿puede etiquetarlos o no? Para eso es la parte de evaluación que ahora nos imprime algunos números.

Pérdida y precisión: [4]

Cuanto menor sea la pérdida, mejor será un modelo (a menos que el modelo se haya ajustado demasiado a los datos de entrenamiento). La pérdida se calcula en el entrenamiento y la validación y su interacción es qué tan bien le está yendo al modelo en estos dos conjuntos. A diferencia de la precisión, la pérdida no es un porcentaje. Es un resumen de los errores cometidos para cada ejemplo en conjuntos de entrenamiento o validación.

..

La precisión de un modelo generalmente se determina después de que los parámetros del modelo se aprenden y arreglan y no se lleva a cabo el aprendizaje. Luego, las muestras de prueba se envían al modelo y se registra el número de errores (pérdida cero-uno) que comete el modelo, después de la comparación con los objetivos reales.


Más información:

En las páginas de github, en el archivo README.md, hay un enlace al tutorial, donde todo en el ejemplo de github se explica con mayor detalle.


[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] Cómo interpretar "pérdida" y "precisión" para un modelo de aprendizaje automático

mico
fuente