Wie trainiere ich ein Modell in nodejs (tensorflow.js)?

29

Ich möchte einen Bildklassifizierer erstellen, kenne aber Python nicht. Tensorflow.js arbeitet mit Javascript, mit dem ich vertraut bin. Können Modelle damit trainiert werden und was wären die Schritte dazu? Ehrlich gesagt habe ich keine Ahnung, wo ich anfangen soll.

Das einzige, was ich herausgefunden habe, ist, wie man "mobilet", anscheinend eine Reihe von vorab trainierten Modellen, lädt und Bilder damit klassifiziert:

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

Das funktioniert, aber es nützt mir nichts, weil ich mein eigenes Modell mit meinen Bildern mit von mir erstellten Beschriftungen trainieren möchte.

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

Angenommen, ich habe eine Reihe von Bildern und Etiketten. Wie trainiere ich damit ein Modell?

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
quelle
Wo stehen Sie vor dem Problem? Wenn Sie Tensorflow geladen haben, können Sie Ihr eigenes Modell trainieren
Abhishek Anand
2
Es scheint, als könnten Sie Modelle mit tensorflow.js trainieren. Tensorflow.org/js/guide/train_models Ich habe TensorFlow mit Python verwendet. Wenn TensorFlow.js keine GPU verwendet, kann das Training lange dauern. Für mich war colab.research.google.com eine nützliche Ressource, da es kostenlos ist und 11 GB GPU bietet.
Canbax
1
Das ist zu weit gefasst eine Frage ... zeigte Wie aus der Dokumentation , können Sie ML5 trainieren ein Modell oder Verwendung TF.js direkt, wie in diesem Beispiel Node.js (expand Beispielcode ein Trainingsbeispiel zu sehen).
Jdehesa
Aber ich sehe nirgendwo in diesem Code, wie man die Bilder und Etiketten weitergibt?
Alex
@Alex Sie werden an die fitMethode oder an das übergebene Dataset übergeben fitDataset, wie in den Beispielen gezeigt.
Jdehesa

Antworten:

22

Zunächst müssen die Bilder in Tensoren konvertiert werden. Der erste Ansatz wäre, einen Tensor zu erstellen, der alle Merkmale enthält (bzw. einen Tensor, der alle Beschriftungen enthält). Dies sollte nur dann der Fall sein, wenn der Datensatz nur wenige Bilder enthält.

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

Die Beschriftungen wären ein Array, das den Typ jedes Bildes angibt

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

Man muss jetzt eine Hot-Codierung der Etiketten erstellen

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

Sobald es die Tensoren gibt, müsste man das Modell für das Training erstellen. Hier ist ein einfaches Modell.

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

Dann kann das Modell trainiert werden

model.fit(tensorFeatures, tensorLabels)

Wenn das Dataset viele Bilder enthält, müsste stattdessen ein tfDataset erstellt werden. Diese Antwort beschreibt , warum.

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

Und verwenden Sie model.fitDataset(ds), um das Modell zu trainieren


Das Obige ist für das Training in Nodejs. Um eine solche Verarbeitung im Browser durchzuführen, genFeatureTensorkann wie folgt geschrieben werden:

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

Ein Wort der Vorsicht ist, dass eine starke Verarbeitung den Hauptthread im Browser blockieren kann. Hier kommen Web-Worker ins Spiel.

edkeveked
quelle
Die Breite und Höhe der Eingabeform muss mit der Breite und Höhe der Bilder übereinstimmen. Ich kann also keine Bilder mit unterschiedlichen Abmessungen übergeben?
Alex
Ja, sie müssen übereinstimmen. Wenn Sie Bilder mit einer anderen Breite und Höhe als die Eingabeform des Modells haben, müssen Sie die Größe des Bilds mithilfe vontf.image.resizeBilinear
edkeveked
Nun, es funktioniert nicht wirklich. Ich bekomme Fehler
Alex
1
@Alex Könnten Sie bitte Ihre Frage mit der Modellzusammenfassung und der Form des zu ladenden Bildes aktualisieren? Alle Bilder müssen die gleiche Form haben, oder das Bild muss für das Training
angepasst werden
1
Hallo @edkeveked, ich spreche über Objekterkennung, ich habe hier eine neue Frage hinzugefügt, bitte werfen Sie einen Blick auf stackoverflow.com/questions/59322382/…
Pranoy Sarkar
10

Betrachten Sie das Beispiel https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/#0

Was sie tun ist:

  • Nehmen Sie ein GROSSES PNG-Bild auf (eine vertikale Verkettung von Bildern).
  • Nimm ein paar Etiketten
  • Erstellen Sie den Datensatz (data.js)

dann trainieren

Der Aufbau des Datensatzes ist wie folgt:

  1. Bilder

Das große Bild ist in n vertikale Abschnitte unterteilt. (n ist chunkSize)

Betrachten Sie eine ChunkSize der Größe 2.

Angesichts der Pixelmatrix von Bild 1:

  1 2 3
  4 5 6

Gegeben ist die Pixelmatrix von Bild 2

  7 8 9
  1 2 3

Das resultierende Array wäre 1 2 3 4 5 6 7 8 9 1 2 3(die 1D-Verkettung irgendwie)

Im Grunde genommen haben Sie am Ende der Verarbeitung einen großen Puffer, der darstellt

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

  1. Etiketten

Diese Art der Formatierung wird häufig bei Klassifizierungsproblemen durchgeführt. Anstatt mit einer Zahl zu klassifizieren, verwenden sie ein boolesches Array. Um 7 von 10 Klassen vorherzusagen, würden wir in Betracht ziehen [0,0,0,0,0,0,0,1,0,0] // 1 in 7e position, array 0-indexed

