开源技术 * IBM 微讲堂:Kubeflow 系列(观看回放 | 下载讲义) 了解详情

使用 TensorFlow.js 编写深度学习模型的代码

在上一篇教程“AI in Node.js 简介”中,我们解释了在 Node.js 应用程序中嵌入深度学习模型的两种基本方法。在本教程中,我们将更进一步,向您展示如何从头开始构建和训练一个简单的深度学习模型。因此,您需要更深入地了解深度学习模型的工作方式才能从本教程中充分获益。

我们从深度学习的编程概念开始,讨论两个不同的编程 API:高级别的 Layers API 和低级别的 Core API。您将为一个简单的模型编写代码来对服装项目进行分类,使用一个小数据集对其进行训练,并评估模型的准确性。然后,为了了解深入学习中的一种常见做法,您将采用经过训练的模型,并应用迁移学习来指导模型对新项目进行分类。我们还描述了如何从 Python 等其他来源获取预训练的模型,并将其转换为可以在 JavaScript 中使用的格式。

为什么要采用 JavaScript 编写模型代码

到目前为止,我们已经看到实际的深度学习模型可以隐藏在 npm 包中,可以从二进制格式加载,也可以通过 REST API 提供。在这些情况下,我们只是在模型上运行一个推理,我们并不关心模型是如何实现的。

许多模型都是用 Python 实现的,因为 Python 是数据科学家的热门之选,而且在函数方面拥有最佳支持。然而,深度学习在所有类型的应用程序中得到广泛采用,吸引了来自不同编程语言背景的开发者。此外,实现模型的实践已经被更好地理解和广泛采用,使更多开发者能够自行构建更适合其应用程序的模型。

幸运的是,TensorFlow 设计为支持不同的语言绑定,特别是 Python、C、R、JavaScript 和 Java™ 编程。因为每种语言都有自己的独特优势,所以开发者选择自己的编程语言是有其原因的。因此,支持开发者继续使用其熟悉的编程环境,而不是要求他们学习新的语言也是非常重要的。

为什么采用高级别 API

深度学习遵循与其他技术一样的成熟度趋势。在早期,技术的实现倾向于使用低级别的构造来完成,但是随着技术的成熟,通用模式开始出现,它们被捕获为高级别的构造,这样实现就更快、更容易。对于深度学习,这些模式包括神经网络中常用的层类型及其激活功能、针对优化的实际选项以及用于监控模型性能的指标。然后,实现这些模式的代码将打包到高级别编程 API 中,以便可轻松复用。

采用高级别 API 编写模型代码可以让您专注于高级别设计并避免低级别实现的琐碎细节,从而提高工作效率。代码会简短得多,更容易阅读和维护。

前提条件

要学习本教程,您需要:

预估时间

完成本教程大约需要 40 分钟。

步骤

  1. TensorFlow.js 中的编程概念
  2. 将预训练的模型转换为 TensorFlow.js
  3. 使用 TensorFlow.js 构建深度学习模型
  4. 使用 TensorFlow.js 迁移学习

TensorFlow.js 中的编程概念

从概念上讲,神经网络由多层权重以及计算组成,这些权重在图中表示为节点和边。编程平台以不同的方式支持这些图的实现。在早期的方法中,您需要通过显式分配张量并对张量上的单个计算进行编码,从而在低级别上工作。随着技术在过去几年迅速发展,通用模式开始出现,这些模式内置到更高级别的编程抽象中,其中神经网络中的整个层都可以作为 API 在平台中使用。通过将这些层堆叠在一起,可以轻松地构建模型,从而大大简化实现。

在 Python 中,Keras 是这一层方法的最热门 API,而早期的 TensorFlow Python 库针对的是低级别方法。在 JavaScript 中,TensorFlow.js 通过其低级别和高级别 API 支持这两种编程风格。我们介绍了这两种编程风格中的关键概念。

完整的 API 文档可在 TensorFlow.js API 网站上获得。

高级别 API:Layers

Layers API 模仿 Python 中的 Keras 编程风格,不过使用的是 JavaScript 语法。Layers API 与 Keras 之间高度相似,但并非一一对应地完全相同。这里的主要编程抽象由模型和层组成。您将创建一个模型对象,表示您的深度学习模型,然后向模型中添加任意数量的层以实现您的模型架构。

