JavaScript 深度学习(二)(2)

简介: JavaScript 深度学习(二)

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

4.4. 语音识别:在音频数据上应用卷积网络

到目前为止,我们已经向您展示了如何使用卷积网络执行计算机视觉任务。但是人类的感知不仅仅是视觉。音频是感知数据的一个重要模态,并且可以通过浏览器 API 进行访问。如何识别语音和其他类型声音的内容和意义?值得注意的是,卷积网络不仅适用于计算机视觉,而且在音频相关的机器学习中也以显著的方式发挥作用。

在本节中,您将看到我们如何使用类似于我们为 MNIST 构建的卷积网络来解决一个相对简单的音频任务。该任务是将短语音片段分类到 20 多个单词类别中。这个任务比您可能在亚马逊 Echo 和 Google Home 等设备中看到的语音识别要简单。特别是,这些语音识别系统涉及比本示例中使用的词汇量更大的词汇。此外,它们处理由多个词连续发音组成的连续语音,而我们的示例处理逐个单词发音。因此,我们的示例不符合“语音识别器”的条件;相反,更准确地描述它为“单词识别器”或“语音命令识别器”。然而,我们的示例仍然具有实际用途(如无需手动操作的用户界面和可访问性功能)。此外,本示例中体现的深度学习技术实际上是更高级语音识别系统的基础。^([9])

Ronan Collobert、Christian Puhrsch 和 Gabriel Synnaeve,“Wav2Letter: 一种基于端到端卷积网络的语音识别系统”,2016 年 9 月 13 日提交,arxiv.org/abs/1609.03193

4.4.1. 声谱图:将声音表示为图像

与任何深度学习应用一样,如果您想要理解模型的工作原理,首先需要了解数据。要理解音频卷积网络的工作原理,我们需要首先查看声音是如何表示为张量的。请回忆高中物理课上的知识,声音是空气压力变化的模式。麦克风捕捉到空气压力变化并将其转换为电信号,然后计算机的声卡可以将其数字化。现代 Web 浏览器提供了WebAudio API,它与声卡通信并提供对数字化音频信号的实时访问(在用户授权的情况下)。因此,从 JavaScript 程序员的角度来看,声音就是一组实值数字的数组。在深度学习中,这种数字数组通常表示为 1D 张量。

你可能会想,迄今为止我们见过的这种卷积网络是如何在 1D 张量上工作的?它们不是应该操作至少是 2D 的张量吗?卷积网络的关键层,包括 conv2d 和 maxPooling2d,利用了 2D 空间中的空间关系。事实证明声音可以被表示为称为声谱图的特殊类型的图像。声谱图不仅使得可以在声音上应用卷积网络,而且在深度学习之外还具有理论上的解释。

如 图 4.12 所示,频谱图是一个二维数组,可以以与 MNIST 图像基本相同的方式显示为灰度图像。水平维度是时间,垂直维度是频率。频谱图的每个垂直切片是一个短时间窗口内的声音的频谱。频谱是将声音分解为不同频率分量的过程,可以粗略地理解为不同的“音高”。就像光可以通过棱镜分解成多种颜色一样,声音可以通过称为傅里叶变换的数学操作分解为多个频率。简而言之,频谱图描述了声音的频率内容如何在一系列连续的短时间窗口(通常约为 20 毫秒)内变化。

图 4.12. “zero” 和 “yes” 这两个孤立口语单词的示例频谱图。频谱图是声音的联合时间-频率表示。你可以将频谱图视为声音的图像表示。沿着时间轴的每个切片(图像的一列)都是时间的短时刻(帧);沿着频率轴的每个切片(图像的一行)对应于特定的窄频率范围(音调)。图像的每个像素的值表示给定时间点上给定频率区段的声音相对能量。本图中的频谱图被渲染为较暗的灰色,对应着较高的能量。不同的语音有不同的特征。例如,类似于“z”和“s”这样的咝音辅音以在 2–3 kHz 以上频率处集中的准稳态能量为特征;像“e”和“o”这样的元音以频谱的低端(< 3 kHz)中的水平条纹(能量峰值)为特征。在声学中,这些能量峰值被称为共振峰。不同的元音具有不同的共振峰频率。所有这些不同语音的独特特征都可以被深度卷积神经网络用于识别单词。

谱图对于以下原因是声音的合适表示。首先,它们节省空间:谱图中的浮点数通常比原始波形中的浮点值少几倍。其次,在宽泛的意义上,谱图对应于生物学中的听力工作原理。内耳内部的一种名为耳蜗的解剖结构实质上执行了傅里叶变换的生物版本。它将声音分解成不同的频率,然后被不同组听觉神经元接收。第三,谱图表示可以更容易地区分不同类型的语音。这在 图 4.12 的示例语音谱图中可以看到:元音和辅音在谱图中都有不同的特征模式。几十年前,在机器学习被广泛应用之前,从谱图中检测不同的元音和辅音的人们实际上尝试手工制作规则。深度学习为我们节省了这种手工制作的麻烦和泪水。

让我们停下来思考一下。看一看 图 4.1 中的 MNIST 图片和 图 4.12 中的声音谱图,你应该能够理解这两个数据集之间的相似之处。两个数据集都包含在二维特征空间中的模式,一双经过训练的眼睛应该能够区分出来。两个数据集都在特征的具体位置、大小和细节上呈现一定的随机性。最后,两个数据集都是多类别分类任务。虽然 MNIST 有 10 个可能的类别,我们的声音命令数据集有 20 个类别(从 0 到 9 的 10 个数字,“上”,“下”,“左”,“右”,“前进”,“停止”,“是”,“否”,以及“未知”词和背景噪音的类别)。正是这些数据集本质上的相似性使得卷积神经网络非常适用于声音命令识别任务。

但是这两个数据集也有一些显著的区别。首先,声音命令数据集中的音频录音有些噪音,可以从 图 4.12 中的示例谱图中看到不属于语音声音的黑色像素点。其次,声音命令数据集中的每个谱图的尺寸为 43×232,与单个 MNIST 图像的 28×28 大小相比显著较大。谱图的尺寸在时间和频率维度之间是不对称的。这些差异将体现在我们将在音频数据集上使用的卷积神经网络中。

定义和训练声音命令卷积神经网络的代码位于 tfjs-models 存储库中。您可以使用以下命令访问代码:

git clone https://github.com/tensorflow/tfjs-models.git
cd speech-commands/training/browser-fft

模型的创建和编译封装在 model.ts 中的createModel()函数中。