Was Sie tun können, um loszulegen

  • Nehmen Sie Ihr Bild (und das dazugehörige Etikett)
  • Laden Sie Ihr Bild auf die Leinwand
  • Extrahieren Sie den zugehörigen Puffer
  • Verketten Sie den gesamten Puffer Ihres Bildes als großen Puffer. Das war's für xs.
  • Nehmen Sie alle zugehörigen Beschriftungen, ordnen Sie sie als boolesches Array zu und verketten Sie sie.

Unten habe ich eine Unterklasse MNistData::load(der Rest kann unverändert gelassen werden (außer in script.js, wo Sie stattdessen Ihre eigene Klasse instanziieren müssen).

Ich generiere immer noch 28x28 Bilder, schreibe eine Ziffer darauf und erhalte eine perfekte Genauigkeit, da ich kein Rauschen oder freiwillig falsche Beschriftungen einbeziehe.


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

Ich fand ein Tutorial [1], wie man ein vorhandenes Modell verwendet, um neue Klassen zu trainieren. Hauptcodeteile hier:

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

    ....

Die Hauptidee besteht darin, das vorhandene Netzwerk zu verwenden, um seine Vorhersage zu treffen, und dann das gefundene Etikett durch Ihr eigenes zu ersetzen.

Der vollständige Code befindet sich im Tutorial. Ein weiteres vielversprechendes, fortgeschritteneres in [2]. Es bedarf einer strengen Vorverarbeitung, also lasse ich es nur hier, ich meine, es ist so viel fortgeschrittener.

Quellen:

[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
quelle
Bitte werfen Sie einen Blick auf meine zweite Antwort, sie ist der Realität viel näher, wo Sie anfangen sollen.
Mico
Warum nicht beide Antworten in einer zusammenfassen?
Edkeveked
Sie haben so unterschiedliche Herangehensweisen an dasselbe. Das obige, wo ich jetzt kommentiere, ist eigentlich eine Problemumgehung, das andere geht von den Grundlagen aus, die ich später für die Frageeinstellung für angemessener halte.
Mico
3

TL; DR

MNIST ist die Bilderkennung Hello World. Nachdem Sie es auswendig gelernt haben, sind diese Fragen in Ihrem Kopf leicht zu lösen.


Frageneinstellung:

Ihre Hauptfrage ist geschrieben

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

in Ihrem Codeblock. Für diejenigen fand ich die perfekte Antwort aus Beispielen von Tensorflow.js Beispielabschnitt: MNIST-Beispiel. Meine unten stehenden Links enthalten reine Javascript- und node.js-Versionen sowie Wikipedia-Erklärungen. Ich werde sie auf der Ebene durchgehen, die zur Beantwortung der Hauptfrage in Ihrem Kopf erforderlich ist, und ich werde auch Perspektiven hinzufügen, wie Ihre eigenen Bilder und Beschriftungen etwas mit dem MNIST-Bildsatz und den Beispielen zu tun haben, die ihn verwenden.

Das wichtigste zuerst:

Code Ausschnitte.

Wo Bilder übergeben werden sollen (Node.js Beispiel)

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

Anmerkungen:

Der MNIST-Datensatz ist ein riesiges Bild, in dem in einer Datei mehrere Bilder wie Kacheln im Puzzle enthalten sind, die alle dieselbe Größe nebeneinander haben, wie Kästchen in der x- und y-Koordinationstabelle. Jede Box hat eine Probe und das entsprechende x und y im Beschriftungsarray hat die Beschriftung. In diesem Beispiel ist es keine große Sache, das Format auf mehrere Dateien zu ändern, sodass der while-Schleife jeweils nur ein Bild zugewiesen wird.

Etiketten:

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

Anmerkungen:

Hier sind Beschriftungen auch Byte-Daten in einer Datei. In der Javascript-Welt und mit dem Ansatz, den Sie als Ausgangspunkt haben, können Beschriftungen auch ein JSON-Array sein.

trainiere das Modell:

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

Anmerkungen:

Hier model.fitist die eigentliche Codezeile, die das Ding macht: trainiert das Modell.

Ergebnisse der ganzen Sache:

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

Hinweis:

In Data Science ist es auch hier am schwierigsten zu wissen, wie gut das Modell den Test neuer Daten und ohne Beschriftungen übersteht. Kann es sie beschriften oder nicht? Denn das ist der Bewertungsteil, der uns jetzt einige Zahlen druckt.

Verlust und Genauigkeit: [4]

Je geringer der Verlust, desto besser ein Modell (es sei denn, das Modell hat sich zu stark an die Trainingsdaten angepasst). Der Verlust wird anhand von Training und Validierung berechnet, und seine Interferenz gibt an, wie gut das Modell für diese beiden Sätze abschneidet. Im Gegensatz zur Genauigkeit ist der Verlust kein Prozentsatz. Es ist eine Zusammenfassung der Fehler, die für jedes Beispiel in Trainings- oder Validierungssätzen gemacht wurden.

..

Die Genauigkeit eines Modells wird normalerweise bestimmt, nachdem die Modellparameter gelernt und festgelegt wurden und kein Lernen stattfindet. Dann werden die Testproben dem Modell zugeführt und die Anzahl der Fehler (Null-Eins-Verlust), die das Modell macht, wird nach dem Vergleich mit den wahren Zielen aufgezeichnet.


Mehr Informationen:

Auf den Github-Seiten in der Datei README.md befindet sich ein Link zum Lernprogramm, in dem alle im Github-Beispiel ausführlicher erläutert werden.


[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] Interpretation von "Verlust" und "Genauigkeit" für ein Modell des maschinellen Lernens

mico
quelle