有两种方法可以构造您的模型:

  1. tf.sequential():要构建模型,最简单的方法是按线性顺序排列各层,一个层为下一个层馈送信息。层之间的张量是自动分配的,因此您只需要管理为第一层馈送信息的输入张量。
  2. tf.model():这些层可以排列成任意的无圈图。您需要管理各层之间的所有张量,并且通过 apply() 方法将输入连接到每个层。

有许多类型的可以用来实现您的模型架构。

  • 基本层:常用函数
  • 激活层:各种激活函数,通常放在主要层末尾
  • 卷积层:各种版本的卷积函数
  • 合并层:通用矩阵运算
  • 归一化层:将激活输出归一化为平均值 0,标准差 1。
  • 池化层:按平均值或最大值池化值
  • 循环层:循环网络的各个层
  • 包装层:在另一层之上应用一些变换
  • 输入层:管理顺序层的输入
  • 填充层:用一些值(通常为零)填充图像的边框
  • 噪音层:丢弃训练过程中正则化的函数以避免过拟合
  • 遮蔽层:在特定条件下跳过剩余层
  • 正则化层:避免过拟合

为权重、偏见和内核初始化张量也非常重要,这样模型的性能会在训练期间收敛。

在使用适当的层定义了模型架构之后,必须指定训练所需的三个参数。可以通过 tf.LayersModel 中的 compile 方法来完成此操作。

  • 优化器
  • 损失函数
  • 指标

可以将这些参数指定为方便的字符串名称,如“accuracy”,也可以指定为使用低级别 Core API(如下所述)创建的对象。

要训练 Layers 模型,tf.LayersModel API 提供了两种方法:

  • fit 将训练运行固定次数的迭代。
  • fitDataset 针对 Dataset 对象提供的输入运行训练。

有关更多详细信息,请访问关于层的 TensorFlow.js 页面

组合顺序模型

现在,您对 TensorFlow.js API 有了更清楚的了解。以下列表总结了如何组合顺序模型。

  1. 定义神经网络层,包括:
    • 层类型
    • 神经节点数
    • 激活函数
    • 初始化程序
  2. 选择合适的优化器来训练模型,如 sgdadam
  3. 确定损失函数,以尽可能缩短模型输出与标签之间的距离。
  4. 选择要在训练模型时监控的指标的列表。
  5. 使用优化器、损失函数和指标调用 compile()
  6. 通过将数据集拟合到模型,开始模型训练。
  7. 监控训练进度并评估训练后的模型。

训练完成后,您可以使用顺序模型的 save API 将训练后的模型存储到文件系统中。

await model.save('file:///path/to/my-model');

稍后,您可以使用 tf.loadLayersModel API 从文件系统加载模型。

const model = await tf.loadLayersModel('file://path/to/my-model/model.json');

低级别 API:Core

在低级别上,深度学习模型是一个有向图,其中节点表示运算,边缘表示流经图的数据。根据此概念,低级别编程构造包括:

  1. 数据对象 tf.tensor 是各种数据类型的多维数组。为支持典型的用法,API 允许您创建各种形状的张量,这些张量由各种数据模式填充。

  2. 运算包括对一些输入张量进行的线性代数和机器学习计算,因此产生了一个新的 tf.tensor。支持基本激活函数,包括 SigmoidRELUleaky RELU。该 API 还包括一些用于音频图像处理的特殊用途运算。

有关张量和运算的更多信息,请参阅张量和运算页面。

对于低级别的训练,Core API 支持以下函数:

  • 计算预测结果与预期结果之间的损失函数
  • 选择最佳优化策略以更好地收敛到需要的性能
  • 跟踪训练后的模型的性能

对所选优化器的训练通过 Optimizer.minimize() 方法完成。

将预训练的模型转换为 TensorFlow.js

虽然 TensorFlow.js 有许多开源的预训练模型,但更多的模型是以 TensorFlow 和 Keras Python 格式进行训练和提供的。对于这些模型,必须进行转换,然后才能将其用于通过 TensorFlow.js 进行推理。

TensorFlow SavedModel 和 Keras 模型可以进行转换。但是,如果模型包含 TensorFlow.js 不支持的运算,转换将失败。请参阅 TensorFlow.js 支持的运算的完整列表

在本节中,我们将展示一个将 Keras HDF5 模型转换为 TensorFlow.js GraphModel 格式的示例。其他格式和转换场景可以在 tensorflow/tfjs GitHub 存储库中找到。

