如何在 nodejs (tensorflow.js) 中训练模型?

Posted

技术标签:

【中文标题】如何在 nodejs (tensorflow.js) 中训练模型?【英文标题】:How to train a model in nodejs (tensorflow.js)? 【发布时间】:2020-03-16 02:45:43 【问题描述】:

我想做一个图像分类器,但我不懂python。 Tensorflow.js 适用于我熟悉的 javascript。可以用它训练模型吗?这样做的步骤是什么? 坦率地说,我不知道从哪里开始。

我唯一想到的是如何加载“mobilenet”,这显然是一组预训练模型,并用它对图像进行分类:

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 ?


【问题讨论】:

您在哪里遇到问题。如果你已经加载了 tensorflow,你可以训练你自己的模型 看来你可以用 tensorflow.js 训练模型 tensorflow.org/js/guide/train_models 我用 TensorFlow 和 python。如果 TensorFlow.js 未使用 GPU,则训练可能需要很长时间。对我来说,colab.research.google.com 是一个有用的资源,因为它是免费的并且提供 11 GB 的 GPU。 这个问题太笼统了...正如the docs中指出的,你可以使用ml5到train一个模型或者直接使用TF.js,比如this Node.js example(展开示例代码以查看训练示例)。 但我在该代码中没有看到如何传递图像和标签? @Alex 将它们传递给fit 方法,或者在数据集中传递给fitDataset,如示例所示。 【参考方案1】:

首先,需要将图像转换为张量。第一种方法是创建一个包含所有特征的张量(分别是一个包含所有标签的张量)。仅当数据集包含少量图像时才应采用这种方式。

  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。这个answer 讨论了原因。

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

需要注意的是,执行繁重的处理可能会阻塞浏览器中的主线程。这就是网络工作者发挥作用的地方。

【讨论】:

inputShape 的宽度和高度必须与图像的宽度和高度匹配?所以我不能传递不同尺寸的图像? 是的,它们必须匹配。如果您有与模型​​的 inputShape 不同宽度和高度的图像,则需要使用 tf.image.resizeBilinear 调整图像大小 错误:操作数无法与形状 4,10 和 4,98,198,10 一起广播。 @Alex 您能否用模型摘要和您正在加载的图像的形状更新您的问题?所有图像都需要具有相同的形状,否则需要为训练调整图像大小 嗨@edkeveked,我说的是对象检测,我在这里添加了一个新问题,请看***.com/questions/59322382/…【参考方案2】:

TL;DR

MNIST 是图像识别 Hello World。熟记下来,你心中的这些问题就迎刃而解了。


问题设置:

你写的主要问题是

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

在您的代码块内。对于那些我从 Tensorflow.js 示例部分的示例中找到完美答案的人:MNIST 示例。我的以下链接有纯 javascript 和 node.js 版本以及 Wikipedia 解释。我将在必要的层面上对它们进行回答,以回答您心中的主要问题,我还将添加您自己的图像和标签如何与 MNIST 图像集以及使用它的示例有关的观点。

第一件事:

代码 sn-ps。

在哪里传递图像(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)`);

注意:

在数据科学中,这一次也是如此,最令人着迷的部分是知道模型在新数据和没有标签的测试中的生存能力如何,它可以给它们加标签吗?因为那是评估部分,现在我们会打印一些数字。

损失和准确性:[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]How to interpret "loss" and "accuracy" for a machine learning model

【讨论】:

【参考方案3】:

考虑示例https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/#0

他们所做的是:

拍摄一张 BIG png 图像(图像的垂直串联) 取一些标签 构建数据集 (data.js)

然后训练

数据集的构建如下:

    图片

大图被分成n个垂直块。 (n 为 chunkSize)

考虑大小为 2 的 chunkSize。

给定图像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(一维串联)

所以基本上在处理结束时,你有一个很大的缓冲区表示

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

    标签

这种格式对分类问题做了很多。他们没有使用数字进行分类,而是采用布尔数组。 要预测 10 个类别中的 7 个,我们会考虑 [0,0,0,0,0,0,0,1,0,0] // 1 in 7e position, array 0-indexed

你可以做些什么来开始

获取您的图片(及其相关标签) 将图像加载到画布上 提取其关联的缓冲区 将所有图像缓冲区连接为一个大缓冲区。 xs 就是这样。 获取所有关联标签,将它们映射为布尔数组,然后将它们连接起来。

下面,我将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);
  


【讨论】:

【参考方案4】:

我找到了一个教程 [1] 如何使用现有模型来训练新课程。主要代码部分在这里:

index.html 头:

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

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

    ....

主要思想是利用现有网络进行预测,然后将找到的标签替换为您自己的标签。

完整的代码在教程中。 [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

【讨论】:

请看我的第二个答案,它更接近现实,从哪里开始。 为什么不把两个答案合二为一? 他们对同一件事有如此不同的方法。上面这个,我现在评论的实际上是一种解决方法,另一个是从基础开始,我认为现在更适合问题设置。

以上是关于如何在 nodejs (tensorflow.js) 中训练模型?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Tensorflow.js 中获取预测值

Tensorflow.js 中的内存泄漏:如何清理未使用的张量?

如何利用TensorFlow.js部署简单的AI版「你画我猜」图像识别应用

在 Tensorflow.js 中获取张量中项目的值

教程 | 如何利用TensorFlow.js部署简单的AI版「你画我猜」图像识别应用

当微信小程序遇上TensorFlow - tensorflow.js篇