4.8 章节的声音命令谱图分类的卷积神经网络
function createModel(inputShape: tf.Shape, numClasses: number) {
  const model = tf.sequential();
  model.add(tf.layers.conv2d({                                           ***1***
    filters: 8,
    kernelSize: [2, 8],
    activation: 'relu',
    inputShape
  }));
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
  model.add(      tf.layers.conv2d({
        filters: 32,
        kernelSize: [2, 4],
        activation: 'relu'
      }));
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
  model.add(
      tf.layers.conv2d({
        filters: 32,
        kernelSize: [2, 4],
        activation: 'relu'
      }));
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
  model.add(
      tf.layers.conv2d({
        filters: 32,
        kernelSize: [2, 4],
        activation: 'relu'
      }));
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [1, 2]}));
  model.add(tf.layers.flatten());                                        ***2***
  model.add(tf.layers.dropout({rate: 0.25}));                            ***3***
  model.add(tf.layers.dense({units: 2000, activation: 'relu'}));
  model.add(tf.layers.dropout({rate: 0.5}));
  model.add(tf.layers.dense({units: numClasses, activation: 'softmax'}));
  model.compile({                                                        ***4***
    loss: 'categoricalCrossentropy',
    optimizer: tf.train.sgd(0.01),
    metrics: ['accuracy']
  });
  model.summary();
  return model;
}
  • 1 conv2d+maxPooling2d 的重复模式
  • 2 多层感知器开始
  • 3 使用 dropout 减少过拟合
  • 4 配置多类别分类的损失和指标

我们的音频卷积网络的拓扑结构看起来很像 MNIST 卷积网络。顺序模型以多个重复的 conv2d 层与 maxPooling2d 层组合开始。模型的卷积 - 池化部分在一个展平层结束,在其上添加了 MLP。MLP 有两个密集层。隐藏的密集层具有 relu 激活,最终(输出)层具有适合分类任务的 softmax 激活。模型编译为在训练和评估期间使用 categoricalCrossentropy 作为损失函数并发出准确度指标。这与 MNIST 卷积网络完全相同,因为两个数据集都涉及多类别分类。音频卷积网络还显示出与 MNIST 不同的一些有趣之处。特别是,conv2d 层的 kernelSize 属性是矩形的(例如,[2, 8])而不是方形的。这些值被选择为与频谱图的非方形形状匹配,该频谱图的频率维度比时间维度大。

要训练模型,首先需要下载语音命令数据集。该数据集源自谷歌 Brain 团队工程师 Pete Warden 收集的语音命令数据集(请参阅 www.tensorflow.org/tutorials/sequences/audio_recognition)。它已经转换为浏览器特定的频谱图格式:

curl -fSsL https://storage.googleapis.com/learnjs-data/speech-
     commands/speech-commands-data- v0.02-browser.tar.gz  -o speech-commands-
     data-v0.02-browser.tar.gz &&
tar xzvf speech-commands-data-v0.02-browser.tar.gz

这些命令将下载并提取语音命令数据集的浏览器版本。一旦数据被提取,您就可以使用以下命令启动训练过程:

yarn
yarn train \
    speech-commands-data-browser/ \
    /tmp/speech-commands-model/

yarn train 命令的第一个参数指向训练数据的位置。以下参数指定了模型的 JSON 文件将保存的路径,以及权重文件和元数据 JSON 文件的路径。就像我们训练增强的 MNIST 卷积网络时一样,音频卷积网络的训练也发生在 tfjs-node 中,有可能利用 GPU。由于数据集和模型的大小都比 MNIST 卷积网络大,训练时间会更长(大约几个小时)。如果您有 CUDA GPU 并且稍微更改命令以使用 tfjs-node-gpu 而不是默认的 tfjs-node(仅在 CPU 上运行),您可以显著加快训练速度。要做到这一点,只需在上一个命令中添加标志 --gpu

yarn train \
        --gpu \
        speech-commands-data-browser/ \
        /tmp/speech-commands-model/

当训练结束时,模型应该达到约 94% 的最终评估(测试)准确率。

训练过的模型保存在上一个命令中指定的路径中。与我们用 tfjs-node 训练的 MNIST 卷积网络一样,保存的模型可以在浏览器中加载以提供服务。然而,您需要熟悉 WebAudio API,以便能够从麦克风获取数据并将其预处理为模型可用的格式。为了方便起见,我们编写了一个封装类,不仅可以加载经过训练的音频卷积网络,还可以处理数据输入和预处理。如果您对音频数据输入流水线的机制感兴趣,可以在 tfjs-model Git 仓库中的 speech-commands/src 文件夹中研究底层代码。这个封装类可以通过 npm 的 @tensorflow-models/speech-commands 名称使用。Listing 4.9 展示了如何使用封装类在浏览器中进行在线语音命令识别的最小示例。

在 tfjs-models 仓库的 speech-commands/demo 文件夹中,您可以找到一个相对完整的示例,该示例展示了如何使用该软件包。要克隆并运行该演示,请在 speech-commands 目录下运行以下命令:

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

yarn watch 命令将自动在默认的网页浏览器中打开一个新的标签页。要看到语音命令识别器的实际效果,请确保您的计算机已准备好麦克风(大多数笔记本电脑都有)。每次识别到词汇表中的一个单词时,屏幕上将显示该单词以及包含该单词的一秒钟的频谱图。所以,这是基于浏览器的单词识别,由 WebAudio API 和深度卷积网络驱动。当然,它没有能力识别带有语法的连接语音?这将需要其他类型的能够处理序列信息的神经网络模块的帮助。我们将在第八章中介绍这些模块。

Listing 4.9. @tensorflow-models/speech-commands 模块的示例用法
import * as SpeechCommands from
    '@tensorflow-models/speech-commands';               ***1***
const recognizer =
    SpeechCommands.create('BROWSER_FFT');               ***2***
console.log(recognizer.wordLabels());                   ***3***
recognizer.listen(result => {                           ***4***
  let maxIndex;
  let maxScore = -Infinity;
  result.scores.forEach((score, i) => {                 ***5***
    if (score > maxScore) {                             ***6***
      maxIndex = i;
      maxScore = score;
    }
  });
  console.log(`Detected word ${recognizer.wordLabels()[maxIndex]}`);
}, {
  probabilityThreshold: 0.75
});
setTimeout(() => recognizer.stopStreaming(), 10e3);     ***7***
  • 1 导入 speech-commands 模块。确保它在 package.json 中列为依赖项。
  • 2 创建一个使用浏览器内置快速傅里叶变换(FFT)的语音命令识别器实例
  • 3 您可以查看模型能够识别的单词标签(包括“background-noise”和“unknown”标签)。
  • 4 启动在线流式识别。第一个参数是一个回调函数,每当识别到概率超过阈值(本例为 0.75)的非背景噪声、非未知单词时,都会调用该函数。
  • 5 result.scores 包含与 recognizer.wordLabels() 对应的概率得分。
  • 6 找到具有最高得分的单词的索引
  • 7 在 10 秒内停止在线流式识别