前提条件

可以使用 tensorflowjs Python 包安装 tensorflowjs_converter 实用程序。tensorflowjs 要求(并安装)特定版本的 TensorFlow 和 Keras。为了确保兼容性并避免破坏现有的 Python 环境,建议在虚拟环境或 Docker 中使用 Python 3.6.8 安装 tensorflowjs

如果系统已经满足以下要求,可以跳过此步骤。

  • Python 3.6.8:使用 pyenv 说明来安装或管理额外的 Python 运行时。
  • Virtualenv:使用特定于操作系统的说明来设置,或者使用以下 virtualenv 说明

例如,我们将预训练的 Fashion-MNIST Keras HDF5 模型转换为 TensorFlow.js GraphModel 格式。

  1. 创建一个文件夹作为工作区,创建两个子文件夹用于转换。

     mkdir tfjs_converter
     cd tfjs_converter
     mkdir kerasmodel graphmodel
    
  2. fashion_mnist.h5 模型下载到 kerasmodel 文件夹中。

     wget -P kerasmodel/ https://dax-cdn.cdn.appdomain.cloud/dax-fashion-mnist/1.0.2/pre-trained-models/fashion_mnist.h5
    

转换

选择以下某个选项。

选项 1:在 Virtualenv 中安装 TensorFlowjs_converter

  1. 使用 pyenv 安装额外的 Python 3.6.8 运行时。

     pyenv install 3.6.8
    
  2. 在具有 Python 3.6.8 运行时的 Virtualenv 中安装包含 tensorflowjs_converter 的 tensorflowjs Python 包。

     virtualenv -p $(pyenv root)/versions/3.6.8/bin/python --no-site-packages venv
     source venv/bin/activate
     pip install tensorflowjs
    
  3. 从 Keras HDF5 转换为 GraphModel。

     tensorflowjs_converter \
     --input_format=keras \
     --output_format=tfjs_graph_model \
     ./kerasmodel/fashion_mnist.h5 \
     ./graphmodel
    

选项 2:使用预构建的 Docker 镜像进行转换

在 Docker 容器中转换。

```
docker run --rm -v ${PWD}:/root/model \
tedhtchang/tensorflowjs_converter \
--input_format=keras \
--output_format=tfjs_graph_model \
/root/model/kerasmodel/fashion_mnist.h5 \
/root/model/graphmodel
```

验证转换

验证 graphmodel 文件夹是否包含一个 model.json 文件和已分片的二进制文件,例如 group1-shard1of1.bin

查找 saved_model_tags 和 signature_name

在转换过程中可能会出现一些常见的错误。您可能需要使用 --saved_model_tags--signature_name 选项来指定其他选项。

以下错误仅适用于 MetaGraphDef 的标记不是默认值 serve 的 TensorFlow SavedModel 转换。

RuntimeError: MetaGraphDef associated with tags 'serve' could not be found in SavedModel.To inspect available tag-sets in the SavedModel, please use the SavedModel CLI: `saved_model_cli`

使用 saved_model_cli 命令列出 --saved_model_tags 的可能值。此命令在安装 Python TensorFlow 包后可用。

saved_model_cli show --dir <model folder>

如果 TFHub 模块签名不是 default 或者 saved_model 签名不是 serving_default,可能会发生另一个转换错误。

ValueError: Signature 'default' is missing from meta graph.

要列出由 <tag name> 标记的可能签名,请运行以下命令。

saved_model_cli show --dir <model folder> --tag_set <tag name>

使用 TensorFlow.js 构建深度学习模型

在教程的本节中,您将学习如何使用 TensorFlow.js Layers API 构建深度学习机器学习模型。在模型构建流程中,我们完成了以下步骤:加载数据,定义模型,训练模型,以及测试模型。

收集、准备和创建数据集不在本教程探讨范围内。您可以改为使用 IBM Data Asset eXchange (DAX) 中现成的数据集。DAX 提供了一系列精心策划的免费、开放数据集。

您将使用的数据集是 DAX 中的 Fashion-MNIST 数据集。Fashion-MNIST 包含 10 种不同服装项目的 60,000 多幅图像的像素数据和标签。使用这些数据,您将构建并训练一个模型来识别服装项目。

加载数据

