JavaScript 深度学习(二)(3)

简介: JavaScript 深度学习(二)

JavaScript 深度学习(二)(2)https://developer.aliyun.com/article/1516954

因此,我们可以将 MLP 建立在 MobileNet 的特征提取层的顶部。即使在这种情况下,特征提取器(截断的 MobileNet)和 MLP 都是两个分离的模型(见图 5.8)。由于这种两个模型的设置,不可能直接使用图像张量(形状为[numExamples,224,224,3])来训练新的 MLP。相反,新的 MLP 必须在图像的嵌入上进行训练——即截断的 MobileNet 的输出。幸运的是,我们已经收集了那些嵌入张量(清单 5.4)。我们只需要在嵌入张量上调用其 fit() 方法即可训练新的 MLP。在 index.js 的 train() 函数中执行此操作的代码十分简单,我们不再详细介绍。

图 5.8. Webcam-transfer-learning 示例背后的迁移学习算法的概要


一旦迁移学习完成,截断模型和新头将一起用于从网络摄像头的输入图像获取概率分数。您可以在 index.js 的 predict() 函数中找到代码,显示在 列表 5.6 中。特别是,涉及两个 predict() 调用。第一个调用将图像张量转换为其嵌入,使用截断的 MobileNet;第二个使用与迁移学习训练的新头将嵌入转换为四个方向的概率分数。列表 5.6 中的随后代码获取获胜索引(在四个方向的最大概率分数中对应的索引)并使用它来控制 Pac-Man 并更新 UI 状态。与之前的示例一样,我们不涵盖示例的 UI 部分,因为它不是机器学习算法的核心。您可以使用下一个列表中的代码自行研究和玩耍 UI 代码。

列表 5.6. 在迁移学习后从网络摄像头输入图像获取预测
async function predict() {
  ui.isPredicting();
  while (isPredicting) {
    const predictedClass = tf.tidy(() => {
      const img = webcam.capture();                         ***1***
      const embedding = truncatedMobileNet.predict(         ***2***
          img);                                             ***2***
      const predictions = model.predict(activation);        ***3***
      return predictions.as1D().argMax();                   ***4***
    });
    const classId = (await predictedClass.data())[0];       ***5***
    predictedClass.dispose();
    ui.predictClass(classId);                               ***6***
    await tf.nextFrame();
  }
  ui.donePredicting();
}
  • 1 从网络摄像头捕获一帧
  • 2 从截断的 MobileNet 获取嵌入
  • 3 使用新头模型将嵌入转换为四个方向的概率分数
  • 4 获取最大概率分数的索引
  • 5 将索引从 GPU 下载到 CPU
  • 6 根据获胜方向更新 UI:控制 Pac-Man 并更新其他 UI 状态,如控制器上相应“按钮”的突出显示

这结束了我们讨论与迁移学习算法相关的 webcam-transfer-learning 示例的部分。这个示例中我们使用的方法的一个有趣之处是训练和推断过程涉及两个独立的模型对象。这对我们的教育目的来说是有好处的,因为它说明了如何从预训练模型的中间层获取嵌入。这种方法的另一个优点是它暴露了嵌入,并且使得应用直接使用这些嵌入的机器学习技术更容易。这种技术的一个例子是k 最近邻(kNN,在信息框 5.2 中讨论)。然而,直接暴露嵌入也可能被视为以下原因的缺点:

  • 这导致稍微复杂一些的代码。例如,推断需要两个 predict() 调用才能对单个图像执行推断。
  • 假设我们希望保存模型以供以后会话使用或转换为非 TensorFlow.js 库。那么截断模型和新的头模型需要分别保存,作为两个单独的构件。
  • 在一些特殊情况下,迁移学习将涉及基础模型的某些部分的反向传播(例如截断的 MobileNet 的前几层)。当基础和头部是两个分开的对象时,这是不可能的。

在接下来的部分中,我们将展示一种通过形成单个模型对象来克服这些限制的方法进行迁移学习。这将是一个端到端模型,因为它可以将原始格式的输入数据转换为最终的期望输出。

基于嵌入的 k 最近邻分类

在机器学习中,解决分类问题的非神经网络方法有很多。其中最著名的之一就是 k 最近邻(kNN)算法。与神经网络不同,kNN 算法不涉及训练步骤,更容易理解。

我们可以用几句话来描述 kNN 分类的工作原理:

  1. 你选择一个正整数k(例如,3)。
  2. 你收集一些带有真实类别标签的参考示例。通常收集的参考示例数量至少是k的几倍。每个示例都被表示为一系列实值数字,或者一个向量。这一步类似于神经网络方法中的训练示例的收集。
  3. 为了预测新输入的类别,你计算新输入的向量表示与所有参考示例的距离。然后对距离进行排序。通过这样做,你可以找到在向量空间中距离输入最近的k个参考示例。这些被称为输入的“k个最近邻居”(算法的名字来源)。
  4. 你观察k个最近邻居的类别,并使用它们中最常见的类别作为输入的预测。换句话说,你让k个最近的邻居“投票”来预测类别。

这个算法的一个示例如下图所示。

在二维嵌入空间中的 kNN 分类示例。在这种情况下,k=3,有两个类别(三角形和圆形)。三角形类别有五个参考示例,圆形类别有七个。输入示例表示为一个正方形。与输入相连的三个最近邻居由连线表示。因为三个最近邻居中有两个是圆形,所以输入示例的预测类别将是圆形。

正如您从前面的描述中可以看到的,kNN 算法的一个关键要求是,每个输入示例都表示为一个向量。像我们从截断的 MobileNet 获取的那样的嵌入是这样的向量表示的良好候选者,原因有两个。首先,与原始输入相比,它们通常具有较低的维度,因此减少了距离计算所需的存储和计算量。其次,由于它们已经在大型分类数据集上进行了训练,所以这些嵌入通常捕捉到输入中的更重要的特征(例如图像中的重要几何特征;参见图 4.5),并忽略了不太重要的特征(例如亮度和大小)。在某些情况下,嵌入给我们提供了原本不以数字形式表示的事物的向量表示(例如第九章中的单词嵌入)。

与神经网络方法相比,kNN 不需要任何训练。在参考样本数量不太多且输入维度不太高的情况下,使用 kNN 可以比训练神经网络并对其进行推断的计算效率更高。

然而,kNN 推断不随数据量的增加而扩展。特别是,给定 N 个参考示例,kNN 分类器必须计算 N 个距离,以便为每个输入进行预测。当 N 变大时,计算量可能变得难以处理。相比之下,神经网络的推断不随训练数据的量而变化。一旦网络被训练,训练数据的数量就不重要了。网络正向传播所需的计算量仅取决于网络的拓扑结构。

^a

但是,请查看研究努力设计近似 kNN 算法但运行速度更快且规模比 kNN 更好的算法:Gal Yona,“利用局部敏感哈希进行快速近似重复图像搜索”,Towards Data Science,2018 年 5 月 5 日,mng.bz/1wm1

如果您有兴趣在您的应用程序中使用 kNN,请查看基于 TensorFlow.js 构建的 WebGL 加速 kNN 库:mng.bz/2Jp8

5.1.3. 通过微调充分利用迁移学习:音频示例

在前几节中,迁移学习的示例处理了视觉输入。在这个例子中,我们将展示迁移学习也适用于表示为频谱图像的音频数据。回想一下,我们在第 4.4 节中介绍了用于识别语音命令(孤立的、短的口头单词)的卷积网络。我们构建的语音命令识别器只能识别 18 个不同的单词(如“one”、“two”、“up” 和 “down”)。如果你想为其他单词训练一个识别器呢?也许你的特定应用程序需要用户说特定的单词,比如“red” 或 “blue”,甚至是用户自己选的单词;或者你的应用程序面向的是讲英语以外语言的用户。这是迁移学习的一个经典例子:在手头数据量很少的情况下,你可以尝试从头开始训练一个模型,但使用预训练模型作为基础可以在更短的时间内和更少的计算资源下获得更高的准确度。