练习

  1. 用于在浏览器中对 MNIST 图像进行分类的卷积网络(listing 4.1)有两组 conv2d 和 maxPooling2d 层。修改代码,将该数量减少到只有一组。回答以下问题:
  1. 这会影响卷积网络可训练参数的总数吗?
  2. 这会影响训练速度吗?
  3. 这会影响训练后卷积网络获得的最终准确率吗?
  1. 这个练习与练习 1 相似。但是,与其调整 conv2d-maxPooling2d 层组的数量,不如在卷积网络的 MLP 部分中尝试调整密集层的数量。如果去除第一个密集层,只保留第二个(输出)层,总参数数量、训练速度和最终准确率会发生什么变化,见列表 4.1。
  2. 从 mnist-node 中的卷积网络(列表 4.5)中移除 dropout,并观察训练过程和最终测试准确率的变化。为什么会发生这种情况?这说明了什么?
  3. 使用tf.browser.fromPixels()方法练习从网页中图像和视频相关元素中提取图像数据,尝试以下操作:
  1. 使用tf.browser.fromPixels()通过img标签获取表示彩色 JPG 图像的张量。
  • tf.browser.fromPixels()返回的图像张量的高度和宽度是多少?是什么决定了高度和宽度?
  • 使用tf.image.resizeBilinear()将图像调整为固定尺寸 100 × 100(高 × 宽)。
  • 重复上一步,但使用替代的调整大小函数tf.image.resizeNearestNeighbor()。你能发现这两种调整大小函数的结果之间有什么区别吗?
  1. 创建一个 HTML 画布并在其中绘制一些任意形状,例如使用rect()函数。或者,如果愿意,也可以使用更先进的库,如 d3.js 或 three.js,在其中绘制更复杂的 2D 和 3D 形状。然后,使用tf.browser.fromPixels()从画布获取图像张量数据。

摘要

  • 卷积网络通过堆叠的 conv2d 和 maxPooling2d 层的层次结构从输入图像中提取 2D 空间特征。
  • conv2d 层是多通道、可调节的空间滤波器。它们具有局部性和参数共享的特性,使它们成为强大的特征提取器和高效的表示转换器。
  • maxPooling2d 层通过在固定大小的窗口内计算最大值来减小输入图像张量的大小,从而实现更好的位置不变性。
  • 卷积网络的 conv2d-maxPooling2d“塔”通常以一个 flatten 层结束,其后是由密集层组成的 MLP,用于分类或回归任务。
  • 受限于其资源,浏览器只适用于训练小型模型。要训练更大的模型,建议使用 tfjs-node,即 TensorFlow.js 的 Node.js 版本;tfjs-node 可以使用与 Python 版本的 TensorFlow 相同的 CPU 和 GPU 并行化内核。
  • 模型容量增加会增加过拟合的风险。通过在卷积网络中添加 dropout 层可以缓解过拟合。在训练期间,dropout 层会随机将给定比例的输入元素归零。
  • Convnets 不仅对计算机视觉任务有用。当音频信号被表示为频谱图时,卷积神经网络也可以应用于它们,以实现良好的分类准确性。

第五章:转移学习:重用预训练的神经网络

本章涵盖

  • 什么是转移学习,为什么它对许多类型的问题而言比从头开始训练模型更好
  • 如何通过将其从 Keras 转换为 TensorFlow.js 来利用最先进的预训练卷积神经网络的特征提取能力
  • 转移学习技术的详细机制,包括层冻结、创建新的转移头和微调
  • 如何使用转移学习在 TensorFlow.js 中训练简单的目标检测模型

在第四章中,我们看到了如何训练卷积神经网络来对图像进行分类。现在考虑以下情景。我们用于分类手写数字的卷积神经网络对某位用户表现不佳,因为他们的手写与原始训练数据非常不同。我们能否通过使用我们可以从他们那里收集到的少量数据(比如,50 个样本)来改进模型,从而更好地为用户提供服务?再考虑另一种情况:一个电子商务网站希望自动分类用户上传的商品图片。但是公开可用的卷积神经网络(例如 MobileNet^([1]))都没有针对这种特定领域的图像进行训练。在给定少量标记图片(比如,几百张)的情况下,是否可以使用公开可用的图像模型来解决定制分类问题?

¹

Andrew G. Howard 等人,“MobileNets: 面向移动视觉应用的高效卷积神经网络”,2017 年 4 月 17 日提交,arxiv.org/abs/1704.04861

幸运的是,本章的主要焦点——一种称为转移学习的技术,可以帮助解决这类任务。

5.1. 转移学习简介:重用预训练模型

本质上,转移学习是通过重用先前学习结果来加速新的学习任务。它涉及使用已经在数据集上训练过的模型来执行不同但相关的机器学习任务。已经训练好的模型被称为基础模型。转移学习有时涉及重新训练基础模型,有时涉及在基础模型的顶部创建一个新模型。我们将新模型称为转移模型。正如图 5.1 所示,用于这个重新训练过程的数据量通常比用于训练基础模型的数据量要小得多(就像本章开头给出的两个例子一样)。因此,与基础模型的训练过程相比,转移学习通常需要的时间和资源要少得多。这使得在像浏览器这样的资源受限环境中使用 TensorFlow.js 进行转移学习成为可能。因此,对于 TensorFlow.js 学习者来说,转移学习是一个重要的主题。

图 5.1. 迁移学习的一般工作流程。大型数据集用于基础模型的训练。这个初始训练过程通常很长且计算量大。然后重新训练基础模型,可能成为新模型的一部分。重新训练过程通常涉及比原始数据集小得多的数据集。重新训练所涉及的计算量远远小于初始训练,并且可以在边缘设备上进行,例如运行 TensorFlow.js 的笔记本电脑或手机。


描述迁移学习中的关键短语“不同但相关”在不同情况下可能意味着不同的事情:

  • 本章开头提到的第一个场景涉及将模型调整为特定用户的数据。尽管数据与原始训练集不同,但任务完全相同——将图像分类为 10 个数字。这种类型的迁移学习被称为模型适应
  • 其他迁移学习问题涉及与原始标签不同的目标。本章开头提到的商品图像分类场景属于这一类别。

与从头开始训练新模型相比,迁移学习的优势是什么?答案有两个方面:

  • 从数据量和计算量两个方面来看,迁移学习更加高效。
  • 它借助基础模型的特征提取能力,建立在先前训练成果的基础上。

这些观点适用于各种类型的问题(例如分类和回归)。在第一个观点上,迁移学习使用来自基础模型(或其子集)的训练权重。因此,与从头开始训练新模型相比,它需要更少的训练数据和训练时间才能收敛到给定精度水平。在这方面,迁移学习类似于人类学习新任务的方式:一旦你掌握了一个任务(例如玩纸牌游戏),学习类似的任务(例如玩类似的纸牌游戏)在将来会变得更容易和更快。对于我们为 MNIST 构建的 convnet 这样的神经网络来说,节省的训练时间成本可能相对较小。然而,对于在大型数据集上训练的较大模型(例如在 TB 级图像数据上训练的工业规模 convnet),节省可以是相当可观的。