Fashion-MNIST 数据集包括两个 CSV 文件(一个训练集和一个测试集)。CSV 文件的第一列表示项目的标签。其余列(784 列)表示图像 (28×28) 的像素值(0 到 255)。

虽然数据集包含 10 种不同服装项目的图片,但您只需要处理一半的数据集(五种服装项目)。这允许您在下一节中使用您在这里构建的模型,并对数据集的后半部分进行一些迁移学习(而不必创建自己的新数据集来进行迁移学习)。

TensorFlow.js 提供了一个 Data API 来加载和解析数据。可以使用 tf.data.csv 来加载 CSV 数据。

  1. 启动一个新的 Node.js 项目(例如,tfjs-tutorial)并安装 tfjs-node 包:

     $ mkdir tfjs-tutorial
     $ cd tfjs-tutorial
     $ npm init -y
     $ npm install @tensorflow/tfjs-node
    
  2. 下载并解压 Fashion-MNIST 数据集。它应该包含两个 CSV 文件(fashion-mnist_train.csvfashion-mnist_test.csv)。

  3. 使用 VS Code 或您最喜欢的 IDE 在 tfjs-tutorial 项目中创建并打开一个 build-model.js 文件。

  4. 将以下代码添加到 build-model.js 文件中,并将 trainDataUrltestDataUrl 更新为已解压的数据文件的正确路径。这里,我们只是简单地初始化环境值。注意,有 10 个标签,但我们只使用五个类。

     // TensorFlow.js for Node,js
     const tf = require('@tensorflow/tfjs-node');
    
     // Fashion-MNIST training & test data
     const trainDataUrl = 'file://./fashion-mnist/fashion-mnist_train.csv';
     const testDataUrl = 'file://./fashion-mnist/fashion-mnist_test.csv';
    
     // mapping of Fashion-MNIST labels (i.e., T-shirt=0, Trouser=1, etc.)
     const labels = [
       'T-shirt/top',
       'Trouser',
       'Pullover',
       'Dress',
       'Coat',
       'Sandal',
       'Shirt',
       'Sneaker',
       'Bag',
       'Ankle boot'
     ];
    
     // Build, train a model with a subset of the data
     const numOfClasses = 5;
    
     const imageWidth = 28;
     const imageHeight = 28;
     const imageChannels = 1;
    
     const batchSize = 100;
     const epochsValue = 5;
    
  5. build-model.js 文件中添加用于加载数据集并在 0 到 1 之间归一化像素值(0 到 255)的代码。这是一种常见的做法,因为库中的数学函数通常对浮点张量进行运算。transform 函数将标签表示转换为独热码矢量,这也常用于类别分类。然后,我们选择属于本次练习所用类的一组图像,并将它们分组为批次以进行训练。

     // load and normalize data
     const loadData = function (dataUrl, batches=batchSize) {
       // normalize data values between 0-1
       const normalize = ({xs, ys}) => {
         return {
             xs: Object.values(xs).map(x => x / 255),
             ys: ys.label
         };
       };
    
       // transform input array (xs) to 3D tensor
       // binarize output label (ys)
       const transform = ({xs, ys}) => {
         // array of zeros
         const zeros = (new Array(numOfClasses)).fill(0);
    
         return {
             xs: tf.tensor(xs, [imageWidth, imageHeight, imageChannels]),
             ys: tf.tensor1d(zeros.map((z, i) => {
                 return i === ys ? 1 : 0;
             }))
         };
       };
    
       // load, normalize, transform, batch
       return tf.data
         .csv(dataUrl, {columnConfigs: {label: {isLabel: true}}})
         .map(normalize)
         .filter(f => f.ys < numOfClasses) // only use a subset of the data
         .map(transform)
         .batch(batchSize);
     };
    
     // run
     const run = async function () {
       const trainData = loadData(trainDataUrl);
    
       const arr = await trainData.take(1).toArray();
       arr[0].ys.print();
       arr[0].xs.print();
     };
    
     run();
    
  6. 运行应用。

     $ node build-model.js
    

运行代码时,会加载训练数据,将其归一化,并将其转换为张量。为第一组图像显示标签值 (xs) 和归一化像素值 (xs)。

构建模型

要构建的模型架构取决于您的用例和您正在使用的数据类型。对于我们在这个例子中用作输入的图像,卷积 (CNN) 已经被证明很适合从图像中提取有用特征,从而使模型能够学习。我们的模型架构很简单,只包括两层二维卷积,以及对每个层之后的最大池的计算。这些构成了模型中的四个隐藏层。然后,我们将张量扁平化,以放入一个致密的层中,进行最终的分类。您使用 TensorFlow.js Layers API 来构建层。

