Comment former un modèle dans nodejs (tensorflow.js)?

29

Je veux faire un classificateur d'images, mais je ne connais pas python. Tensorflow.js fonctionne avec javascript, que je connais bien. Les modèles peuvent-ils être formés avec elle et quelles seraient les étapes pour le faire? Franchement, je n'ai aucune idée par où commencer.

La seule chose que j'ai trouvée est de savoir comment charger "mobilenet", qui est apparemment un ensemble de modèles pré-formés, et classer les images avec:

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

ce qui fonctionne, mais cela ne me sert à rien car je veux former mon propre modèle en utilisant mes images avec des étiquettes que je crée.

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

Disons que j'ai un tas d'images et d'étiquettes. Comment les utiliser pour former un modèle?

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
la source
où êtes-vous confronté au problème. si vous avez chargé tensorflow, vous pouvez entraîner votre propre modèle
Abhishek Anand
2
Il semble que vous puissiez entraîner des modèles avec tensorflow.js tensorflow.org/js/guide/train_models J'ai utilisé TensorFlow avec python. Si TensorFlow.js n'utilise pas de GPU, la formation peut prendre un certain temps. Pour moi, colab.research.google.com était une ressource utile car il est gratuit et fournit 11 Go de GPU.
canbax
1
C'est une question trop large ... Comme indiqué dans la documentation , vous pouvez utiliser ml5 pour entraîner un modèle ou utiliser TF.js directement, comme dans cet exemple Node.js (développez l'exemple de code pour voir un exemple de formation).
jdehesa
Mais je ne vois nulle part dans ce code comment passer les images et les étiquettes?
Alex
@Alex Ils sont transmis à la fitméthode, ou dans l'ensemble de données transmis fitDataset, comme indiqué dans les exemples.
jdehesa

Réponses:

22

Tout d'abord, les images doivent être converties en tenseurs. La première approche serait de créer un tenseur contenant toutes les entités (respectivement un tenseur contenant toutes les étiquettes). Cela ne devrait être le cas que si l'ensemble de données contient peu d'images.

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

Les étiquettes seraient un tableau indiquant le type de chaque image

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

Il faut maintenant créer un encodage à chaud des étiquettes

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

Une fois qu'il y a les tenseurs, il faudrait créer le modèle de formation. Voici un modèle 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'}));

Ensuite, le modèle peut être formé

model.fit(tensorFeatures, tensorLabels)

Si l'ensemble de données contient beaucoup d'images, il faudrait plutôt créer un tfDataset. Cette réponse explique pourquoi.

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

Et utiliser model.fitDataset(ds)pour former le modèle


Ce qui précède est pour la formation dans nodejs. Pour effectuer un tel traitement dans le navigateur, genFeatureTensorpeut être écrit comme suit:

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

Une mise en garde est que faire un traitement lourd peut bloquer le thread principal dans le navigateur. C'est là que les travailleurs du Web entrent en jeu.

edkeveked
la source
la largeur et la hauteur de la forme d'entrée doivent correspondre à la largeur et la hauteur des images? Je ne peux donc pas transmettre des images de différentes dimensions?
Alex
Oui, ils doivent correspondre. Si vous avez des images de largeur et de hauteur différentes de la forme d'entrée du modèle, vous devrez redimensionner l'image en utilisanttf.image.resizeBilinear
edkeveked
Eh bien, ça ne marche pas vraiment. Je reçois des erreurs
Alex
1
@Alex Pourriez-vous s'il vous plaît mettre à jour votre question avec le résumé du modèle et la forme de l'image que vous chargez? Toutes les images doivent avoir la même forme ou l'image devra être redimensionnée pour la formation
edkeveked
1
salut @edkeveked, je parle de détection d'objet, j'ai ajouté une nouvelle question ici s'il vous plaît jetez un œil stackoverflow.com/questions/59322382/…
Pranoy Sarkar
10

Prenons l'exemple https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/#0

