Jak wytrenować model w nodejs (tensorflow.js)?

29

Chcę utworzyć klasyfikator obrazów, ale nie znam Pythona. Tensorflow.js działa z javascript, który znam. Czy można na tym trenować modele i jakie kroki należy tego dokonać? Szczerze mówiąc, nie mam pojęcia, od czego zacząć.

Jedyne, co wymyśliłem, to jak załadować „mobilenet”, który najwyraźniej jest zestawem wstępnie przeszkolonych modeli i klasyfikować za pomocą niego obrazy:

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

co działa, ale nie ma dla mnie sensu, ponieważ chcę trenować własny model, używając moich obrazów z tworzonymi przeze mnie etykietami.

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

Powiedz, że mam kilka zdjęć i etykiet. Jak używać ich do trenowania modelu?

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
źródło
gdzie stoisz przed problemem. jeśli załadowałeś tensorflow, możesz wytrenować własny model
Abhishek Anand,
2
Wygląda na to, że możesz trenować modele z tensorflow.js tensorflow.org/js/guide/train_models Użyłem TensorFlow z pythonem. Jeśli TensorFlow.js nie korzysta z GPU, szkolenie może zająć dużo czasu. Dla mnie colab.research.google.com był przydatnym zasobem, ponieważ jest bezpłatny i zapewnia 11 GB GPU.
canbax,
1
To zbyt szerokie pytanie ... Jak wskazano w dokumentacji , możesz użyć ml5 do trenowania modelu lub użyć TF.js bezpośrednio, jak w tym przykładzie Node.js (rozwiń przykładowy kod, aby zobaczyć przykład szkolenia).
jdehesa
Ale nigdzie w tym kodzie nie widzę, jak przekazać obrazy i etykiety?
Alex
@Alex Są one przekazywane do fitmetody lub w przekazywanym zestawie danych fitDataset, jak pokazano w przykładach.
jdehesa,

Odpowiedzi:

22

Przede wszystkim obrazy należy przekonwertować na tensory. Pierwszym podejściem byłoby utworzenie tensora zawierającego wszystkie cechy (odpowiednio tensora zawierającego wszystkie etykiety). Tak powinno być tylko wtedy, gdy zestaw danych zawiera kilka obrazów.

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

Etykiety byłyby tablicą wskazującą typ każdego obrazu

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

Trzeba teraz stworzyć gorące kodowanie etykiet

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

Gdy pojawią się tensory, należałoby stworzyć model treningu. Oto prosty model.

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

Następnie model można wytrenować

model.fit(tensorFeatures, tensorLabels)

Jeśli zestaw danych zawiera wiele obrazów, należałoby zamiast tego utworzyć tfDataset. Ta odpowiedź wyjaśnia, dlaczego.

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

I użyj model.fitDataset(ds)do trenowania modelu


Powyższe dotyczy treningu w nodejs. Aby wykonać takie przetwarzanie w przeglądarce, genFeatureTensormożna napisać w następujący sposób:

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

Jedno słowo ostrożności: ciężkie przetwarzanie może zablokować główny wątek w przeglądarce. Tutaj wchodzą w grę pracownicy sieci.

edkeveked
źródło
szerokość i wysokość z inputShape musi pasować do szerokości i wysokości obrazów? Więc nie mogę przekazać zdjęć o różnych wymiarach?
Alex
Tak, muszą się zgadzać. Jeśli masz obrazy o różnych szerokościach i wysokościach od inputShape modelu, musisz zmienić rozmiar obrazu za pomocątf.image.resizeBilinear
edkeveked
Cóż, to naprawdę nie działa. Dostaję błędy
Alex
1
@Alex Czy możesz zaktualizować swoje pytanie o podsumowanie modelu i kształt ładowanego obrazu? Wszystkie obrazy muszą mieć ten sam kształt, inaczej obraz musiałby zostać zmieniony podczas szkolenia
edkeveked,
1
cześć @edkeveked, mówię o wykrywaniu obiektów, dodałem tutaj nowe pytanie, proszę, spójrz stackoverflow.com/questions/59322382/...
Pranoy Sarkar
10

Rozważ przykład https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/#0

Co oni robią to:

  • Zrób DUŻY obraz png (pionowe połączenie zdjęć)
  • weź jakieś etykiety
  • zbuduj zestaw danych (data.js)

następnie trenuj

Zestaw danych jest następujący:

  1. obrazy

Duży obraz jest podzielony na n pionowych fragmentów. (n jest chunkSize)

Rozważ kawałek rozmiaru 2.

Biorąc pod uwagę macierz pikseli obrazu 1:

  1 2 3
  4 5 6

Biorąc pod uwagę matrycę pikseli obrazu 2, jest

  7 8 9
  1 2 3

Powstała tablica byłaby 1 2 3 4 5 6 7 8 9 1 2 3(jakoś konkatenacja 1D)

Zasadniczo pod koniec przetwarzania masz duży bufor reprezentujący

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

  1. etykiety

Tego rodzaju formatowanie wykonuje się często w przypadku problemów z klasyfikacją. Zamiast klasyfikować za pomocą liczby, biorą tablicę boolowską. Aby przewidzieć 7 z 10 klas, które rozważymy [0,0,0,0,0,0,0,1,0,0] // 1 in 7e position, array 0-indexed