如何在语音命令示例应用中进行迁移学习

在我们描述如何在这个示例中进行迁移学习之前,最好让你熟悉如何通过 UI 使用迁移学习功能。要使用 UI,请确保您的计算机连接了音频输入设备(麦克风),并且在系统设置中将音频输入音量设置为非零值。要下载演示代码并运行它,请执行以下操作(与第 4.4.1 节相同的过程):

git clone https://github.com/tensorflow/tfjs-models.git
cd tfjs-models/speech-commands
yarn && yarn publish-local
cd demo
yarn && yarn link-local && yarn watch

当 UI 启动时,请允许浏览器访问麦克风的请求。图 5.9 显示了演示的示例截图。当演示页面启动时,将自动从互联网上加载预训练的语音命令模型,使用指向 HTTPS URL 的 tf.loadLayersModel() 方法。模型加载完成后,“开始” 和 “输入转移词” 按钮将被启用。如果点击 “开始” 按钮,演示将进入推理模式,连续检测屏幕上显示的 18 个基本单词。每次检测到一个单词时,屏幕上相应的单词框将点亮。但是,如果点击 “输入转移词” 按钮,屏幕上将会出现一些额外的按钮。这些按钮是从右侧的文本输入框中的逗号分隔的单词创建的。默认单词是 “noise”、“red” 和 “green”。这些是转移学习模型将被训练识别的单词。但是,如果你想为其他单词训练转移模型,可以自由修改输入框的内容,只要保留 “noise” 项即可。“noise” 项是特殊的一个,你应该收集背景噪声样本,即没有任何语音声音的样本。这允许转移模型区分语音和静音(背景噪声)的时刻。当你点击这些按钮时,演示将从麦克风记录 1 秒的音频片段,并在按钮旁边显示其频谱图。单词按钮中的数字跟踪到目前为止已经收集到的特定单词的示例数量。

图 5.9. 语音命令示例的转移学习功能的示例截图。在这里,用户已经为转移学习输入了一组自定义单词:“feel”、“seal”、“veal” 和 “zeal”,以及始终需要的 “noise” 项。此外,用户已经收集了每个单词和噪声类别的 20 个示例。

如同机器学习问题中的一般情况,你能够收集的数据越多(在可用时间和资源允许的范围内),训练出的模型就会越好。示例应用程序至少需要每个单词的八个示例。如果你不想或无法自己收集声音样本,可以从mng.bz/POGY(文件大小:9 MB)下载预先收集好的数据集,并在 UI 的数据集 IO 部分使用上传按钮上传。

数据集准备好后,通过文件上传或你自己的样本收集,“开始迁移学习” 按钮将变为可用状态。你可以点击该按钮启动迁移模型的训练。该应用在你收集的音频频谱图上执行 3:1 的分割,随机选择其中 75%用于训练,剩余的 25%用于验证。应用程序在迁移学习过程中显示训练集损失和准确度值以及验证集值。一旦训练完成,可以点击 “开始” 按钮,让演示程序连续识别迁移词,此时你可以经验性地评估迁移模型的准确度。

这也是为什么演示要求你每个单词至少收集八个样本的原因。如果单词更少,在验证集中每个单词的样本数量将很少,可能会导致不可靠的损失和准确度估计。

你应该尝试不同的词汇组合,观察它们在经过迁移学习后对精确度的影响。默认集合中,“red”和“green”这两个词在音位内容方面非常不同。例如,它们的起始辅音是两个非常不同的声音,“r”和“g”。它们的元音也听起来非常不同(“e”和“ee”);结尾辅音也很不同(“d”和“n”)。因此,只要每个单词收集的样本数量不太小(例如>=8),使用的时代数不太小(这会导致欠拟合)或太大(这会导致过拟合;请参阅第八章),你就能够在迁移训练结束时获得几乎完美的验证精度。

为使模型的迁移学习任务更具挑战性,使用由 1)更具混淆性的单词和 2)更大的词汇组成的集合。这就是我们在图 5.9 的屏幕截图中所做的。在该截图中,使用了四个听起来相似的单词:“feel”、“seal”、“veal”和“zeal”。这些单词的元音和结尾辅音相同,开头的辅音也相似。它们甚至可能会让一个不注意或在坏电话线路上听的人听起来混淆。从图的右下角的准确度曲线可以看出,模型要达到 90%以上的准确度并不是一件容易的事,必须通过额外的“微调”阶段来补充初始的迁移学习 - 这是一种迁移学习技巧。

深入了解迁移学习中的微调

微调是一种技术,它可以帮助您达到仅通过训练迁移模型的新头部无法达到的准确度水平。如果您希望了解微调的工作原理,本节将更详细地解释。您需要消化一些技术细节。但通过它,您将深入理解迁移学习及其相关的 TensorFlow.js 实现,这将是值得的努力。

构建单个迁移学习模型