关于第二点,迁移学习的核心思想是重用之前的训练结果。通过从一个非常大的数据集中学习,原始神经网络已经非常擅长从原始输入数据中提取有用的特征。只要迁移学习任务中的新数据与原始数据不相差太大,这些特征对于新任务将是有用的。研究人员已经为常见的机器学习领域组装了非常大的数据集。在计算机视觉领域,有 ImageNet^([2]),其中包含大约一千个类别的数百万张带标签的图像。深度学习研究人员已经使用 ImageNet 数据集训练了深度卷积神经网络,包括 ResNet、Inception 和 MobileNet(我们将很快接触到的最后一个)。由于 ImageNet 中图像的数量和多样性,训练在其上的卷积神经网络是一般类型图像的良好特征提取器。这些特征提取器对于处理像前述情景中的小数据集这样的小数据集将是有用的,但是使用小数据集训练这样有效的特征提取器是不可能的。迁移学习的机会也存在于其他领域。例如,在自然语言处理领域,人们已经在包含数十亿个单词的大型文本语料库上训练了词嵌入(即语言中所有常见单词的向量表示)。这些嵌入对于可用的远小于大文本数据集的语言理解任务是有用的。话不多说,让我们通过一个例子来看看迁移学习是如何在实践中工作的。

²

不要被名字所迷惑。“ImageNet”指的是一个数据集,而不是一个神经网络。

5.1.1. 基于兼容输出形状的迁移学习:冻结层

让我们从一个相对简单的例子开始。我们将在 MNIST 数据集的前五个数字(0 到 4)上训练一个卷积神经网络。然后,我们将使用得到的模型来识别剩余的五个数字(5 到 9),这些数字在原始训练中模型从未见过。虽然这个例子有些人为,但它展示了迁移学习的基本工作流程。你可以通过以下命令查看并运行这个例子:

git clone https://github.com/tensorflow/tfjs-examples.git
cd tfjs-examples/mnist-transfer-cnn
yarn && yarn watch

在打开的演示网页中,通过点击“重新训练”按钮来开始迁移学习过程。你可以看到这个过程在新的五个数字(5 到 9)上达到了约 96%的准确率,这需要在一台性能较强的笔记本电脑上大约 30 秒。正如我们将要展示的,这比不使用迁移学习的替代方案(即从头开始训练一个新模型)要快得多。让我们逐步看看这是如何实现的。

我们的例子从 HTTP 服务器加载预训练的基础模型,而不是从头开始训练,以免混淆工作流程的关键部分。回想一下第 4.3.3 节,TensorFlow.js 提供了tf.loadLayersModel()方法来加载预训练模型。这在 loader.js 文件中调用:

const model = await tf.loadLayersModel(url);
model.summary();

模型的打印摘要看起来像 图 5.2。正如你所见,该模型由 12 层组成。^([3]) 其中的大约 600,000 个权重参数都是可训练的,就像迄今为止我们见过的所有 TensorFlow.js 模型一样。请注意,loadLayersModel() 不仅加载模型的拓扑结构,还加载所有权重值。因此,加载的模型已经准备好预测数字 0 到 4 的类别。但是,这不是我们将使用模型的方式。相反,我们将训练模型来识别新的数字(5 到 9)。

³