注意,第一层(输入)conv2d 需要一个 inputShape 以表明模型收到的输入的形状。最终层(输出)dense 包括 units 参数,以表明输出的维。还请注意,在层间没有张量和形状的声明,因为这都是由 Layers API 自动完成的。conv2d 层需要一些特定于卷积的参数,例如内核大小和形状。这一层中内置了激活函数。在最后一层 dense 中,softmax 激活函数生成图像的分类概率。

在定义了层后,必须使用优化器和损失函数对模型进行编译,以配置和准备用于训练和评估的模型。通过使用高级别 API,您可以看到用于实现模型的代码是如此简单明了、易于理解。

  1. 将以下代码添加到 build-model.js 文件中。

     // Define the model architecture
     const buildModel = function () {
       const model = tf.sequential();
    
       // add the model layers
       model.add(tf.layers.conv2d({
         inputShape: [imageWidth, imageHeight, imageChannels],
         filters: 8,
         kernelSize: 5,
         padding: 'same',
         activation: 'relu'
       }));
       model.add(tf.layers.maxPooling2d({
         poolSize: 2,
         strides: 2
       }));
       model.add(tf.layers.conv2d({
         filters: 16,
         kernelSize: 5,
         padding: 'same',
         activation: 'relu'
       }));
       model.add(tf.layers.maxPooling2d({
         poolSize: 3,
         strides: 3
       }));
       model.add(tf.layers.flatten());
       model.add(tf.layers.dense({
         units: numOfClasses,
         activation: 'softmax'
       }));
    
       // compile the model
       model.compile({
         optimizer: 'adam',
         loss: 'categoricalCrossentropy',
         metrics: ['accuracy']
       });
    
       return model;
     }
    
  2. 更新 run 代码。

     // run
     const run = async function () {
       const trainData = loadData(trainDataUrl);
       const model = buildModel();
       model.summary();
     };
    
  3. 运行应用。

     $ node build-model.js
    

此更改将构建模型并显示模型架构的摘要。

训练模型

在使用模型之前,需要进行训练。要训练模型,必须使用训练数据集拟合该模型。

  1. 使用训练代码更新 build-model.js 文件。

     // train the model against the training data
     const trainModel = async function (model, trainingData, epochs=epochsValue) {
       const options = {
         epochs: epochs,
         verbose: 0,
         callbacks: {
           onEpochBegin: async (epoch, logs) => {
             console.log(`Epoch ${epoch + 1} of ${epochs} ...`)
           },
           onEpochEnd: async (epoch, logs) => {
             console.log(`  train-set loss: ${logs.loss.toFixed(4)}`)
             console.log(`  train-set accuracy: ${logs.acc.toFixed(4)}`)
           }
         }
       };
    
       return await model.fitDataset(trainingData, options);
     };
    
  2. 更新 run 代码。

     // run
     const run = async function () {
       const trainData = loadData(trainDataUrl);
       const model = buildModel();
       model.summary();
       const info = await trainModel(model, trainData);
       console.log(info);
     };
    
  3. 运行应用。注意,训练可能需要几分钟。

     $ node build-model.js
    

运行此代码时,模型将使用训练数据完成训练,并按照定义的戳记次数进行迭代。每次迭代都会显示损失和准确性值。在每个戳记之后,应该看到准确度提高。您可以通过添加更多成对的 conv2dmaxPooling2d 层来试验模型架构,以查看准确性是否进一步提高。

评估模型

训练后,可以使用之前未出现的测试数据集来评估模型。测试数据集应以与训练数据集相同的方式处理。我们重新运行训练,然后在训练完成后通过模型运行测试数据集。

  1. 编辑 build-model.js 文件并添加评估代码。

     // verify the model against the test data
     const evaluateModel = async function (model, testingData) {
       const result = await model.evaluateDataset(testingData);
       const testLoss = result[0].dataSync()[0];
       const testAcc = result[1].dataSync()[0];
    
       console.log(`  test-set loss: ${testLoss.toFixed(4)}`);
       console.log(`  test-set accuracy: ${testAcc.toFixed(4)}`);
     };
    
  2. 更新 run 代码。

     // run
     const run = async function () {
       const trainData = loadData(trainDataUrl);
       const testData = loadData(testDataUrl);
    
       const model = buildModel();
       model.summary();
    
       const info = await trainModel(model, trainData);
       console.log(info);
    
       console.log('Evaluating model...');
       await evaluateModel(model, testData);
     };
    
  3. 运行应用。

     $ node build-model.js
    