首先,我们需要了解语音迁移学习应用程序如何为迁移学习创建模型。列表 5.7(来自 speech-commands/src/browser_ fft_recognizer.ts 的代码)中的代码从基础语音命令模型(您在 第 4.4.1 节中学到的模型)创建一个模型。它首先找到模型的倒数第二个(倒数第二个)密集层,并获取其输出符号张量(代码中的truncatedBaseOutput)。然后,它创建一个仅包含一个密集层的新头模型。这个新头的输入形状与truncatedBaseOutput符号张量的形状匹配,其输出形状与迁移数据集中的单词数匹配(在图 5.9 的情况下为五个)。密集层配置为使用 softmax 激活,适用于多类别分类任务。(请注意,与书中大多数其他代码清单不同,以下代码是用 TypeScript 编写的。如果您不熟悉 TypeScript,可以简单地忽略类型标记,例如voidtf.SymbolicTensor`。)

列表 5.7. 将迁移学习模型创建为单个tf.Model对象^([9])

关于此代码列表的两点说明:1)代码是用 TypeScript 编写的,因为它是可重用的 @tensorflow-models/speech-commands 库的一部分。2)出于简化的目的,此代码中删除了一些错误检查代码。

private createTransferModelFromBaseModel(): void {
    const layers = this.baseModel.layers;
    let layerIndex = layers.length - 2;
    while (layerIndex >= 0) {                                              ***1***
      if (layers[layerIndex].getClassName().toLowerCase() === 'dense') {   ***1***
        break;                                                             ***1***
      }                                                                    ***1***
      layerIndex--;                                                        ***1***
    }                                                                      ***1***
    if (layerIndex < 0) {
      throw new Error('Cannot find a hidden dense layer in the base model.');
    }
    this.secondLastBaseDenseLayer =                                        ***2***
        layers[layerIndex];                                                ***2***
    const truncatedBaseOutput = layers[layerIndex].output as               ***3***
        tf.SymbolicTensor;                                                 ***3***
    this.transferHead = tf.layers.dense({                                  ***4***
      units: this.words.length,                                            ***4***
      activation: 'softmax',                                               ***4***
      inputShape: truncatedBaseOutput.shape.slice(1)                       ***4***
    }));                                                                   ***4***
    const transferOutput =                                                 ***5***
        this.transferHead.apply(truncatedBaseOutput) as tf.SymbolicTensor; ***5***
    this.model =                                                           ***6***
        tf.model({inputs: this.baseModel.inputs, outputs: transferOutput});***6***
  }
  • 1 找到基础模型的倒数第二个密集层
  • 2 获取稍后在微调过程中将解冻的层(请参阅列表 5.8)
  • 3 找到符号张量
  • 4 创建模型的新头
  • 5 在截断的基础模型输出上“应用”新的头部,以获取新模型的最终输出作为符号张量。
  • 6 使用tf.model() API 创建一个新的用于迁移学习的模型,指定原始模型的输入作为其输入,新的符号张量作为输出。

新的头部以一种新颖的方式使用:其apply()方法使用截断的基础输出符号张量作为输入参数进行调用。apply()是 TensorFlow.js 中每个层和模型对象上都可用的方法。apply()方法的作用是什么?顾名思义,它“应用”新的头模型于输入,并给出输出。要认识到的重要事项如下:

  • 输入和输出都是符号化的——它们是具体张量值的占位符。
  • 图 5.10 给出了一个图形示例:符号输入(truncatedBaseOutput)不是一个孤立的实体;而是基模型倒数第二个密集层的输出。该密集层从另一层接收输入,该层又从其上游层接收输入,依此类推。因此,truncatedBaseOutput携带着基模型的一个子图,即基模型的输入到倒数第二个密集层的输出之间的子图。换句话说,它是基模型的整个图,减去倒数第二个密集层之后的部分。因此,apply()调用的输出包含该子图以及新的密集层。输出和原始输入在调用tf.model()函数时共同使用,得到一个新模型。这个新模型与基模型相同,只是其头部被新的密集层替换了(参见图 5.10 的底部部分)。
图 5.10. 示出了创建迁移学习的新端到端模型的方式的示意图。在阅读此图时,请参考 list 5.7。与 list 5.7 中的变量对应的图的某些部分用固定宽度字体标记。步骤 1:获取原始模型倒数第二个密集层的输出符号张量(由粗箭头指示)。它将在步骤 3 中被使用。步骤 2:创建新的头模型,包含一个单输出的密集层(标记为“dense 3”)。步骤 3:使用步骤 1 中的符号张量作为输入参数调用新头模型的apply()方法。此调用将该输入与步骤 1 中的截断的基模型连接起来。步骤 4:将apply()调用的返回值与原始模型的输入符号张量一起在调用tf.model()函数时使用。此调用返回一个新模型,其中包含了原始模型的所有层,从第一层到倒数第二个密集层,以及新头的密集层。实际上,这将原始模型的旧头和新头交换,为后续在迁移数据上训练做准备。请注意,为了简化可视化效果,图中省略了实际语音命令模型的一些(七个)层。在此图中,有颜色的层是可训练的,而白色的层是不可训练的。

请注意,这里的方法与我们在 5.1.2 节 中如何融合模型的方法不同。在那里,我们创建了一个被截断的基础模型和一个新的头模型作为两个独立的模型实例。因此,对每个输入示例进行推断涉及两个 predict() 调用。在这里,新模型期望的输入与基础模型期望的音频频谱张量相同。同时,新模型直接输出新单词的概率分数。每次推断仅需一个 predict() 调用,因此是一个更加流畅的过程。通过将所有层封装在单个模型中,我们的新方法在我们的应用中具有一个额外的重要优势:它允许我们通过参与识别新单词的任何层执行反向传播。这使我们能够执行微调技巧。这是我们将在下一节中探讨的内容。

通过解冻层进行微调

微调是转移学习的可选步骤,紧随模型训练的初始阶段。在初始阶段,来自基础模型的所有层都被冻结(它们的trainable属性设置为false),权重更新仅发生在头部层。我们在本章前面的 mnist-transfer-cnn 和 webcam-transfer-learning 示例中已经看到了这种初始训练类型。在微调期间,基础模型的一些层被解冻(它们的trainable属性设置为true),然后模型再次在转移数据上进行训练。这种层解冻在 图 5.11 中以示意图显示。代码在 TensorFlow.js 中显示了如何为语音命令示例执行此操作,详见 清单 5.8(来自 speech-commands/src/browser_fft_recognizer.ts)。

图 5.11。展示了转移学习初始阶段(面板 A)和微调阶段(面板 B)期间冻结和未冻结(即可训练)层的示意图,代码见 清单 5.8。注意 dense1 紧随 dense3 之后的原因是,dense2(基础模型的原始输出)已被截断为转移学习的第一步(参见 图 5.10)。


清单 5.8。初始转移学习,然后进行微调^([10])

¹⁰

一些错误检查代码已被删除,以便集中关注算法的关键部分。

async train(config?: TransferLearnConfig):
      Promise<tf.History|[tf.History, tf.History]> {
    if (config == null) {
      config = {};
    }
    if (this.model == null) {
      this.createTransferModelFromBaseModel();
    }
    this.secondLastBaseDenseLayer.trainable = false;                       ***1***
    this.model.compile({                                                   ***2***
      loss: 'categoricalCrossentropy',                                     ***2***
      optimizer: config.optimizer || 'sgd',                                ***2***
      metrics: ['acc']                                                     ***2***
    });                                                                    ***2***
    const {xs, ys} = this.collectTransferDataAsTensors();
    let trainXs: tf.Tensor;
    let trainYs: tf.Tensor;
    let valData: [tf.Tensor, tf.Tensor];
    try {
      if (config.validationSplit != null) {
        const splits = balancedTrainValSplit(                              ***3***
            xs, ys, config.validationSplit);                               ***3***
        trainXs = splits.trainXs;
        trainYs = splits.trainYs;
        valData = [splits.valXs, splits.valYs];
      } else {
        trainXs = xs;
        trainYs = ys;
      }
      const history = await this.model.fit(trainXs, trainYs, {             ***4***
        epochs: config.epochs == null ? 20 : config.epochs,                ***4***
        validationData: valData,                                           ***4***
        batchSize: config.batchSize,                                       ***4***
        callbacks: config.callback == null ? null : [config.callback]      ***4***
      });                                                                  ***4***
      if (config.fineTuningEpochs != null && config.fineTuningEpochs > 0) {***5***
        this.secondLastBaseDenseLayer.trainable =                          ***5***
            true;
        const fineTuningOptimizer: string|tf.Optimizer =
            config.fineTuningOptimizer == null ? 'sgd' :
                                                 config.fineTuningOptimizer;
        this.model.compile({                                               ***6***
          loss: 'categoricalCrossentropy',                                 ***6***
          optimizer: fineTuningOptimizer,                                  ***6***
          metrics: ['acc']                                                 ***6***
        });                                                                ***6***
        const fineTuningHistory = await this.model.fit(trainXs, trainYs, { ***7***
          epochs: config.fineTuningEpochs,                                 ***7***
          validationData: valData,                                         ***7***
          batchSize: config.batchSize,                                     ***7***
          callbacks: config.fineTuningCallback == null ?                   ***7***
              null :                                                       ***7***
              [config.fineTuningCallback]                                  ***7***
        });                                                                ***7***
        return [history, fineTuningHistory];
      } else {
        return history;
      }
    } finally {
      tf.dispose([xs, ys, trainXs, trainYs, valData]);
    }
  }
  • 1 确保所有截断基础模型的层,包括稍后将进行微调的层,在转移训练的初始阶段都被冻结
  • 2 为初始转移训练编译模型
  • 3 如果需要 validationSplit,则以平衡的方式将转移数据分割为训练集和验证集
  • 4 调用 Model.fit() 进行初始转移训练
  • 5 对于微调,解冻基础模型的倒数第二个密集层(截断基础模型的最后一层)
  • 6 在解冻层之后重新编译模型(否则解冻不会生效)
  • 7 调用 Model.fit() 进行微调

有几个关于 列表 5.8 中代码需要指出的重要事项:

  • 每次您通过更改它们的 trainable 属性来冻结或解冻任何层时,都需要再次调用模型的 compile() 方法,以使更改生效。我们已经在 第 5.1.1 节 中讨论了这一点,当我们谈到 MNIST 迁移学习示例时。
  • 我们保留了训练数据的一部分用于验证。这样做可以确保我们观察的损失和准确率反映了模型在反向传播期间没有见过的输入上的表现。然而,我们为验证而从收集的数据中拆分出一部分的方式与以前不同,并且值得注意。在 MNIST 卷积网络示例(列表 4.2 在 第四章)中,我们使用了 validationSplit 参数,让 Model.fit() 保留最后的 15–20% 的数据用于验证。但是,在这里使用相同的方法效果不佳。为什么?因为与早期示例中的数据量相比,我们这里的训练集要小得多。因此,盲目地将最后几个示例拆分为验证可能会导致一些词在验证子集中表示不足。例如,假设您为“feel”、“seal”、“veal” 和 “zeal” 中的每个词收集了八个示例,并选择最后的 32 个样本(8 个示例)的 25% 作为验证。那么,平均而言,验证子集中每个单词只有两个示例。由于随机性,一些单词可能最终只在验证子集中有一个示例,而其他单词可能根本没有示例!显然,如果验证集缺少某些单词,它将不是用于测量模型准确性的很好的集合。这就是为什么我们使用一个自定义函数(balancedTrainValSplit 在 列表 5.8)。此函数考虑了示例的真实单词标签,并确保所有不同的单词在训练和验证子集中都得到公平的表示。如果您有一个涉及类似小数据集的迁移学习应用程序,那么做同样的事情是个好主意。

那么,微调为我们做了什么呢?在迁移学习的初始阶段之上,微调提供了什么附加价值?为了说明这一点,我们将面板 A 中初始阶段和微调阶段的损失和准确率曲线连续绘制在一起,如图 5.12 所示。这里涉及的迁移数据集包含了我们在图 5.9 中看到的相同的四个单词。每条曲线的前 100 个纪元对应于初始阶段,而最后的 300 个纪元对应于微调。你可以看到,在初始训练的前 100 个纪元结束时,损失和准确率曲线开始变平并开始进入递减回报的区域。在验证子集的准确率在约 84% 时达到平稳状态。(请注意,仅查看 训练子集 的准确率曲线是多么具有误导性,因为它很容易接近 100%。)然而,解冻基础模型中的密集层,重新编译模型,并开始微调训练阶段,验证准确率就不再停滞,可以提高到 90–92%,这是一个非常可观的准确率增加 6–8 个百分点。验证损失曲线也可以看到类似的效果。

图 5.12. 面板 A:迁移学习和随后微调(图例中标为 FT)的示例损失和准确率曲线。注意曲线初始部分和微调部分之间的拐点。微调加速了损失的减少和准确率的提高,这是由于基础模型的顶部几层解冻以及模型容量的增加,以及向迁移学习数据中的独特特征的调整所致。面板 B:在不进行微调的情况下训练迁移模型相同数量的纪元(400 纪元)的损失和准确率曲线。注意,没有微调时,验证损失收敛到较高值,验证准确率收敛到比面板 A 低的值。请注意,虽然进行了微调(面板 A)的最终准确率达到约 0.9,但在没有进行微调但总纪元数相同的情况下(面板 B),准确率停留在约 0.85


为了说明微调相对于不进行微调的迁移学习的价值,我们在图 5.12 的面板 B 中展示了如果不微调基础模型的顶部几层,而将迁移模型训练相同数量(400)的纪元时会发生什么。在面板 A 中发生的在第 100 纪元时进行微调的损失或准确率曲线上没有“拐点”。相反,损失和准确率曲线趋于平稳,并收敛到较差的值。

那么为什么微调有帮助呢?可以理解为增加了模型的容量。通过解冻基本模型的一些最顶层,我们允许转移模型在比初始阶段更高维的参数空间中最小化损失函数。这类似于向神经网络添加隐藏层。解冻的密集层的权重参数已经针对原始数据集进行了优化(由诸如“one”、“two”、“yes”和“no”之类的单词组成的数据集),这可能对转移单词不是最优的。这是因为帮助模型区分这些原始单词的内部表示可能不是使转移单词最容易区分的表示。通过允许进一步优化(即微调)这些参数以用于转移单词,我们允许表示被优化用于转移单词。因此,我们在转移单词上获得验证准确性的提升。请注意,当转移学习任务很难时(如四个易混淆的单词:“feel”、“seal”、“veal”和“zeal”),更容易看到这种提升。对于更容易的任务(更不同的单词,如“red”和“green”),验证准确性可能仅仅通过初始的转移学习就可以达到 100%。

你可能想问的一个问题是,在这里我们只解冻了基本模型的一层,但是解冻更多的层会有帮助吗?简短的答案是,这取决于情况,因为解冻更多的层会使模型的容量更高。但正如我们在第四章中提到的,并且将在第八章中更详细地讨论,更高的容量会导致过拟合的风险增加,特别是当我们面对像这里收集到的音频示例这样的小数据集时。更不用说训练更多层所需的额外计算负载了。鼓励你作为本章末尾的一部分来进行自己的实验。

让我们结束 TensorFlow.js 中关于迁移学习的这一部分。我们介绍了三种在新任务上重用预训练模型的不同方法。为了帮助你决定在将来的迁移学习项目中使用哪种方法,我们在 table 5.1 中总结了这三种方法及其相对优缺点。

表 5.1. TensorFlow.js 中三种迁移学习方法及其相对优势和缺点的总结
方法 优势 缺点
使用原始模型并冻结其前几层(特征提取层)(section 5.1.1)。
  • 简单而方便

|

  • 仅当迁移学习所需的输出形状和激活与基本模型的形状和激活匹配时才起作用

|

从原始模型中获取内部激活作为输入示例的嵌入,并创建一个以该嵌入作为输入的新模型(section 5.1.2)。
  • 适用于需要与原始输出形状不同的迁移学习情况
  • 嵌入张量是直接可访问的,使得 k 最近邻(kNN,见信息框 5.2)分类器等方法成为可能

|

  • 需要管理两个独立的模型实例
  • 很难微调原始模型的层

|

创建一个包含原始模型的特征提取层和新头部层的新模型(请参见第 5.1.3 节)。
  • 适用于需要与原始输出形状不同的迁移学习情况
  • 只需要管理一个模型实例
  • 允许对特征提取层进行微调

|

  • 无法直接访问内部激活(嵌入)张量

|

5.2. 通过卷积神经网络进行目标检测的迁移学习

到目前为止,你在本章中所看到的迁移学习示例都有一个共同点:在迁移后机器学习任务的性质保持不变。特别是,它们采用了一个在多类分类任务上训练的计算机视觉模型,并将其应用于另一个多类分类任务。在本节中,我们将展示这并不一定是这样的。基础模型可以用于非常不同于原始任务的任务,例如当你想使用在分类任务上训练的基础模型来执行回归(拟合数字)时。这种跨领域迁移是深度学习的多功能性和可重复使用性的良好例证,是该领域成功的主要原因之一。

为了说明这一点,我们将使用新的任务——目标检测,这是本书中第一个非分类计算机视觉问题类型。目标检测涉及在图像中检测特定类别的物体。它与分类有何不同?在目标检测中,检测到的物体不仅会以类别(它是什么类型的物体)的形式报告,还会包括有关物体在图像中内部位置的一些附加信息(物体在哪里)。后者是一个普通分类器无法提供的信息。例如,自动驾驶汽车使用的典型目标检测系统会分析输入图像的一个框架,以便该系统不仅输出图像中存在的有趣对象的类型(如车辆和行人),还输出这些对象在图像坐标系内的位置、表面积和姿态等信息。

示例代码位于 tfjs-examples 仓库的 simple-object-detection 目录中。请注意,此示例与您迄今为止看到的示例不同,因为它将在 Node.js 中的模型训练与浏览器中的推理结合起来。具体来说,模型训练使用 tfjs-node(或 tfjs-node-gpu)进行,训练好的模型将保存到磁盘上。然后使用 parcel 服务器来提供保存的模型文件,以及静态的 index.html 和 index.js,以展示浏览器中模型的推理。

您可以使用的运行示例的命令序列如下(其中包含一些您在输入命令时不需要包含的注释字符串):

git clone https://github.com/tensorflow/tfjs-examples.git
        cd tfjs-examples/simple-object-detection
        yarn
        # Optional step for training your own model using Node.js:
        yarn train \
            --numExamples 20000 \
            --initialTransferEpochs 100 \
            --fineTuningEpochs 200
        yarn watch  # Run object-detection inference in the browser.

yarn train命令会在您的机器上进行模型训练,并在完成后将模型保存到./dist文件夹中。请注意,这是一个长时间运行的训练任务,如果您有 CUDA 启用的 GPU,则最好处理,因为这可以将训练速度提高 3 到 4 倍。为此,您只需要向yarn train命令添加--gpu标志:

yarn train --gpu \
            --numExamples 20000 \
            --initialTransferEpochs 100 \
            --fineTuningEpochs 200

如果您没有时间或资源在自己的机器上对模型进行训练,不用担心:您可以直接跳过yarn train命令,直接执行yarn watch。在浏览器中运行的推理页面将允许您通过 HTTP 从集中位置加载我们已经为您训练好的模型。

5.2.1. 基于合成场景的简单目标检测问题

最先进的目标检测技术涉及许多技巧,这些技巧不适合用于初学者教程。我们在这里的目标是展示目标检测的本质,而不受太多技术细节的困扰。为此,我们设计了一个涉及合成图像场景的简单目标检测问题(见图 5.13)。这些合成图像的尺寸为 224 × 224,色深为 3(RGB 通道),因此与将成为我们模型基础的 MobileNet 模型的输入规范匹配。正如图 5.13 中的示例所示,每个场景都有一个白色背景。要检测的对象可以是等边三角形或矩形。如果对象是三角形,则其大小和方向是随机的;如果对象是矩形,则其高度和宽度是随机变化的。如果场景仅由白色背景和感兴趣的对象组成,则任务将太容易,无法展示我们技术的强大之处。为了增加任务的难度,在场景中随机分布了一些“噪声对象”。这些对象包括每个图像中的 10 个圆和 10 条线段。圆的位置和大小以随机方式生成,线段的位置和长度也是如此。一些噪声对象可能位于目标对象的顶部,部分遮挡它。所有目标和噪声对象都具有随机生成的颜色。

图 5.13. 简单物体检测使用的合成场景示例。面板 A:一个旋转的等边三角形作为目标对象。面板 B:一个矩形作为目标对象。标记为“true”的框是感兴趣对象的真实边界框。请注意,感兴趣对象有时可能会被一些噪声对象(线段和圆)部分遮挡。

随着输入数据被完全描述,我们现在可以为我们即将创建和训练的模型定义任务。该模型将输出五个数字,这些数字被组织成两组:

  • 第一组包含一个数字,指示检测到的对象是三角形还是矩形(不考虑其位置、大小、方向和颜色)。
  • 剩下的四个数字组成了第二组。它们是检测到的物体周围边界框的坐标。具体来说,它们分别是边界框的左 x 坐标、右 x 坐标、顶部 y 坐标和底部 y 坐标。参见图 5.13 作为示例。

使用合成数据的好处是 1)真实标签值会自动知道,2)我们可以生成任意数量的数据。每次生成场景图像时,对象的类型和其边界框都会自动从生成过程中对我们可用。因此,不需要对训练图像进行任何劳动密集型的标记。这种输入特征和标签一起合成的非常高效的过程在许多深度学习模型的测试和原型环境中使用,并且这是一种你应该熟悉的技术。然而,用于真实图像输入的训练物体检测模型需要手动标记的真实场景。幸运的是,有这样的标记数据集可用。通用物体和背景(COCO)数据集就是其中之一(参见cocodataset.org)。

训练完成后,模型应能够以相当高的准确性定位和分类目标对象(如图 5.13 中所示的示例所示)。要了解模型如何学习这个物体检测任务,请跟随我们进入下一节中的代码。

5.2.2. 深入了解简单物体检测

现在让我们构建神经网络来解决合成对象检测问题。与以前一样,我们在预训练的 MobileNet 模型上构建我们的模型,以使用模型的卷积层中的强大的通用视觉特征提取器。这是 列表 5.9 中的 loadTruncatedBase() 方法所做的。然而,我们的新模型面临的一个新挑战是如何同时预测两个东西:确定目标对象的形状以及在图像中找到其坐标。我们以前没有见过这种“双任务预测”类型。我们在这里使用的技巧是让模型输出一个张量,该张量封装了两个预测,并且我们将设计一个新的损失函数,该函数同时衡量模型在两个任务中的表现如何。我们可以训练两个单独的模型,一个用于分类形状,另一个用于预测边界框。但是与使用单个模型执行两个任务相比,运行两个模型将涉及更多的计算和更多的内存使用,并且不利用特征提取层可以在两个任务之间共享的事实。(以下代码来自 simple-object-detection/train.js。)

列表 5.9. 基于截断 MobileNet 定义简单对象学习模型^([11])

¹¹

为了清晰起见,一些用于检查错误条件的代码已被删除。

const topLayerGroupNames = [                                            ***1***
    'conv_pw_9', 'conv_pw_10', 'conv_pw_11'];                           ***1***
const topLayerName =
    `${topLayerGroupNames[topLayerGroupNames.length - 1]}_relu`;
async function loadTruncatedBase() {
  const mobilenet = await tf.loadLayersModel(
      'https://storage.googleapis.com/' +
          'tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json');
  const fineTuningLayers = [];
  const layer = mobilenet.getLayer(topLayerName);                       ***2***
  const truncatedBase =                                                 ***3***
      tf.model({                                                        ***3***
        inputs: mobilenet.inputs,                                       ***3***
        outputs: layer.output                                           ***3***
      });                                                               ***3***
  for (const layer of truncatedBase.layers) {
    layer.trainable = false;                                            ***4***
    for (const groupName of topLayerGroupNames) {
      if (layer.name.indexOf(groupName) === 0) {                        ***5***
        fineTuningLayers.push(layer);
        break;
      }
    }
  }
  return {truncatedBase, fineTuningLayers};
}
function buildNewHead(inputShape) {                                     ***6***
  const newHead = tf.sequential();                                      ***6***
  newHead.add(tf.layers.flatten({inputShape}));                         ***6***
  newHead.add(tf.layers.dense({units: 200, activation: 'relu'}));       ***6***
  newHead.add(tf.layers.dense({units: 5}));                             ***6*** ***7***
  return newHead;                                                       ***6***
}                                                                       ***6***
async function buildObjectDetectionModel() {                            ***8***
  const {truncatedBase, fineTuningLayers} = await loadTruncatedBase();  ***8***
  const newHead = buildNewHead(truncatedBase.outputs[0].shape.slice(1));***8***
  const newOutput = newHead.apply(truncatedBase.outputs[0]);            ***8***
  const model = tf.model({                                              ***8***
    inputs: truncatedBase.inputs,                                       ***8***
    outputs: newOutput                                                  ***8***
  });                                                                   ***8***
  return {model, fineTuningLayers};
}
  • 1 设置要解冻以进行微调的层
  • 2 获取中间层:最后一个特征提取层
  • 3 形成截断的 MobileNet
  • 4 冻结所有特征提取层以进行迁移学习的初始阶段
  • 5 跟踪在微调期间将解冻的层
  • 6 为简单对象检测任务构建新头模型
  • 7 长度为 5 的输出包括长度为 1 的形状指示器和长度为 4 的边界框(请参见 图 5.14)。
  • 8 将新的头模型放在截断的 MobileNet 顶部,形成整个对象检测模型

“双任务”模型的关键部分由 列表 5.9 中的 buildNewHead() 方法构建。模型的示意图显示在 图 5.14 的左侧。新头部由三层组成。一个展平层将截断的 MobileNet 基础的最后一个卷积层的输出形状为后续可以添加的密集层。第一个密集层是具有 relu 非线性的隐藏层。第二个密集层是头部的最终输出,因此也是整个对象检测模型的最终输出。该层具有默认的线性激活。这是理解该模型如何工作的关键,因此需要仔细查看。

图 5.14. 对象检测模型及其基于的自定义损失函数。请参见 列表 5.9,了解模型(左侧部分)的构建方式。请参见 列表 5.10,了解自定义损失函数的编写方式。

正如你从代码中看到的那样,最终的稠密层输出单元数为 5。这五个数字代表什么?它们结合了形状预测和边界框预测。有趣的是,决定它们含义的不是模型本身,而是将用于模型的损失函数。之前,你看到过各种类型的损失函数,可以直接使用字符串名称,如"meanSquaredError",并适用于各自的机器学习任务(例如,请参阅第三章表 3.6)。然而,这只是在 TensorFlow.js 中指定损失函数的两种方法之一。另一种方法,也是我们在这里使用的方法,涉及定义一个满足某个特定签名的自定义 JavaScript 函数。该签名如下:

  • 有两个输入参数:1)输入示例的真实标签和 2)模型的对应预测。它们都表示为 2D 张量。这两个张量的形状应该是相同的,每个张量的第一个维度都是批处理大小。
  • 返回值是一个标量张量(形状为[]的张量),其值是批处理中示例的平均损失。

我们根据这个签名编写的自定义损失函数在列表 5.10 中显示,并在图 5.14 的右侧进行了图形化说明。customLossFunction的第一个输入(yTrue)是真实标签张量,其形状为[batchSize, 5]

第二个输入(yPred)是模型的输出预测,其形状与yTrue完全相同。在yTrue的第二轴上的五个维度(如果我们将其视为矩阵,则为五列)中,第一个维度是目标对象形状的 0-1 指示器(三角形为 0,矩形为 1)。这是由数据合成方式确定的(请参阅 simple-object-detection/synthetic_images.js)。其余四列是目标对象的边界框,即其左、右、上和下值,每个值范围从 0 到 CANVAS_SIZE(224)。数字 224 是输入图像的高度和宽度,来自于 MobileNet 的输入图像大小,我们的模型基于此。

列表 5.10。为对象检测任务定义自定义损失函数
const labelMultiplier = tf.tensor1d([CANVAS_SIZE, 1, 1, 1, 1]);
function customLossFunction(yTrue, yPred) {
  return tf.tidy(() => {
    return tf.metrics.meanSquaredError(
        yTrue.mul(labelMultiplier), yPred);     ***1***
  });
}
  • 1 yTrue的形状指示器列通过 CANVAS_SIZE(224)缩放,以确保形状预测和边界框预测对损失的贡献大致相等。

自定义损失函数接收yTrue并将其第一列(0-1 形状指示器)按CANVAS_SIZE进行缩放,同时保持其他列不变。然后它计算yPred和缩放后的yTrue之间的均方误差。为什么我们要缩放yTrue中的 0-1 形状标签?我们希望模型输出一个代表它预测形状是三角形还是矩形的数字。具体地说,它输出一个接近于 0 的数字表示三角形,一个接近于CANVAS_SIZE(224)的数字表示矩形。因此,在推理时,我们只需将模型输出的第一个值与CANVAS_SIZE/2(112)进行比较,以获取模型对形状更像是三角形还是矩形的预测。那么如何衡量这种形状预测的准确性以得出损失函数呢?我们的答案是计算这个数字与 0-1 指示器之间的差值,乘以CANVAS_SIZE

为什么我们这样做而不像在第三章中钓鱼检测示例中使用二进制交叉熵呢?我们需要在这里结合两个准确度指标:一个是形状预测,另一个是边界框预测。后者任务涉及预测连续值,可以视为回归任务。因此,均方误差是边界框的自然度量标准。为了结合这些度量标准,我们只需“假装”形状预测也是一个回归任务。这个技巧使我们能够使用单个度量函数(列表 5.10 中的tf.metric.meanSquaredError()调用)来封装两种预测的损失。

但是为什么我们要将 0-1 指示器按CANVAS_SIZE进行缩放呢?如果我们不进行这种缩放,我们的模型最终会生成一个接近于 0-1 的数字,作为它预测形状是三角形(接近于 0)还是矩形(接近于 1)的指示器。在[0, 1]区间内的数字之间的差异显然要比我们从比较真实边界框和预测边界框得到的差异要小得多,后者在 0-224 的范围内。因此,来自形状预测的误差信号将完全被来自边界框预测的误差信号所掩盖,这对于我们得到准确的形状预测没有帮助。通过缩放 0-1 形状指示器,我们确保形状预测和边界框预测对最终损失值(customLossFunction()的返回值)的贡献大致相等,因此当模型被训练时,它将同时优化两种类型的预测。在本章末尾的练习 4 中,我们鼓励你自己尝试使用这种缩放。

¹²

这里的一个替代方法是采用基于缩放和 meanSquaredError 的方法,将 yPred 的第一列作为形状概率分数,并与 yTrue 的第一列计算二元交叉熵。然后,将二元交叉熵值与在 yTrueyPred 的其余列上计算的 MSE 相加。但在这种替代方法中,需要适当缩放交叉熵,以确保与边界框损失的平衡,就像我们当前的方法一样。缩放涉及一个自由参数,其值需要仔细选择。在实践中,它成为模型的一个额外超参数,并且需要时间和计算资源来调整,这是该方法的一个缺点。为了简单起见,我们选择了当前方法,而不采用该方法。

数据准备就绪,模型和损失函数已定义,我们准备好训练我们的模型!模型训练代码的关键部分显示在 清单 5.11(来自 simple-object-detection/train.js)。与我们之前见过的微调(第 5.1.3 节)类似,训练分为两个阶段:初始阶段,在此阶段仅训练新的头部层;微调阶段,在此阶段将新的头部层与截断的 MobileNet 基础的顶部几层一起训练。需要注意的是,在微调的 fit() 调用之前,必须(再次)调用 compile() 方法,以便更改层的 trainable 属性生效。如果在您自己的机器上运行训练,很容易观察到损失值在微调阶段开始时出现显着下降,这反映了模型容量的增加以及解冻特征提取层对目标检测数据中的唯一特征的适应能力。在微调期间解冻的层的列表由 fineTuningLayers 数组确定,在截断 MobileNet 时填充(参见 清单 5.9 中的 loadTruncatedBase() 函数)。这些是截断的 MobileNet 的顶部九层。在本章末尾的练习 3 中,您可以尝试解冻更少或更多的基础顶部层,并观察它们如何改变训练过程产生的模型的准确性。

清单 5.11. 训练目标检测模型的第二阶段
const {model, fineTuningLayers} = await buildObjectDetectionModel();
  model.compile({                                  ***1***
    loss: customLossFunction,                      ***1***
    optimizer: tf.train.rmsprop(5e-3)              ***1***
  });                                              ***1***
  await model.fit(images, targets, {               ***2***
    epochs: args.initialTransferEpochs,            ***2***
    batchSize: args.batchSize,                     ***2***
    validationSplit: args.validationSplit          ***2***
  });                                              ***2***
  // Fine-tuning phase of transfer learning.
  for (const layer of fineTuningLayers) {          ***3***
    layer.trainable = true;                        ***4***
  }
  model.compile({                                  ***5***
    loss: customLossFunction,                      ***5***
    optimizer: tf.train.rmsprop(2e-3)              ***5***
  });                                              ***5***
  await model.fit(images, targets, {
    epochs: args.fineTuningEpochs,
    batchSize: args.batchSize / 2,                 ***6***
    validationSplit: args.validationSplit
  });                                              ***7***
  • 1 在初始阶段使用相对较高的学习率
  • 2 执行迁移学习的初始阶段
  • 3 开始微调阶段
  • 4 解冻一些层进行微调
  • 5 在微调阶段使用稍低的学习率
  • 6 在微调阶段,我们将 batchSize 减少以避免由于反向传播涉及更多权重并消耗更多内存而引起的内存不足问题。
  • 7 执行微调阶段

微调结束后,模型将保存到磁盘,并在浏览器内推断步骤期间加载(通过yarn watch命令启动)。如果加载托管模型,或者您已经花费了时间和计算资源在自己的计算机上训练了一个相当不错的模型,那么您在推断页面上看到的形状和边界框预测应该是相当不错的(在初始训练的 100 个周期和微调的 200 个周期后,验证损失在<100)。推断结果是好的但并非完美(请参阅 图 5.13 中的示例)。当您检查结果时,请记住,在浏览器内评估是公平的,并且反映了模型的真实泛化能力,因为训练模型在浏览器中要解决的示例与迁移学习过程中它所见到的训练和验证示例不同。

结束本节时,我们展示了如何将之前在图像分类上训练的模型成功地应用于不同的任务:对象检测。在此过程中,我们演示了如何定义一个自定义损失函数来适应对象检测问题的“双重任务”(形状分类 + 边界框回归)的特性,以及如何在模型训练过程中使用自定义损失。该示例不仅说明了对象检测背后的基本原理,还突显了迁移学习的灵活性以及它可能用于的问题范围。在生产应用中使用的对象检测模型当然比我们在这里使用合成数据集构建的玩具示例更复杂,并且涉及更多技巧。信息框 5.3 简要介绍了一些有关高级对象检测模型的有趣事实,并描述了它们与您刚刚看到的简单示例的区别以及您如何通过 TensorFlow.js 使用其中之一。

生产对象检测模型

TensorFlow.js 版本的 Single-Shot Detection (SSD) 模型的一个示例对象检测结果。注意多个边界框及其关联的对象类和置信度分数。

对象检测是许多类型应用的重要任务,例如图像理解、工业自动化和自动驾驶汽车。最著名的最新对象检测模型包括 Single-Shot Detection^([a])(SSD,示例推断结果如图所示)和 You Only Look Once (YOLO)^([b])。这些模型在以下方面与我们在简单对象检测示例中看到的模型类似:

^a

Wei Liu 等人,“SSD: Single Shot MultiBox Detector,” 计算机科学讲义 9905,2016,mng.bz/G4qD

^b

Joseph Redmon 等人,“You Only Look Once: Unified, Real-Time Object Detection,” IEEE 计算机视觉与模式识别会议论文集 (CVPR), 2016, pp. 779–788, mng.bz/zlp1.

  • 它们预测对象的类别和位置。
  • 它们是建立在 MobileNet 和 VGG16 等预训练图像分类模型上的,并通过迁移学习进行训练。

^c

Karen Simonyan 和 Andrew Zisserman,“Very Deep Convolutional Networks for Large-Scale Image Recognition,” 2014 年 9 月 4 日提交, arxiv.org/abs/1409.1556.

然而,它们在很多方面也与我们的玩具模型不同:

  • 真实的目标检测模型预测的对象类别比我们的简单模型多得多(例如,COCO 数据集有 80 个对象类别;参见cocodataset.org/#home)。
  • 它们能够在同一图像中检测多个对象(请参见示例图)。
  • 它们的模型架构比我们简单模型中的更复杂。例如,SSD 模型在截断的预训练图像模型顶部添加了多个新头部,以便预测输入图像中多个对象的类别置信度分数和边界框。
  • 与使用单个meanSquaredError度量作为损失函数不同,真实目标检测模型的损失函数是两种类型损失的加权和:1)对于对象类别预测的类似 softmax 交叉熵的损失和 2)对于边界框的meanSquaredErrormeanAbsoluteError-like 损失。两种类型损失值之间的相对权重被精心调整以确保来自两种错误来源的平衡贡献。
  • 真实的目标检测模型会为每个输入图像产生大量的候选边界框。这些边界框被“修剪”以便保留具有最高对象类别概率分数的边界框作为最终输出。
  • 一些真实的目标检测模型融合了关于对象边界框位置的“先验知识”。这些是关于边界框在图像中位置的教育猜测,基于对更多标记的真实图像的分析。这些先验知识通过从一个合理的初始状态开始而不是完全随机的猜测(就像我们简单的目标检测示例中一样)来加快模型的训练速度。

一些真实的目标检测模型已被移植到 TensorFlow.js 中。例如,你可以玩的最好的模型之一位于 tfjs-models 存储库的 coco-ssd 目录中。要看它的运行情况,执行以下操作:

git clone https://github.com/tensorflow/tfjs-models.git
    cd tfjs-models/coco-ssd/demo
    yarn && yarn watch

如果你对了解更多真实目标检测模型感兴趣,你可以阅读以下博文。它们分别是关于 SSD 模型和 YOLO 模型的,它们使用不同的模型架构和后处理技术:

  • “理解 SSD MultiBox——深度学习中的实时目标检测” by Eddie Forson: mng.bz/07dJ
  • “使用 YOLO、YOLOv2 和现在的 YOLOv3 进行实时目标检测” by Jonathan Hui: mng.bz/KEqX

到目前为止,在本书中,我们处理的是交给我们并准备好探索的机器学习数据集。它们格式良好,已经通过我们之前的数据科学家和机器学习研究人员的辛勤工作进行了清理,以至于我们可以专注于建模,而不用太担心如何摄取数据以及数据是否正确。这对本章中使用的 MNIST 和音频数据集是真实的;对于我们在第三章中使用的网络钓和鸢尾花数据集也是如此。

我们可以肯定地说,这永远不是您将遇到的真实世界机器学习问题的情况。事实上,大多数机器学习从业者的时间都花在获取、预处理、清理、验证和格式化数据上。在下一章中,我们将教您在 TensorFlow.js 中可用的工具,使这些数据整理和摄取工作流更容易。

¹³

Gil Press,“清理大数据:调查显示,最耗时、最不受欢迎的数据科学任务”,《福布斯》,2016 年 3 月 23 日,mng.bz/9wqj

练习

  1. 当我们在第 5.1.1 节中访问 mnist-transfer-cnn 示例时,我们指出设置模型层的trainable属性在训练期间不会生效,除非在训练之前调用模型的compile()方法。通过对示例的 index.js 文件中的retrainModel()方法进行一些更改来验证这一点。具体来说,
  1. 在带有 this.model .compile() 行之前添加一个 this.model.summary() 调用,并观察可训练和不可训练参数的数量。它们显示了什么?它们与在 compile() 调用之后获得的数字有何不同?
  2. 独立于前一项,将 this.model.compile() 调用移到特征层的 trainable 属性设置之前。换句话说,在 compile() 调用之后设置这些层的属性。这样做会如何改变训练速度?速度是否与仅更新模型的最后几层一致?你能找到其他确认,在这种情况下,模型的前几层的权重在训练期间是否被更新的方法吗?
  1. 在第 5.1.1 节的迁移学习中(列表 5.1),我们通过将其trainable属性设置为false来冻结了前两个 conv2d 层,然后开始了fit()调用。你能在 mnist-transfer-cnn 示例的 index.js 中添加一些代码来验证 conv2d 层的权重确实没有被fit()调用改变吗?我们在同一节中尝试的另一种方法是在不冻结层的情况下调用fit()。你能验证在这种情况下层的权重值确实被fit()调用改变了吗?(提示:回想一下在第 2.4.2 节的第二章中,我们使用了模型对象的layers属性和其getWeights()方法来访问权重的值。)
  2. 将 Keras MobileNetV2^([14])(不是 MobileNetV1!—我们已经完成了)应用转换为 TensorFlow.js 格式,并将其加载到浏览器的 TensorFlow.js 中。详细步骤请参阅信息框 5.1。你能使用summary()方法来检查 MobileNetV2 的拓扑结构,并确定其与 MobileNetV1 的主要区别吗?

¹⁴

Mark Sandler 等人,“MobileNetV2: Inverted Residuals and Linear Bottlenecks”,2019 年 3 月 21 日修订,arxiv.org/abs/1801.04381

  1. 列表 5.8 中微调代码的一个重要方面是在基础模型中解冻密集层后再次调用compile()方法。你能做到以下几点吗?
  1. 使用与练习 2 相同的方法来验证密集层的权重(核心和偏置)确实没有被第一次fit()调用(用于迁移学习的初始阶段)所改变,并且确实被第二次fit()调用(用于微调阶段)所改变。
  2. 尝试在解冻行之后(更改 trainable 属性值的行)注释掉compile()调用,看看这如何影响你刚刚观察到的权重值变化。确信compile()调用确实是让模型的冻结/解冻状态变化生效的必要步骤。
  3. 更改代码,尝试解冻基础 speech-command 模型的更多承载权重的层(例如,倒数第二个密集层之前的 conv2d 层),看看这对微调的结果有何影响。
  1. 在我们为简单对象检测任务定义的自定义损失函数中,我们对 0-1 形状标签进行了缩放,以便来自形状预测的误差信号与来自边界框预测的误差信号匹配(参见 listing 5.10 中的代码)。试验一下,如果在代码中删除 mul() 调用会发生什么。说服自己这种缩放对于确保相当准确的形状预测是必要的。这也可以通过在 compile() 调用中简单地将 customLossFunction 的实例替换为 meanSquaredError 来完成(参见 listing 5.11)。还要注意,训练期间移除缩放需要伴随着对推断时的阈值调整:将推断逻辑中的阈值从 CANVAS_SIZE/2 更改为 1/2(在 simple-object-detection/index.js 中)。
  2. 在简单对象检测示例中的微调阶段,涉及到解冻截断的 MobileNet 基础的前九个顶层(参见 listing 5.9 中的 fineTuningLayers 如何填充)。一个自然的问题是,为什么是九个?在这个练习中,通过在 fineTuningLayers 数组中包含更少或更多的层来改变解冻层的数量。当你在微调期间解冻更少的层时,你期望在以下情况下会看到什么:1) 最终损失值和 2) 每个纪元在微调阶段花费的时间?实验结果是否符合你的期望?解冻更多的层在微调期间会怎样?

摘要

  • 迁移学习是在与模型最初训练的任务相关但不同的学习任务上重新使用预训练模型或其一部分的过程。这种重用加快了新的学习任务的速度。
  • 在迁移学习的实际应用中,人们经常会重用已经在非常大的分类数据集上训练过的卷积网络,比如在 ImageNet 数据集上训练过的 MobileNet。由于原始数据集的规模庞大以及包含的示例的多样性,这些预训练模型带来了强大的、通用的特征提取器卷积层,适用于各种计算机视觉问题。这样的层对于在典型的迁移学习问题中可用的少量数据来说很难,如果不是不可能的话,进行训练。
  • 我们在 TensorFlow.js 中讨论了几种通用的迁移学习方法,它们在以下方面有所不同:1) 新层是否被创建为迁移学习的“新头部”,以及 2) 是否使用一个模型实例或两个模型实例进行迁移学习。每种方法都有其优缺点,并适用于不同的用例(参见 table 5.1)。
  • 通过设置模型层的trainable属性,我们可以防止在训练(Model.fit()函数调用)期间更新其权重。这被称为冻结,并用于在迁移学习中“保护”基础模型的特征提取层。
  • 在一些迁移学习问题中,我们可以在初始训练阶段之后解冻一些基础模型的顶层,从而提升新模型的性能。这反映了解冻层对新数据集中独特特征的适应性。
  • 迁移学习是一种多用途、灵活的技术。基础模型可以帮助我们解决与其初始训练内容不同的问题。我们通过展示如何基于 MobileNet 训练目标检测模型来阐明这一点。
  • TensorFlow.js 中的损失函数可以定义为在张量输入和输出上操作的自定义 JavaScript 函数。正如我们在简单目标检测示例中展示的,为了解决实际的机器学习问题,通常需要自定义损失函数。

第三部分:TensorFlow.js 的高级深度学习。

阅读完第一部分和第二部分,您现在应该熟悉在 TensorFlow.js 中进行基本深度学习的方法。第三部分旨在为想要更牢固掌握技术和对深度学习有更广泛理解的用户提供帮助。第六章介绍了如何处理、转换和使用机器学习数据的技巧。第七章介绍了用于可视化数据和模型的工具。第八章关注欠拟合和过拟合等重要现象以及如何有效应对。在这个讨论的基础上,我们介绍了机器学习的通用工作流程。第九章至第十一章是三个高级领域的实践之旅:序列导向模型、生成模型和强化学习。它们将使您熟悉深度学习最激动人心的前沿技术。

JavaScript 深度学习(二)(4)https://developer.aliyun.com/article/1516956

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
2天前
|
机器学习/深度学习 JavaScript 前端开发
【JS】深度学习JavaScript
【JS】深度学习JavaScript
6 2
|
1月前
|
机器学习/深度学习 自然语言处理 JavaScript
JavaScript 深度学习(四)(1)
JavaScript 深度学习(四)
13 2
|
1月前
|
机器学习/深度学习 前端开发 JavaScript
JavaScript 深度学习(四)(5)
JavaScript 深度学习(四)
15 1
|
1月前
|
机器学习/深度学习 存储 算法
JavaScript 深度学习(四)(4)
JavaScript 深度学习(四)
23 1
|
1月前
|
机器学习/深度学习 算法 JavaScript
JavaScript 深度学习(四)(3)
JavaScript 深度学习(四)
28 1
|
1月前
|
机器学习/深度学习 编解码 JavaScript
JavaScript 深度学习(四)(2)
JavaScript 深度学习(四)
24 1
|
1月前
|
机器学习/深度学习 JavaScript 前端开发
JavaScript 深度学习(五)(5)
JavaScript 深度学习(五)
11 0
|
1月前
|
机器学习/深度学习 JavaScript 前端开发
JavaScript 深度学习(五)(4)
JavaScript 深度学习(五)
26 0
|
1月前
|
存储 机器学习/深度学习 JavaScript
JavaScript 深度学习(五)(3)
JavaScript 深度学习(五)
25 0
|
1月前
|
机器学习/深度学习 并行计算 JavaScript
JavaScript 深度学习(五)(2)
JavaScript 深度学习(五)
32 0