Co możesz zrobić, aby zacząć?

  • Zrób zdjęcie (i powiązaną z nim etykietę)
  • Załaduj obraz na płótno
  • Wyodrębnij powiązany bufor
  • Połącz bufor całego obrazu jako duży bufor. To wszystko dla xs.
  • Weź wszystkie powiązane etykiety, zamapuj je jako tablicę boolowską i połącz je.

Poniżej I podklasę MNistData::load(resztę można pozostawić taką, jaka jest (z wyjątkiem script.js, gdzie zamiast tego należy utworzyć instancję własnej klasy)

Nadal generuję obrazy 28 x 28, piszę na nich cyfrę i uzyskuję idealną dokładność, ponieważ nie uwzględniam szumu ani dobrowolnie niewłaściwych etykiet.


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
źródło
8

Znalazłem tutorial [1], jak wykorzystać istniejący model do szkolenia nowych klas. Główne części kodu tutaj:

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

    ....

Główną ideą jest wykorzystanie istniejącej sieci do wykonania prognozy, a następnie zastąpienie znalezionej etykiety własną.

Kompletny kod znajduje się w samouczku. Kolejna obiecująca, bardziej zaawansowana w [2]. Wymaga ścisłego wstępnego przetwarzania, więc zostawiam to tylko tutaj, to znaczy, że jest o wiele bardziej zaawansowane.

Źródła:

[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
źródło
Proszę spojrzeć na moją drugą odpowiedź, od której jest znacznie bliżej rzeczywistości.
mico
Dlaczego nie połączyć obu odpowiedzi w jedną?
edkeveked
Mają tak różne podejście do tego samego. Ten powyżej, w którym teraz komentuję, jest w rzeczywistości obejściem, drugi zaczyna się od podstaw, które, jak myślę, teraz są bardziej odpowiednie do zadawania pytań.
mico
3

TL; DR

MNIST to rozpoznawanie obrazów Hello World. Po nauczeniu się na pamięć, te pytania w twoim umyśle są łatwe do rozwiązania.


Ustawienie pytania:

Twoje główne pytanie jest napisane

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

wewnątrz twojego bloku kodu. Dla tych znalazłem idealną odpowiedź z przykładów sekcji przykładów Tensorflow.js: przykład MNIST. Moje poniższe linki mają czystą wersję javascript i node.js oraz objaśnienie Wikipedii. Przejdę je na poziomie niezbędnym do odpowiedzi na główne pytanie w twoim umyśle i dodam również perspektywy, w jaki sposób twoje własne obrazy i etykiety mają cokolwiek wspólnego z zestawem obrazów MNIST i przykładami z nich korzystającymi.

Po pierwsze:

Fragmenty kodu.

gdzie przekazać obrazy (próbka 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;
}

Uwagi:

Zestaw danych MNIST to ogromny obraz, w którym w jednym pliku znajduje się kilka obrazów, takich jak kafelki w puzzlach, każdy o tym samym rozmiarze, obok siebie, jak pola w tabeli koordynacyjnej xiy. Każde pudełko ma jedną próbkę, a odpowiednie xiy w tablicy etykiet mają etykietę. Z tego przykładu nie jest wielkim problemem, aby zmienić go na kilka formatów plików, tak że właściwie tylko jedno zdjęcie na raz jest przekazywane pętli while do obsługi.

Etykiety:

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

Uwagi:

W tym przypadku etykiety są również danymi bajtowymi w pliku. W świecie Javascript i przy podejściu do punktu wyjścia etykiety mogą być również tablicą json.

wytrenuj model:

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

Uwagi:

Oto model.fitwłaściwy wiersz kodu, który działa: trenuje model.

Wyniki całej sprawy:

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

Uwaga:

W Data Science, również tym razem, najbardziej fascynujące jest to, jak dobrze model przetrwa test nowych danych i żadnych etykiet, czy może je oznaczać, czy nie? Jest to część ewaluacyjna, która drukuje nam teraz niektóre liczby.

Utrata i dokładność: [4]

Im niższa strata, tym lepszy model (chyba że model zbytnio dopasował się do danych treningowych). Stratę oblicza się na podstawie szkolenia i walidacji, a jej interperacja określa, jak dobrze model radzi sobie w tych dwóch zestawach. W przeciwieństwie do dokładności, strata nie jest procentem. Jest to suma błędów popełnianych dla każdego przykładu w zestawach szkoleniowych lub walidacyjnych.

..

Dokładność modelu jest zwykle określana po poznaniu i ustaleniu parametrów modelu i braku uczenia się. Następnie próbki testowe są podawane do modelu i rejestrowana jest liczba błędów (zero zero jeden), które popełnił model, po porównaniu do prawdziwych celów.


Więcej informacji:

Na stronach github w pliku README.md znajduje się link do samouczka, w którym wszystko w przykładzie github wyjaśniono bardziej szczegółowo.


[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] Jak interpretować „utratę” i „dokładność” dla modelu uczenia maszynowego

mico
źródło