Как обучить модель в nodejs (tenorflow.js)?

29

Я хочу сделать классификатор изображений, но я не знаю Python. Tensorflow.js работает с JavaScript, с которым я знаком. Могут ли модели обучаться с этим и каковы будут шаги для этого? Честно говоря, я понятия не имею, с чего начать.

Единственное, что я понял, это как загрузить «мобильную сеть», которая, по-видимому, представляет собой набор предварительно обученных моделей, и классифицировать изображения по ним:

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

это работает, но это бесполезно для меня, потому что я хочу тренировать свою собственную модель, используя мои изображения с метками, которые я создаю.

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

Скажем, у меня есть куча изображений и ярлыков. Как я могу использовать их для обучения модели?

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
источник
где вы столкнулись с проблемой. если вы загрузили тензор потока, вы можете тренировать свою собственную модель
Абхишек Ананд
2
Кажется, что вы можете обучать модели с tenorflow.js tenorflow.org/js/guide/train_models Я использовал TensorFlow с Python. Если TensorFlow.js не использует графический процессор, обучение может занять много времени. Для меня colab.research.google.com был полезным ресурсом, поскольку он бесплатный и предоставляет 11 ГБ графического процессора.
canbax
1
Это слишком широкий вопрос ... Как указано в документации , вы можете использовать ml5 для обучения модели или напрямую использовать TF.js, как в этом примере Node.js (разверните пример кода, чтобы увидеть пример обучения).
jdehesa
Но я нигде не вижу в этом коде, как передать изображения и метки?
Алекс
@Alex Они передаются fitметоду или в наборе данных fitDataset, как показано в примерах.
jdehesa

Ответы:

22

Прежде всего, изображения должны быть преобразованы в тензоры. Первый подход заключается в создании тензора, содержащего все признаки (соответственно, тензора, содержащего все метки). Это следует сделать, только если набор данных содержит несколько изображений.

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

Метки были бы массивом, указывающим тип каждого изображения

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

Теперь нужно создать горячую кодировку меток

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

Как только появятся тензоры, нужно будет создать модель для обучения. Вот простая модель.

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

Тогда модель можно обучить

model.fit(tensorFeatures, tensorLabels)

Если набор данных содержит много изображений, вместо этого нужно будет создать набор tfDataset. Этот ответ обсуждает почему.

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

И использовать model.fitDataset(ds)для обучения модели


Выше для обучения в nodejs. Чтобы сделать такую ​​обработку в браузере, genFeatureTensorможно написать так:

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

Одно предостережение: тяжелая обработка может блокировать основной поток в браузере. Это где веб-работники вступают в игру.

edkeveked
источник
ширина и высота от inputShape должны соответствовать ширине и высоте изображений? Так я не могу передать изображения с разными размерами?
Алекс
Да, они должны совпадать. Если у вас есть изображения, отличающиеся по ширине и высоте от inputShape модели, вам нужно изменить размер изображения с помощьюtf.image.resizeBilinear
edkeveked
Ну, это на самом деле не работает. Я получаю ошибки
Алекс
1
@ Алекс Не могли бы вы обновить свой вопрос с кратким описанием модели и формой загружаемого изображения? Все изображения должны иметь одинаковую форму или размер изображения должен быть изменен для обучения
edkeveked
1
Привет @edkeveked, я говорю об обнаружении объектов, я добавил новый вопрос здесь, пожалуйста, посмотрите stackoverflow.com/questions/59322382/…
Pranoy Sarkar
10

Рассмотрим пример https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/#0

Что они делают:

  • взять БОЛЬШОЕ изображение PNG (вертикальная конкатенация изображений)
  • взять несколько этикеток
  • построить набор данных (data.js)

затем тренироваться

Построение набора данных выглядит следующим образом:

  1. картинки

Большое изображение делится на n вертикальных кусков. (n это chunkSize)

Рассмотрим размер куска размера 2.

Учитывая матрицу пикселей изображения 1:

  1 2 3
  4 5 6

Учитывая матрицу пикселей изображения 2

  7 8 9
  1 2 3

Результирующий массив будет 1 2 3 4 5 6 7 8 9 1 2 3(конкатенация 1D как-то)

Так что в основном в конце обработки у вас есть большой буфер, представляющий

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

  1. этикетки