Ce qu'ils font c'est:

  • prendre une GRANDE image png (une concaténation verticale d'images)
  • prendre quelques étiquettes
  • construire l'ensemble de données (data.js)

puis train

La construction de l'ensemble de données est la suivante:

  1. images

La grande image est divisée en n morceaux verticaux. (n étant chunkSize)

Prenons un morceau de taille 2.

Étant donné la matrice de pixels de l'image 1:

  1 2 3
  4 5 6

Étant donné la matrice de pixels de l'image 2 est

  7 8 9
  1 2 3

Le tableau résultant serait 1 2 3 4 5 6 7 8 9 1 2 3(la concaténation 1D en quelque sorte)

Donc, fondamentalement, à la fin du traitement, vous avez un grand tampon représentant

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

  1. Étiquettes

Ce type de mise en forme se fait beaucoup pour les problèmes de classification. Au lieu de classer avec un nombre, ils prennent un tableau booléen. Pour prévoir 7 classes sur 10, nous considérerions [0,0,0,0,0,0,0,1,0,0] // 1 in 7e position, array 0-indexed

Ce que vous pouvez faire pour commencer

  • Prenez votre image (et son étiquette associée)
  • Chargez votre image sur la toile
  • Extraire son tampon associé
  • Concatène tout le tampon de votre image en tant que grand tampon. C'est tout pour xs.
  • Prenez toutes vos étiquettes associées, mappez-les en tant que tableau booléen et concaténez-les.

Ci-dessous, je sous-classe MNistData::load(le reste peut être laissé tel quel (sauf dans script.js où vous devez plutôt instancier votre propre classe)

Je génère toujours des images 28x28, j'écris un chiffre dessus et j'obtiens une précision parfaite car je n'inclus pas de bruit ou d'étiquettes volontairement erronées.


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
la source
8

J'ai trouvé un tutoriel [1] sur la façon d'utiliser le modèle existant pour former de nouvelles classes. Principales parties du code ici:

tête index.html:

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

index.html corps:

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

    ....

L'idée principale est d'utiliser le réseau existant pour faire sa prédiction, puis de remplacer l'étiquette trouvée par la vôtre.

Le code complet est dans le tutoriel. Un autre prometteur, plus avancé dans [2]. Il a besoin d'un prétraitement strict, donc je ne le laisse qu'ici, je veux dire qu'il est beaucoup plus avancé.

Sources:

[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
la source
S'il vous plaît, jetez un œil à ma deuxième réponse, elle est beaucoup plus proche de la réalité, par où commencer.
mico
Pourquoi ne pas mettre les deux réponses en une seule?
edkeveked
Ils ont une approche tellement différente de la même chose. Celui ci-dessus, où je commente maintenant est en fait une solution de contournement, l'autre commence à partir des bases, qui je pense maintenant plus tard est plus approprié vers le réglage de la question.
mico
3

TL; DR

MNIST est la reconnaissance d'image Hello World. Après l'avoir appris par cœur, ces questions dans votre esprit sont faciles à résoudre.


Réglage de la question:

Votre principale question écrite est

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

à l'intérieur de votre bloc de code. Pour ceux que j'ai trouvé la réponse parfaite à partir d'exemples de la section d'exemples Tensorflow.js: exemple MNIST. Mes liens ci-dessous contiennent des versions javascript et node.js et une explication Wikipedia. Je vais les parcourir au niveau nécessaire pour répondre à la question principale dans votre esprit et j'ajouterai également des perspectives sur la façon dont vos propres images et étiquettes ont quelque chose à voir avec l'ensemble d'images MNIST et les exemples qui l'utilisent.

Tout d'abord:

Extraits de code.

où passer les images (exemple 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;
}

Remarques:

Le jeu de données MNIST est une image énorme, où dans un fichier il y a plusieurs images comme des tuiles dans un puzzle, chacune avec la même taille, côte à côte, comme des boîtes dans la table de coordination x et y. Chaque boîte a un échantillon et x et y correspondants dans le tableau des étiquettes ont l'étiquette. Dans cet exemple, il n'est pas très important de le transformer en plusieurs fichiers, de sorte qu'en fait, une seule image à la fois est donnée à la boucle while à gérer.

Étiquettes:

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

Remarques:

Ici, les étiquettes sont également des données d'octets dans un fichier. Dans le monde Javascript, et avec l'approche que vous avez dans votre point de départ, les étiquettes pourraient également être un tableau json.

former le modèle:

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

Remarques:

Voici model.fitla véritable ligne de code qui fait la chose: entraîne le modèle.

Résultats de l'ensemble:

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

Remarque:

En Data Science, également cette fois-ci, la partie la plus intéressante est de savoir dans quelle mesure le modèle survit au test de nouvelles données et sans étiquettes, peut-il les étiqueter ou non? Car c'est la partie évaluation qui nous imprime maintenant quelques chiffres.

Perte et précision: [4]

Plus la perte est faible, meilleur est le modèle (à moins que le modèle ne soit trop adapté aux données d'entraînement). La perte est calculée lors de la formation et de la validation et son interaction est la performance du modèle pour ces deux ensembles. Contrairement à la précision, la perte n'est pas un pourcentage. Il s'agit d'une somme des erreurs commises pour chaque exemple dans les ensembles de formation ou de validation.

..

La précision d'un modèle est généralement déterminée après que les paramètres du modèle ont été appris et fixés et qu'aucun apprentissage n'a lieu. Ensuite, les échantillons de test sont introduits dans le modèle et le nombre d'erreurs (perte nulle) que le modèle commet est enregistré, après comparaison avec les vraies cibles.


Plus d'information:

Dans les pages github, dans le fichier README.md, il y a un lien vers le tutoriel, où tout dans l'exemple github est expliqué plus en détail.


[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] Comment interpréter la "perte" et la "précision" pour un modèle d'apprentissage automatique

mico
la source