在这个模型中,你可能没有看到激活层类型。激活层是仅对输入执行激活函数(如 relu 和 softmax)的简单层。假设你有一个具有默认(线性)激活的稠密层;在其上叠加一个激活层等同于使用具有非默认激活的稠密层。这就是我们在 第四章 中的例子所做的。但是有时也会看到前一种风格。在 TensorFlow.js 中,你可以通过以下代码获取这样的模型拓扑:const model = tf.sequential(); model.add(tf.layers.dense({untis: 5, `inputShape})); model.add(tf.layers.activation({activation: ‘relu’}))。

图 5.2. MNIST 图像识别和迁移学习卷积神经网络的打印摘要

查看回调函数以重新训练按钮(在 index.js 的 retrainModel() 函数中)时,如果选择了冻结特征层选项(默认情况下选择了),你会注意到一些代码行将模型的前七层的 trainable 属性设置为 false

那是做什么用的?默认情况下,模型加载后通过 loadLayersModel() 方法或从头创建后,模型的每个层的 trainable 属性都是 truetrainable 属性在训练期间(即调用 fit()fitDataset() 方法)中使用。它告诉优化器是否应该更新层的权重。默认情况下,模型的所有层的权重都在训练期间更新。但是,如果你将某些模型层的属性设置为 false,那么这些层的权重在训练期间将不会更新。在 TensorFlow.js 的术语中,这些层变为不可训练冻结。列表 5.1 中的代码冻结了模型的前七层,从输入 conv2d 层到 flatten 层,同时保持最后几层(稠密层)可训练。

列表 5.1. 冻结卷积网络的前几层以进行迁移学习
const trainingMode = ui.getTrainingMode();
    if (trainingMode === 'freeze-feature-layers') {
      console.log('Freezing feature layers of the model.');
      for (let i = 0; i < 7; ++i) {
        this.model.layers[i].trainable = false;               ***1***
      }
    } else if (trainingMode === 'reinitialize-weights') {
      const returnString = false ;
      this.model = await tf.models.modelFromJSON({            ***2***
        modelTopology: this.model.toJSON(null, returnString)  ***2***
      });                                                     ***2***
    }
    this.model.compile({                                      ***3***
      loss: 'categoricalCrossentropy',
      optimizer: tf.train.adam(0.01),
      metrics: ['acc'],
    });
    this.model.summary();                                     ***4***
  • 1 冻结层
  • 2 创建一个与旧模型具有相同拓扑结构但重新初始化权重值的新模型
  • 3 冻结将不会在调用 fit() 时生效,除非你首先编译模型。
  • 4 在 compile() 后再次打印模型摘要。您应该看到模型的一些权重已变为不可训练。

然而,仅设置层的 trainable 属性是不够的:如果您只修改 trainable 属性并立即调用模型的 fit() 方法,您将看到这些层的权重在 fit() 调用期间仍然会被更新。在调用 Model.fit() 之前,您需要调用 Model.compile() 以使 trainable 属性更改生效,就像在 列表 5.1 中所做的那样。我们之前提到 compile() 调用配置了优化器、损失函数和指标。但是,该方法还允许模型在这些调用期间刷新要更新的权重变量列表。在 compile() 调用之后,我们再次调用 summary() 来打印模型的新摘要。通过将新摘要与 图 5.2 中的旧摘要进行比较,您会发现一些模型的权重已变为不可训练:

Total params: 600165
Trainable params: 590597
Non-trainable params: 9568

您可以验证非可训练参数的数量,即 9,568,是两个冻结层中的权重参数之和(两个 conv2d 层的权重)。请注意,我们已冻结的一些层不包含权重(例如 maxPooling2d 层和 flatten 层),因此当它们被冻结时不会对非可训练参数的计数产生贡献。

实际的迁移学习代码显示在 列表 5.2 中。在这里,我们使用了与从头开始训练模型相同的 fit() 方法。在此调用中,我们使用 validationData 字段来衡量模型在训练期间未见过的数据上的准确性。此外,我们将两个回调连接到 fit() 调用,一个用于在用户界面中更新进度条,另一个用于使用 tfjs-vis 模块绘制损失和准确率曲线(更多细节请参见 第七章)。这显示了 fit() API 的一个方面,我们之前没有提到过:您可以给 fit() 调用一个回调或一个包含多个回调的数组。在后一种情况下,所有回调将在训练期间被调用(按照数组中指定的顺序)。

列表 5.2 使用 Model.fit() 进行迁移学习
await this.model.fit(this.gte5TrainData.x, this.gte5TrainData.y, {
      batchSize: batchSize,
      epochs: epochs,
      validationData: [this.gte5TestData.x, this.gte5TestData.y],
      callbacks: [                                                        ***1***
        ui.getProgressBarCallbackConfig(epochs),
        tfVis.show.fitCallbacks(surfaceInfo, ['val_loss', 'val_acc'], {   ***2***
          zoomToFit: true,                                                ***2***
          zoomToFitAccuracy: true,                                        ***2***
          height: 200,                                                    ***2***
          callbacks: ['onEpochEnd'],                                      ***2***
        }),                                                               ***2***
      ]
    });
  • 1fit() 调用添加多个回调是允许的。
  • 2 使用 tfjs-vis 绘制迁移学习过程中的验证损失和准确率

迁移学习的结果如何?正如您在图 5.3 的 A 面板中所看到的,经过 10 个 epoch 的训练后,准确率达到约 0.968,大约需要在一台相对更新的笔记本电脑上花费约 15 秒,还算不错。但与从头开始训练模型相比如何呢?我们可以通过一个实验演示从预训练模型开始相对于从头开始的价值,即在调用 fit() 前随机重新初始化预训练模型的权重。在点击重新训练按钮之前,在训练模式下拉菜单中选择重新初始化权重选项即可。结果显示在同一图表的 B 面板中。

图 5.3. 在 MNIST 卷积网络上的迁移学习的损失和验证曲线。面板 A:使用预训练模型的前七层冻结得到的曲线。面板 B:使用模型的所有权重随机重新初始化得到的曲线。面板 C:不冻结任何预训练模型层获得的曲线。请注意三个面板之间的 y 轴有所不同。面板 D:一个多系列图,显示了面板 A–C 中的损失和准确度曲线在相同轴上以便进行比较。

通过比较 B 面板和 A 面板,可以看出模型权重的随机重新初始化导致损失从一个显著更高的值开始(0.36 对比 0.30),准确度则从一个显著更低的值开始(0.88 对比 0.91)。重新初始化的模型最终的验证准确率也比重复使用从基本模型中的权重的模型低(约 0.954 对比 ~0.968)。这些差异反映了迁移学习的优势:通过重复使用模型的初始层(特征提取层)中的权重,相对于从头开始学习,模型获得了一个良好的起步。这是因为迁移学习任务中遇到的数据与用于训练原始模型的数据相似。数字 5 到 9 的图像与数字 0 到 4 的图像有很多共同点:它们都是带有黑色背景的灰度图像;它们有类似的视觉模式(相似宽度和曲率的笔画)。因此,模型从数字 0 到 4 中学习提取的特征对学习分类新数字(5 到 9)也很有用。

如果我们不冻结特征层的权重会怎样?在训练模式下拉菜单中选择不冻结特征层选项可以进行此实验。结果显示在图 5.3 的 C 面板中。与 A 面板的结果相比,有几个值得注意的差异:

  • 没有特征层冻结时,损失值开始较高(例如,在第一个时期之后:0.37 对比 0.27);准确率开始较低(0.87 对比 0.91)。为什么会这样?当预训练模型首次开始在新数据集上进行训练时,预测结果将包含大量错误,因为预训练权重为五个新数字生成基本上是随机的预测。因此,损失函数将具有非常高的值和陡峭的斜率。这导致在训练的早期阶段计算的梯度非常大,进而导致所有模型的权重出现大幅波动。因此,所有层的权重都将经历一个大幅波动的时期,这导致面板 C 中看到的初始损失较高。在正常的迁移学习方法(面板 A)中,模型的前几层被冻结,因此免受这些大的初始权重扰动的影响。
  • 由于这些大的初始扰动,采用无冻结方法达到的最终准确率(约为 0.945,面板 C)与采用正常的迁移学习方法相比(约为 0.968,面板 A)并没有明显提高。
  • 当模型的任何一层都没有被冻结时,训练时间会更长。例如,在我们使用的其中一台笔记本电脑上,使用冻结特征层训练模型大约需要 30 秒,而没有任何层冻结的模型训练大约需要两倍长(60 秒)。图 5.4 以示意的方式说明了其中的原因。在反向传播期间,冻结的层被从方程中排除,这导致每个fit()调用的批次速度大大加快。
图 5.4. 模型冻结某些层加快训练速度的示意图。在该图中,通过指向左边的黑色箭头显示了反向传播路径。面板 A:当没有任何层被冻结时,所有模型的权重(v[1]–v[5])在每个训练步骤(每个批次)中都需要更新,因此将参与反向传播,由黑色箭头表示。请注意,特征(x)和目标(y)永远不会包括在反向传播中,因为它们的值不需要更新。面板 B:通过冻结模型的前几层,一部分权重(v[1]–v[3])不再是反向传播的一部分。相反,它们变成了类似于 x 和 y 的常数,只是作为影响损失计算的因素。因此,执行反向传播所需的计算量减少,训练速度提高。

这些观点为迁移学习的层冻结方法提供了理由:它利用了基础模型的特征提取层,并在新训练的早期阶段保护它们免受大的权重扰动,从而在较短的训练周期内实现更高的准确性。

在我们继续下一节之前,有两点需要注意。首先,模型适应——重新训练模型以使其在特定用户的输入数据上更有效的过程——使用的技术与此处展示的技术非常相似,即冻结基础层,同时让顶层的权重通过对用户特定数据的训练而发生变化。尽管本节解决的问题并不涉及来自不同用户的数据,而是涉及具有不同标签的数据。其次,你可能想知道如何验证冻结层(在这种情况下是 conv2d 层)的权重在fit()调用之前和之后是否确实相同。这个验证并不是很难做到的。我们把它留给你作为一个练习(参见本章末尾的练习 2)。

5.1.2. 不兼容输出形状上的迁移学习:使用基础模型的输出创建一个新的 m 模型

在前一节中看到的迁移学习示例中,基础模型的输出形状与新输出形状相同。这种属性在许多其他迁移学习案例中并不成立(参见图 5.5)。例如,如果你想要使用最初在五个数字上进行训练的基础模型来对四个新数字进行分类,先前描述的方法将不起作用。更常见的情况是:给定一个已经在包含 1,000 个输出类别的 ImageNet 分类数据集上训练过的深度卷积网络,你手头有一个涉及更少输出类别的图像分类任务(图 5.5 中的 B 案例)。也许这是一个二元分类问题——图像是否包含人脸——或者这是一个具有少数类别的多类分类问题——图片中包含什么类型的商品(回想一下本章开头的例子)。在这种情况下,基础模型的输出形状对于新问题不起作用。

图 5.5. 根据新模型的输出形状和激活方式是否与原模型相同,迁移学习可分为三种类型。情况 A:新模型的输出形状和激活函数与基础模型相匹配。将 MNIST 模型迁移到 5.1.1 节中的新数字就是这种类型的迁移学习示例。情况 B:新模型具有与基础模型相同的激活类型,因为原任务和新任务是相同类型的(例如,都是多类分类)。然而,输出形状不同(例如,新任务涉及不同数量的类)。这种类型的迁移学习示例可在 5.1.2 节(通过网络摄像头控制类似于 Pac-Man^(TM 4) 的视频游戏)和 5.1.3(识别一组新的口语单词)中找到。情况 C:新任务与原始任务的类型不同(例如,回归与分类)。基于 MobileNet 的目标检测模型就是这种类型的示例。

在某些情况下,甚至机器学习任务的类型也与基础模型训练的类型不同。例如,您可以通过对分类任务训练的基础模型应用迁移学习来执行回归任务(预测一个数字,如图 5.5 中的情况 C)5.2 节中,您将看到迁移学习的更加有趣的用途——预测一系列数字,而不是单个数字,用于在图像中检测和定位对象。

这些情况都涉及期望的输出形状与基础模型不同。这使得需要构建一个新模型。但因为我们正在进行迁移学习,所以新模型不会从头开始创建。相反,它将使用基础模型。我们将在 tfjs-examples 存储库中的 webcam-transfer-learning 示例中说明如何做到这一点。

要查看此示例的实际操作,请确保您的设备具有前置摄像头——示例将从摄像头收集用于迁移学习的数据。现在大多数笔记本电脑和平板电脑都配备了内置的前置摄像头。但是,如果您使用的是台式电脑,可能需要找到一个网络摄像头并将其连接到设备上。与之前的示例类似,您可以使用以下命令来查看和运行演示:

git clone https://github.com/tensorflow/tfjs-examples.git
cd tfjs-examples/webcam-transfer-learning

这个有趣的演示将您的网络摄像头转换为游戏控制器,通过对 MobileNet 的 TensorFlow.js 实现进行迁移学习,让您可以用它玩 Pac-Man 游戏。让我们走过运行演示所需的三个步骤:数据收集、模型迁移学习和游戏进行^([4])。

Pac-Man 是万代南梦宫娱乐公司的商标。

迁移学习的数据来自于您的网络摄像头。一旦演示在您的浏览器中运行,您将在页面右下角看到四个黑色方块。它们的排列方式类似于任天堂家庭电脑控制器上的四个方向按钮。它们对应着模型将实时识别的四个类别。这四个类别对应着 Pac-Man 将要移动的四个方向。当您点击并按住其中一个时,图像将以每秒 20–30 帧的速度通过网络摄像头收集。方块下面的数字告诉您目前已经为此控制器方向收集了多少图像。

为了获得最佳的迁移学习质量,请确保您:1)每个类别至少收集 50 张图像;2)在数据收集过程中稍微移动和摆动您的头部和面部,以使训练图像包含更多的多样性,这有利于您从迁移学习中获得的模型的稳健性。在这个演示中,大多数人会在四个方向(上、下、左、右;参见图 5.6)转动头部,以指示 Pac-Man 应该朝哪个方向移动。但您可以使用任何您想要的头部位置、面部表情甚至手势作为输入图像,只要输入图像在各个类别之间足够视觉上有区别即可。

图 5.6. 网络摄像头迁移学习示例的用户界面^([5])

这个网络摄像头迁移学习示例的用户界面是由吉姆博·威尔逊(Jimbo Wilson)和山姆·卡特(Shan Carter)完成的。您可以在 youtu.be/YB-kfeNIPCE?t=941 查看这个有趣示例的视频录制。

收集完训练图像后,点击“训练模型”按钮,这将开始迁移学习过程。迁移学习应该只需要几秒钟。随着进展,您应该看到屏幕上显示的损失值变得越来越小,直到达到一个非常小的正值(例如 0.00010),然后停止变化。此时,迁移学习模型已经被训练好了,您可以用它来玩游戏了。要开始游戏,只需点击“播放”按钮,等待游戏状态稳定下来。然后,模型将开始对来自网络摄像头的图像流进行实时推理。在每个视频帧中,赢得的类别(由迁移学习模型分配的概率分数最高的类别)将在用户界面的右下角用明亮的黄色突出显示。此外,它会导致 Pac-Man 沿着相应的方向移动(除非被墙壁挡住)。

对于那些对机器学习不熟悉的人来说,这个演示可能看起来像魔术一样,但它基于的只是一个使用 MobileNet 执行四类分类任务的迁移学习算法。该算法使用通过网络摄像头收集的少量图像数据。这些图像通过您收集图像时执行的点击和按住操作方便地标记。由于迁移学习的力量,这个过程不需要太多数据或太多的训练时间(它甚至可以在智能手机上运行)。这就是这个演示的工作原理的简要概述。如果您希望了解技术细节,请在下一节中与我们一起深入研究底层的 TensorFlow.js 代码。

深入研究网络摄像头迁移学习

列表 5.3 中的代码(来自 webcam-transfer-learning/index.js)负责加载基础模型。特别地,我们加载了一个可以在 TensorFlow.js 中高效运行的 MobileNet 版本。信息框 5.1 描述了这个模型是如何从 Python 的 Keras 深度学习库转换而来的。一旦模型加载完成,我们使用 getLayer() 方法来获取其中一个层。getLayer() 允许您通过名称(在本例中为 'conv_pw_13_relu')指定一个层。您可能还记得另一种从 第 2.4.2 节 访问模型层的方法——即通过索引到模型的 layers 属性,该属性将所有模型的层作为 JavaScript 数组保存。当模型由少量层组成时,这种方法很容易使用。我们正在处理的 MobileNet 模型有 93 层,这使得这种方法变得脆弱(例如,如果将来向模型添加更多层会发生什么?)。因此,基于名称的 getLayer() 方法更可靠,如果我们假设 MobileNet 的作者在发布新版本模型时会保持关键层的名称不变的话。

列表 5.3. 加载 MobileNet 并从中创建一个“截断”模型
async function loadTruncatedMobileNet() {
      const mobilenet = await tf.loadLayersModel(                   ***1***
        'https://storage.googleapis.com/' +                         ***1***
            'tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json');   ***1***
      const layer = mobilenet.getLayer(                             ***2***
          'conv_pw_13_relu');                                       ***2***
      return tf.model({                                             ***3***
        inputs: mobilenet.inputs,                                   ***3***
        outputs: layer.output                                       ***3***
      });                                                           ***3***
    }
  • 1 storage.google.com/tfjs-models 下的 URL 设计为永久和稳定的。
  • 2 获取 MobileNet 的一个中间层。这个层包含对于自定义图像分类任务有用的特征。
  • 3 创建一个新模型,它与 MobileNet 相同,只是它在 ‘conv_pw_13_relu’ 层结束,也就是说,最后几层(称为“头部”)被截断

将 Python Keras 模型转换为 TensorFlow .js 格式

TensorFlow.js 具有与 Keras 高度兼容和互操作的特性,Keras 是最受欢迎的 Python 深度学习库之一。从这种兼容性中获益的其中一个好处是,你可以利用 Keras 中的许多所谓的“应用程序”。这些应用程序是一组在大型数据集(如 ImageNet)上预训练的深度卷积神经网络(详见 keras.io/applications/)。Keras 的作者们已经在库中辛苦地对这些卷积神经网络进行了训练,并使它们可通过库随时重用,包括推理和迁移学习,就像我们在这里所做的那样。对于在 Python 中使用 Keras 的人来说,导入一个应用程序只需一行代码。由于前面提到的互操作性,一个 TensorFlow.js 用户也很容易使用这些应用程序。以下是所需步骤:

  1. 确保已安装名为tensorflowjs的 Python 包。最简单的安装方法是通过pip命令:
pip install tensorflowjs
  1. 通过 Python 源文件或者像 ipython 这样的交互式 Python REPL 运行以下代码:
import keras
import tensorflowjs as tfjs
model = keras.applications.mobilenet.MobileNet(alpha=0.25)
tfjs.converters.save_keras_model(model, '/tmp/mobilnet_0.25')

前两行导入了所需的kerastensorflowjs模块。第三行将 MobileNet 加载到一个 Python 对象(model)中。实际上,你可以以几乎与打印 TensorFlow.js 模型摘要相同的方式打印模型的摘要:即model.summary()。你可以看到模型的最后一层(模型的输出)确实具有形状(None, 1000)(在 JavaScript 中相当于[null, 1000]),反映了 MobileNet 模型在 ImageNet 分类任务上训练的 1000 类。我们为这个构造函数调用指定的alpha=0.25关键字参数选择了一个更小的 MobileNet 版本。你可以选择更大的alpha值(如0.75, 1),同样的转换代码仍将继续工作。

前一代码片段中的最后一行使用了tensorflowjs模块中的一个方法,将模型保存到指定目录中。在该行运行结束后,将在磁盘上的/tmp/mobilenet_0.25 路径下创建一个新目录,其内容如下所示:

group1-shard1of6
        group1-shard2of6
        ...
        group1-shard6of6
        model.json

这与我们在第 4.3.3 节中看到的格式完全相同,当时我们展示了如何在 Node.js 版本的 TensorFlow.js 中使用其save()方法将训练好的 TensorFlow.js 模型保存到磁盘上。因此,对于从磁盘加载此转换模型的 TensorFlow.js 程序而言,保存的格式与在 TensorFlow.js 中创建和训练模型的格式是相同的:它可以简单地调用tf.loadLayersModel()方法并指向模型.json 文件的路径(无论是在浏览器中还是在 Node.js 中),这正是 listing 5.3 中发生的事情。

载入的 MobileNet 模型已经准备好执行模型最初训练的机器学习任务——将输入图像分类为 ImageNet 数据集的 1,000 个类别。请注意,该特定数据集非常强调动物,特别是各种品种的猫和狗(这可能与互联网上此类图像的丰富性有关!)。对于对此特定用法感兴趣的人,tfjs-example 仓库中的 MobileNet 示例展示了如何做到这一点(github.com/tensorflow/tfjs-examples/tree/master/mobilenet)。然而,在本章中,我们不专注于直接使用 MobileNet;相反,我们探讨如何使用载入的 MobileNet 进行迁移学习。

先前展示的 tfjs.converters.save_keras_model() 方法能够转换和保存不仅是 MobileNet 还有其他 Keras 应用,例如 DenseNet 和 NasNet。在本章末尾的练习 3 中,您将练习将另一个 Keras 应用(MobileNetV2)转换为 TensorFlow.js 格式并在浏览器中加载它。此外,应指出 tfjs.converters.save_keras_model() 通常适用于您在 Keras 中创建或训练的任何模型对象,而不仅仅是来自 keras.applications 的模型。

一旦获得 conv_pw_13_relu 层,我们该怎么做?我们创建一个包含原始 MobiletNet 模型层的新模型,从其第一(输入)层到 conv_pw_13_relu 层。这是本书中首次看到这种模型构建方式,因此需要一些仔细的解释。为此,我们首先需要介绍符号张量的概念。

创建符号张量模型

到目前为止,您已经看到了张量。Tensor 是 TensorFlow.js 中的基本数据类型(也缩写为 dtype)。一个张量对象携带着给定形状和 dtype 的具体数值,支持由 WebGL 纹理(如果在启用 WebGL 的浏览器中)或 CPU/GPU 内存(如果在 Node.js 中)支持的存储。然而,SymbolicTensor 是 TensorFlow.js 中另一个重要的类。与携带具体值不同,符号张量仅指定形状和 dtype。可以将符号张量视为“槽”或“占位符”,可以稍后插入一个实际张量值,前提是张量值具有兼容的形状和 dtype。在 TensorFlow.js 中,层或模型对象接受一个或多个输入(到目前为止,您只看到了一个输入的情况),这些输入被表示为一个或多个符号张量。

让我们使用一个类比来帮助你理解符号张量。想象一下编程语言(比如 Java 或 TypeScript,或者其他你熟悉的静态类型语言)中的函数。函数接受一个或多个输入参数。函数的每个参数都有一个类型,规定了可以作为参数传递的变量类型。然而,参数本身并不包含任何具体的值。参数本身只是一个占位符。符号张量类似于函数的参数:它指定了可以在该位置使用的张量的种类(形状和 dtype 的组合)。类似地,静态类型语言中的函数有一个返回类型。这与模型或层对象的输出符号张量相似。它是模型或层对象输出的实际张量值形状和 dtype 的“蓝图”。

张量形状和符号张量形状之间的区别在于前者始终具有完全指定的维度(比如 [8, 32, 20]),而后者可能具有未确定的维度(比如 [null, null, 20])。你已经在模型摘要的“输出形状”列中见过这一点。

在 TensorFlow.js 中,模型对象的两个重要属性是其输入和输出。这两者都是符号张量的数组。对于具有一个输入和一个输出的模型,这两个数组的长度都为 1。类似地,层对象具有两个属性:输入和输出,每个都是一个符号张量。符号张量可以用于创建新模型。这是 TensorFlow.js 中创建模型的新方法,与你之前见过的方法有所不同:即使用 tf.sequential() 创建顺序模型,然后调用 add() 方法。在新方法中,我们使用 tf.model() 函数,它接受一个包含两个必填字段 inputsoutputs 的配置对象。inputs 字段需要是一个符号张量(或者是一个符号张量数组),outputs 亦然。因此,我们可以从原始 MobileNet 模型中获取符号张量,并将它们提供给 tf.model() 调用。结果是一个由原始 MobileNet 的一部分组成的新模型。

这个过程在 图 5.7 中以示意图形式说明。(请注意,为了简单的图示,该图将实际 MobileNet 模型的层数减少了。)重要的是要意识到,从原始模型中提取的符号张量并不是孤立的对象。相反,它们携带关于它们属于哪些层以及层如何相互连接的信息。对于熟悉数据结构中图的读者来说,原始模型是一个符号张量的图,连接边是层。通过在原始模型中指定新模型的输入和输出为符号张量,我们正在提取原始 MobileNet 图的一个子图。这个子图成为新模型,包含 MobileNet 的前几层(特别是前 87 层),而最后 6 层则被略过。深度卷积网络的最后几层有时被称为头部。我们在 tf.model() 调用中所做的可以称为截断模型。截断的 MobileNet 保留了提取特征的层,同时丢弃了头部。为什么头部包含层?这是因为这些层是 MobileNet 最初训练的 1,000 类分类任务所特有的。这些层对我们面对的四类分类任务没有用处。

图 5.7. 示意图解释了如何从 MobileNet 创建新的(“截断的”)模型。在 代码清单 5.3 中的 tf.model() 调用中查看相应的代码。每一层都有一个输入和一个输出,都是 SymbolicTensor 实例。在原始模型中,SymbolicTensor0 是第一层的输入,也是整个模型的输入。它被用作新模型的输入符号张量。此外,我们将中间层的输出符号张量(相当于 conv_pw_13_relu)作为新模型的输出张量。因此,我们得到一个由原始模型的前两层组成的模型,如图的底部所示。原始模型的最后一层,即输出层,有时被称为模型的头部,被丢弃。这就是为什么有时会将这样的方法称为截断模型的原因。请注意,这个图示了具有少量层的模型,以便清楚地表达。实际上,在 代码清单 5.3 中的代码涉及一个比这个图示的层多得多(93 层)的模型。

基于嵌入的迁移学习

截断的 MobileNet 的输出是原始 MobileNet 的中间层激活。但是 MobileNet 的中间层激活对我们有何用呢?答案可以在处理每个四个黑色方块的点击和保持事件的函数中看到(列表 5.4)每当摄像头可用的时候(通过 capture() 方法),我们调用截断的 MobileNet 的 predict() 方法,并将输出保存在一个名为 controllerDataset 的对象中,稍后将用于迁移学习。

有关 TensorFlow.js 模型的常见问题是如何获取中间层的激活。我们展示的方法就是答案。

但是如何解释截断的 MobileNet 的输出?对于每个图像输入,它都是一个形状为 [1, 7, 7, 256] 的张量。它不是任何分类问题的概率,也不是任何回归问题的预测值。它是输入图像在某个高维空间中的表示。该空间具有 7 * 7 * 256,约为 12.5k,维度。尽管空间具有很多维度,但与原始图像相比,它是低维的,原始图像由于具有 224 × 224 的图像尺寸和三个颜色通道,有 224 * 224 * 3 ≈ 150k 个维度。因此,截断的 MobileNet 的输出可以被视为图像的有效表示。这种输入的低维表示通常称为嵌入。我们的迁移学习将基于从网络摄像头收集到的四组图像的嵌入。

列表 5.4. 使用截断的 MobileNet 获取图像嵌入
ui.setExampleHandler(label => {
  tf.tidy(() => {                         ***1***
    const img = webcam.capture();
    controllerDataset.addExample(
        truncatedMobileNet.predict(img),  ***2***
        label);
    ui.drawThumb(img, label);
  });
});
  • 1 使用 tf.tidy() 来清理中间张量,比如 img。有关在浏览器中使用 TensorFlow.js 内存管理的教程,请参见附录 B,第 B.3 节。
  • 2 获取 MobileNet 的输入图像的内部激活

现在我们有了获取网络摄像头图像嵌入的方法,我们如何使用它们来预测给定图像对应的方向呢?为此,我们需要一个新模型,该模型以嵌入作为其输入,并输出四个方向类的概率值。以下代码(来自 index.js)创建了这样一个模型。

列表 5.5. 使用图像嵌入预测控制器方向
model = tf.sequential({
    layers: [
      tf.layers.flatten({                                          ***1***
        inputShape: truncatedMobileNet.outputs[0].shape.slice(1)   ***1***
      }),                                                          ***1***
      tf.layers.dense({                                            ***2***
        units: ui.getDenseUnits(),                                 ***2***
        activation: 'relu',                                        ***2***
        kernelInitializer: 'varianceScaling',                      ***2***
        useBias: true                                              ***2***
      }),                                                          ***2***
      tf.layers.dense({                                            ***3***
        units: NUM_CLASSES,                                        ***3***
        kernelInitializer: 'varianceScaling',                      ***3***
        useBias: false,                                            ***3***
        activation: 'softmax'                                      ***3***
      })                                                           ***3***
    ]
  });
  • 1 将截断的 MobileNet 的[7, 7, 256]嵌入层展平。slice(1) 操作丢弃了第一个(批次)维度,该维度存在于输出形状中,但是不需要在层的工厂方法的 inputShape 属性中,因此它可以与密集层一起使用。
  • 2 一个具有非线性(relu)激活的第一个(隐藏的)密集层
  • 3 最后一层的单元数应该与我们想要预测的类的数量相对应。

与 MobileNet 截断版相比,清单 5.5 中创建的新模型具有更小的尺寸。它仅由三层组成:

  • 输入层是一个展平层。它将来自截断模型的 3D 嵌入转换为 1D 张量,以便后续的密集层可以采用。我们在 inputShape 中设置其与截断的 MobileNet 的输出形状匹配(不包括批处理维度),因为新模型将接收来自截断的 MobileNet 的嵌入。
  • 第二层是隐藏层。它是隐藏的,因为它既不是模型的输入层也不是输出层。相反,它被夹在其他两层之间,以增强模型的能力。这与第三章中遇到的 MLP 非常相似。它是一个带有 relu 激活的密集的隐藏层。回想一下,在第三章的“避免堆叠没有非线性的层的谬论”一节中,我们讨论了使用类似这样的隐藏层的非线性激活的重要性。
  • 第三层是新模型的最终(输出)层。它具有适合我们面临的多类分类问题的 softmax 激活(即,四个类别:每个 Pac-Man 方向一个)。

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

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