此代码使用测试数据集对模型进行评估,并显示模型的损失和准确性值。通常,测试数据的准确性略低于训练数据。如果明显偏低,那么说明模型性能不佳,过度拟合可能是原因之一。

保存模型

此时,您可以开始使用您的模型进行预测。为此,必须首先导出/保存模型,以便以后可以在浏览器环境或单独的 Node.js 应用程序中加载并运行模型。也可以用于迁移学习。模型使用 tf.LayersModel.save 方法来保存。

  1. run 代码和 saveModelPath 更新为要保存模型的首选路径。

     // run
     const run = async function () {
       const trainData = loadData(trainDataUrl);
       const testData = loadData(testDataUrl);
       const saveModelPath = 'file://./fashion-mnist-tfjs';
    
       const model = buildModel();
       model.summary();
    
       const info = await trainModel(model, trainData);
       console.log(info);
    
       console.log('Evaluating model...');
       await evaluateModel(model, testData);
    
       console.log('Saving model...');
       await model.save(saveModelPath);
     };
    
  2. 运行应用。

     $ node build-model.js
    

模型保存在当前工作目录中的 fashion-mnist-tfjs 文件夹中。保存的内容包括模型的拓扑 (model.json) 和权重 (weights.bin)。

您可以在 /src/build-model.js 文件中找到用于构建、训练和保存模型的完整 Node.js 应用程序。

注意:请随意试验不同的模型架构,并尝试改进或创建性能更高的模型。您还可以在训练期间增加戳记数量,或者尝试不同的激活函数和优化器。

运行模型

您保存的模型现在可以加载并用于对服装项目的图像进行预测。

切记,我们使用的 Fashion-MNIST 数据集派生自 28×28 灰度图像。您要在模型中运行的任何图像都必须转换为 28×28 灰度图像。我们使用 jimp 库来帮助完成必要的图像处理。

  1. jimp 库安装到 Node.js 项目中。

     $ npm install --save jimp
    
  2. 在项目中创建并打开 test-model.js 文件。

  3. test-model.js 文件中添加初始化代码。

     // TensorFlow.js for Node,js
     const tf = require('@tensorflow/tfjs-node');
    
     // mapping of Fashion-MNIST labels
     const labels = [
       'T-shirt/top',
       'Trouser',
       'Pullover',
       'Dress',
       'Coat',
       'Sandal',
       'Shirt',
       'Sneaker',
       'Bag',
       'Ankle boot'
     ];
    
     const imageWidth = 28;
     const imageHeight = 28;
     const imageChannels = 1;
    
  4. test-model.js 文件中添加用于将图像转换为所需格式的代码。

     const Jimp = require('jimp');
    
     // Convert image to array of normalized pixel values
     const toPixelData = async function (imgPath) {
       const pixeldata = [];
       const image = await Jimp.read(imgPath);
       await image
           .resize(imageWidth, imageHeight)
           .greyscale()
           .invert()
           .scan(0, 0, imageWidth, imageHeight, (x, y, idx) => {
             let v = image.bitmap.data[idx + 0];
             pixeldata.push(v / 255);
           });
    
       return pixeldata;
     };
    
  5. 更新 test-model.js 文件以添加用于执行预测的代码。

     const runPrediction = function (model, imagepath) {
       return toPixelData(imagepath).then(pixeldata => {
         const imageTensor = tf.tensor(pixeldata, [imageWidth, imageHeight, imageChannels]);
         const inputTensor = imageTensor.expandDims();
         const prediction = model.predict(inputTensor);
         const scores = prediction.arraySync()[0];
    
         const maxScore = prediction.max().arraySync();
         const maxScoreIndex = scores.indexOf(maxScore);
    
         const labelScores = {};
    
         scores.forEach((s, i) => {
             labelScores[labels[i]] = parseFloat(s.toFixed(4));
         });
    
         return {
             prediction: `${labels[maxScoreIndex]} (${parseInt(maxScore * 100)}%)`,
             scores: labelScores
         };
       });
     };
    
  6. 添加用于运行应用并将 modelUrl 更新为所保存模型的正确路径的代码。

     // run
     const run = async function () {
       if (process.argv.length < 3) {
         console.log('please pass an image to process. ex:');
         console.log('  node test-model.js /path/to/image.jpg');
       } else {
         // e.g., /path/to/image.jpg
         const imgPath = process.argv[2];
    
         const modelUrl = 'file://./fashion-mnist-tfjs/model.json';
    
         console.log('Loading model...');
         const model = await tf.loadLayersModel(modelUrl);
         model.summary();
    
         console.log('Running prediction...');
         const prediction = await runPrediction(model, imgPath);
         console.log(prediction);
       }
     };
    
     run();
    
  7. 运行应用,并传入图像的完整路径。

     $ node test-model.js dress-red.jpg
    