Такого рода форматирование сделано для задач классификации. Вместо того, чтобы классифицировать по числу, они принимают логический массив. Для прогнозирования 7 из 10 классов мы рассмотрим [0,0,0,0,0,0,0,1,0,0] // 1 in 7e position, array 0-indexed

Что вы можете сделать, чтобы начать

  • Возьмите свое изображение (и связанный с ним ярлык)
  • Загрузите свое изображение на холст
  • Извлеките связанный с ним буфер
  • Объедините весь буфер вашего изображения как большой буфер. Вот и все для хз.
  • Возьмите все связанные с ними метки, сопоставьте их как логический массив и объедините их.

Ниже я подкласс MNistData::load(остальное можно разрешить как есть (кроме как в script.js, где вместо этого вам нужно создать собственный экземпляр)

Я по-прежнему генерирую изображения размером 28x28, пишу на них цифру и получаю идеальную точность, поскольку я не включаю шум или добровольно неправильные надписи.


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
источник
8

Я нашел учебное пособие [1], как использовать существующую модель для обучения новых классов. Основные части кода здесь:

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

    ....

Основная идея состоит в том, чтобы использовать существующую сеть, чтобы сделать ее прогноз, а затем заменить найденную метку своей.

Полный код в учебнике. Другой многообещающий, более продвинутый в [2]. Она требует строгой предварительной обработки, поэтому я оставляю ее только здесь, я имею в виду, что она намного более продвинутая.

Источники:

[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

Мико
источник
Пожалуйста, посмотрите на мой второй ответ, он гораздо ближе к реальности, с чего начать.
Мико
Почему бы не объединить оба ответа в один?
edkeveked
У них такой разный подход к одной и той же вещи. Это выше одно, где я сейчас комментирую, на самом деле является обходным путем, другое начинается с основ, которые, как мне кажется, теперь будут более подходящими для постановки вопроса.
Мико
3

TL; DR

MNIST - это распознавание изображений Hello World. Выучив это наизусть, эти вопросы легко решить.


Постановка вопроса:

Ваш главный вопрос написан

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

внутри вашего блока кода. Для тех, кто нашел идеальный ответ из примеров раздела примеров Tensorflow.js: пример MNIST. Мои ссылки ниже содержат только версии javascript и node.js, а также пояснения из Википедии. Я расскажу о них на уровне, необходимом для ответа на главный вопрос в вашем уме, и добавлю также перспективы того, как ваши собственные изображения и метки имеют какое-либо отношение к набору изображений MNIST и примеры, использующие его.

Перво-наперво:

Фрагменты кода.

куда передавать изображения (образец 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;
}

Ноты:

Набор данных MNIST - это огромное изображение, в котором в одном файле есть несколько изображений, таких как мозаика в мозаике, каждое с одинаковым размером, бок о бок, как прямоугольники в координатной таблице x и y. Каждый блок имеет один образец, и соответствующие x и y в массиве меток имеют метку. Из этого примера нет ничего сложного в том, чтобы преобразовать его в формат нескольких файлов, так что фактически для обработки в цикле while дается только один рис за раз.

Метки:

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

Ноты:

Здесь метки также являются байтовыми данными в файле. В мире Javascript и с подходом, который вы используете в своей начальной точке, метки также могут быть массивом json.

обучаем модель:

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

Ноты:

Вот model.fitфактическая строка кода, которая делает это: тренирует модель.

Результаты всего этого:

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

Замечания:

В Data Science, также и в этот раз, самая важная часть состоит в том, чтобы узнать, насколько хорошо модель выдерживает испытание новыми данными и отсутствием меток, может ли она маркировать их или нет? Поскольку это часть оценки, которая теперь печатает нам некоторые цифры.

Потеря и точность: [4]

Чем меньше потери, тем лучше модель (если модель не перетянута на тренировочные данные). Потери рассчитываются при обучении и проверке, и их взаимодействие определяет, насколько хорошо модель справляется с этими двумя наборами. В отличие от точности, потери не в процентах. Это сумма ошибок, сделанных для каждого примера в обучающих или проверочных наборах.

..

Точность модели обычно определяется после того, как параметры модели изучены и зафиксированы, а обучение не проводится. Затем тестовые образцы подаются в модель, и после сравнения с истинными целями регистрируется количество ошибок (потеря ноль один), которые делает модель.


Больше информации:

На страницах github в файле README.md есть ссылка на учебник, где все в примере с github объясняется более подробно.


[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] Как интерпретировать «потери» и «точность» для модели машинного обучения

Мико
источник