运行此代码,将处理图像并进行预测。预测输出是一个 JSON 对象,其中包含每个可用标签的预测和分数。例如:

{
  "prediction": "Dress (62%)",
  "scores": {
    "T-shirt/top": 0.0018,
    "Trouser": 0.0106,
    "Pullover": 0.0133,
    "Dress": 0.6229,
    "Coat": 0.3427
  }
}

您可以在 /src/test-model.js 文件中找到用于加载和测试模型的完整 Node.js 应用程序。

使用 TensorFlow.js 迁移学习

我们来练习一种常用的技巧,将已经训练过的模型用于我们自己的特定用例。最先进的现代模型通常有数以百万计的参数,并且需要超乎寻常的时间来进行充分的训练。迁移学习通过采用针对一个任务进行了训练的模型重新利用于另一个相关任务,从而大大减少了这种训练工作。要实现这一点,我们采用的方法是用新层替换预训练模型的最后层,然后用新的数据来进行训练。这种技巧的一大优点是,为新类训练有效模型所需的训练数据量会大大减少。

切记,要使这种方法有效,从第一个任务中学习到的模型特性应该有普遍性,也就是说,第一个任务和第二个任务之间的特性应该是相似的。

在上一节中,我们通过 Fashion MNIST 数据集创建了针对五个类进行训练的模型。我们来尝试使用迁移学习为其他五个类制作一个分类器。但是,这一次我们只对每个类使用训练数据集的一小部分。我们训练分类器的速度应该会快得多。

设置

首先,将 build-model.js 文件内容复制到另一个文件中,您将在后一个文件中调整内容以进行迁移学习。

cp build-model.js transfer-learn.js

进行数据加载调整

可以保持几乎相同的加载方式,只需少数几项调整。

  1. loadData 的返回语句中更改 filter 函数。我们想要使用数据集中以前没有训练过的另一部分,所以让我们将表达式更改为只获取标签大于或等于 labels.length - numOfClasses 所给出的截止值的数据。

     return tf.data
       .csv(dataUrl, {columnConfigs: {label: {isLabel: true}}})
       .map(normalize)
       .filter(f => f.ys >= (labels.length - numOfClasses)) // Note the change here.
       .map(transform)
       .batch(batchSize);
    
  2. 更改 loadData 中的 transform 函数,以准确地将最后五个标签映射到独热码矢量。例如,“Sandal” 的标签号是 5,因此我们从中减去类的数量(在我们的例子中是 5),即可获得独热编码数组(即 [1, 0, 0, 0, 0])中的“热”索引。

     const transform = ({xs, ys}) => {
       const zeros = (new Array(numOfClasses)).fill(0);
    
       return {
         xs: tf.tensor(xs, [imageWidth, imageHeight, imageChannels]),
         ys: tf.tensor1d(zeros.map((z, i) => {
           // Note the change from ys to (ys - numOfClasses)
           return i === (ys - numOfClasses) ? 1 : 0;
         }))
       };
     };
    

更改模型构建函数

因为我们不再从头开始构建模型,而是要依赖于以前构建的模型,所以我们要更改 buildModel 函数。这一次,buildModel 需要一个自变量用于基本模型(也就是之前的预训练模型)。我们把它作为新模型的基础。

const buildModel = function (baseModel) {

  // Remove the last layer of the base model.This is the softmax
  // classification layer used for classifying the first  classes
  // of Fashion-MNIST.This leaves us with the 'Flatten' layer as the
  // new final layer.
  baseModel.layers.pop();

  // Freeze the weights in the base model layers (feature layers) so they
  // don't change when we train the new model.
  for (layer of baseModel.layers) {
    layer.trainable = false;
  }

  // Create a new sequential model starting from the layers of the
  // previous model.
  const model = tf.sequential({
    layers: baseModel.layers
  });

  // Add a new softmax dense layer.This layer will have the trainable
  // parameters for classifying our new classes.
  model.add(tf.layers.dense({
    units: numOfClasses,
    activation: 'softmax',
    name: 'topSoftmax'
  }));

  model.compile({
    optimizer: 'adam',
      loss: 'categoricalCrossentropy',
      metrics: ['accuracy']
  });

  return model;
}

更新 run 代码

更新 run 代码以加载之前的预训练模型,并将其用于新模型。我们还将只对数据集的一个子集进行训练。在这种情况下,我们只对一组新类的可用训练图像中的 10%(每个类大约 600 张图像,原始图像是 6000 张)进行训练。

const run = async function () {

  const trainData = loadData(trainDataUrl);
  const testData = loadData(testDataUrl);

  // Determine how many batches to take for reduced training set.
  const amount = Math.floor(3000 / batchSize);
  const trainDataSubset = trainData.take(amount);

  const baseModelUrl = 'file://./fashion-mnist-tfjs/model.json';
  const saveModelPath = 'file://./fashion-mnist-tfjs-transfer';

  const baseModel =  await tf.loadLayersModel(baseModelUrl);
  const newModel = buildModel(baseModel);
  newModel.summary();

  const info = await trainModel(newModel, trainDataSubset);
  console.log(info);

  console.log('Evaluating model...');
  await evaluateModel(newModel, testData);

  console.log('Saving model...');
  await newModel.save(saveModelPath);
}

run();

训练模型

从命令行运行应用,以开始使用迁移学习来训练新模型。

$ node transfer-learn.js

希望您能看到训练完成得快得多,并且测试的准确性与我们从头开始使用前五个类训练模型的准确性相似。这是因为我们不仅对更少的数据进行训练,而且只对最后一层进行训练。最后一层之前的所有层都保持不变。

您可以在 /src/transfer-learn.js 文件中找到用于加载预训练的模型和执行迁移学习的完整 Node.js 应用程序。

测试新模型

让我们在表示其中一个新类的项目的图像上尝试新模型。

  1. run 代码中,更改 test-model.js 脚本以使用新的模型文件。

     const modelUrl = 'file://./fashion-mnist-tfjs-transfer/model.json';
    
  2. 注释掉或删除 labels 数组中的前五个类,以便 softmax 输出索引具有正确的映射。这应该类似于以下代码。

     const labels = [
       'Sandal',
       'Shirt',
       'Sneaker',
       'Bag',
       'Ankle boot'
     ];
    
  3. 运行应用,并传入图像的完整路径。

     $ node test-model.js shirt-blue.jpg
    

请注意,新模型现在只能识别出它在迁移学习过程中训练识别的五个新类,而不是原来的五个类。我们所做的就是使用已经对前五个类的特征进行了训练的权重,并替换和重新训练最后一层来对后五个类进行分类。作为试验,您可以尝试重新配置最后一个层来识别所有的 10 个类。

在本次练习中,我们对小数据集进行了划分以说明编程概念,因此准确性可能不会很高。在实践中,您会希望拥有足够大的数据集,并且其中应该包含您在迁移学习数据中可能期望的特征。

结束语

在本教程中,我们深入研究了如何用 JavaScript 对深度学习模型进行编程。我们从高级别概念开始,并讨论了可供选择的编程 API。然后,我们进行了两个编程练习,它们代表了构建和使用模型时的常见做法。我们还描述了如何将使用 Python 实现的模型转换为 JavaScript 格式。

构建深度学习模型确实需要相当程度的本领域专业知识。幸运的是,深度学习得以迅速采用,让这种专业知识更加普及,使更多的开发者能够构建自己的模型,而不是依赖于预定义的模型。如果您有兴趣了解该领域的专业知识,有包括 IBM Developer 站点上的学习路径在内的许多资源可供学习。

在本系列的下一部分中,我们将讨论如何运行 JavaScript AI 应用程序,同时考虑性能和 IoT 设备。

视频

本文翻译自:Coding a deep learning model using TensorFlow.js(2020-04-17)