TensorFlow-CNN-与-Swift-教程-一-

96 阅读1小时+

TensorFlow CNN 与 Swift 教程(一)

原文:Convolutional Neural Networks with Swift for Tensorflow

协议:CC BY-NC-SA 4.0

一、MNIST:1D 神经网络

在本章中,我们将研究一个名为 MNIST 的简单图像识别数据集,并构建一个基本的一维神经网络,通常称为多层感知器,以对我们的数字进行分类,并对黑白图像进行分类。

数据集概述

MNIST(修正的国家标准和技术研究所)是 1999 年建立的数据集,是计算机视觉问题的一个极其重要的试验台。你会在这个领域的学术论文中随处看到它,它被认为是相当于 hello world 的计算机视觉。它是数字 0-9 的手绘数字的预处理灰度图像的集合。每幅图像宽 28×28 像素,总共 784 像素。对于每个像素,都有相应的 8 位灰度值,即从 0(白色)到 255(全黑)的数字。

首先,我们甚至不会把它当作实际的图像数据。我们将展开它——我们将从最上面一行开始,一次抽出每一行,直到我们得到一长串数字。我们可以想象将这个概念扩展到 28×28 像素,以产生一长行输入值,这是一个 784 像素长和 1 像素宽的向量,每个向量都有一个从 0 到 255 的对应值。

数据集已经过清理,因此没有很多非数字噪声(例如,灰白色背景)。这将使我们的工作更简单。如果您下载实际的数据集,您通常会以逗号分隔文件的形式获得它,每行对应一个条目。我们可以通过一次一个地反向赋值来将它转换成图像。实际数据集是 60000 个手绘训练位数,对应标签(实际数),10000 个* 测试 位数,对应 标签 *。数据集本身通常以 python pickle(一种存储字典的简单方法)文件的形式分发(您不需要知道这一点,只是以防您在网上遇到这种情况)。

因此,我们的目标是学习如何根据我们从训练数据集中学习到的模型* 正确猜测 测试 *数据集中的数字。这被称为监督学习* 任务,因为我们的目标是模仿另一个人(或模型)所做的。我们将简单地选取单个行,并尝试使用一种简单的称为**多层感知器 *的神经网络来猜测相应的数字。这通常是MLP的简称。

数据集处理程序

我们可以使用 Swift for Tensorflow 项目的一部分“swift-models”中的数据集加载器,以简化前面示例的处理。为了使以下代码生效,您将需要使用以下 swift package manager import 来自动将数据集添加到您的代码中。

基本:如果你是 swift 编程新手,只是想开始,只需使用在我们为 Tensorflow 设置 swift 的章节中使用的 swift-models checkout,并将以下代码(MLP 演示)放入 LeNet-MNIST 示例中的“main.swift”文件,然后运行“swift run LeNet-MNIST”。

高级:如果您已经是 swift 程序员,以下是我们将使用的基本 swift 模型导入文件:

/// swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package( name: "ConvolutionalNeuralNetworksWithSwiftForTensorFlow", platforms: [ .macOS(.v10_13), ], dependencies: [ .package( name: "swift-models", url: "github.com/tensorflow/…", .branch("master") ), ], targets: [ .target( name: "MNIST-1D", dependencies: [.product(name: "Datasets", package: "swift-models")], path: "MNIST-1D"), ] )

希望前面的代码不会太混乱。导入这个代码库会让我们的生活轻松很多。现在,让我们建立我们的第一个神经网络!

代码:多层感知器+ MNIST

让我们看一个非常简单的演示。将这段代码放入带有适当导入的“main.swift”文件中,我们将运行它:

/// 1 import Datasets import TensorFlow

// 2 struct MLP: Layer { var flatten = Flatten() var inputLayer = Dense(inputSize: 784, outputSize: 512, activation: relu) var hiddenLayer = Den se(inputSize: 512, outputSize: 512, activation: relu) var outputLayer = Dense(inputSize: 512, outputSize: 10)

@differentiable public func forward(_ input: Tensor) -> Tensor { return input.sequenced(through: flatten, inputLayer, hiddenLayer, outputLayer) } }

// 3 let batchSize = 128 let epochCount = 12 var model = MLP() let optimizer = SGD(for: model, learningRate: 0.1) let dataset = MNIST(batchSize: batchSize)

print("Starting training...")

for (epoch, epochBatches) in dataset.training.prefix(epochCount).enumerated() { // 4 Context.local.learningPhase = .training for batch in epochBatches { let (images, labels) = (batch.data, batch.label) let (_, gradients) = valueWithGradient(at: model) { model -> Tensor in let logits = model(images) return softmaxCrossEntropy(logits: logits, labels: labels) } optimizer.update(&model, along: gradients) }

// 5 Context.local.learningPhase = .inference var testLossSum: Float = 0 var testBatchCount = 0 var correctGuessCount = 0 var totalGuessCount = 0 for batch in dataset.validation { let (images, labels) = (batch.data, batch.label) let logits = model(images) testLossSum += softmaxCrossEntropy(logits: logits, labels: labels).scalarized() testBatchCount += 1

let correctPredictions = logits.argmax(squeezingAxis: 1) .== labels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.data.shape[0]

}

let accuracy = Float(correctGuessCount) / Float(totalGuessCount) print( """ [Epoch (epoch + 1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount)) """ ) }

结果

当您运行前面的代码时,您应该会得到如下所示的输出:

Loading resource: train-images-idx3-ubyte Loading resource: train-labels-idx1-ubyte Loading resource: t10k-images-idx3-ubyte Loading resource: t10k-labels-idx1-ubyte Starting training… [Epoch 1] Accuracy: 9364/10000 (0.9364) Loss: 0.21411717 [Epoch 2] Accuracy: 9547/10000 (0.9547) Loss: 0.15427242 [Epoch 3] Accuracy: 9630/10000 (0.963) Loss: 0.12323072 [Epoch 4] Accuracy: 9645/10000 (0.9645) Loss: 0.11413358 [Epoch 5] Accuracy: 9700/10000 (0.97) Loss: 0.094898805 [Epoch 6] Accuracy: 9747/10000 (0.9747) Loss: 0.0849531 [Epoch 7] Accuracy: 9757/10000 (0.9757) Loss: 0.076825164 [Epoch 8] Accuracy: 9735/10000 (0.9735) Loss: 0.082270846 [Epoch 9] Accuracy: 9782/10000 (0.97) Loss: 0.07173009 [Epoch 10] Accuracy: 9782/10000 (0.97) Loss: 0.06860765 [Epoch 11] Accuracy: 9779/10000 (0.9779) Loss: 0.06677916 [Epoch 12] Accuracy: 9794/10000 (0.9794) Loss: 0.063436724


恭喜你,你已经完成了机器学习!这个演示只有几行,但是实际上在幕后发生了很多事情。我们来分析一下这是怎么回事。

## 演示细分(高级别)

我们将使用注释中的编号(例如,//1,//2 等)逐段查看前面的所有代码。).我们将首先进行一遍尝试并解释在高层次上正在发生的事情,然后进行第二遍,在那里我们解释本质细节。

## 进口(1)

我们的前几行非常简单;我们正在导入 swift-models MNIST 数据集处理器,然后是 TensorFlow 库。

## 模型分解(2)

接下来,我们建立实际的神经网络,一个 MLP 模型:

/// 2
struct MLP: Layer {
  var flatten = Flatten<Float>()
  var inputLayer = Dense<Float>(inputSize: 784, outputSize: 512, activation: relu)
  var hiddenLayer = Dense<Float>(inputSize: 512, outputSize: 512, activation: relu)
  var outputLayer = Dense<Float>(inputSize: 512, outputSize: 10)

  @differentiable
  public func forward(_ input: Tensor<Float>) -> Tensor<Float> {
    return input.sequenced(through: flatten, inputLayer, hiddenLayer, outputLayer)
  }
}

这个数据结构里有什么?我们的第一行只是定义了一个名为 MLP 的新结构,它子类化了**Layer**,这是 swift 中 tensorflow 的一个类型。为了定义这个类,S4tf 实施了一个**协议**定义,我们实现了函数**forward**(以前的**callAsFunction**),它接受**输入**并将其映射到**输出* *。我们的中线实际上定义了我们感知机的层次:

    var flatten = Flatten<Float>()
    var inputLayer = Dense<Float>(inputSize: 784, outputSize: 512, activation: relu)
    var hiddenLayer = Dense<Float>(inputSize: 512, outputSize: 512, activation: relu)
    var outputLayer = Dense<Float>(inputSize: 512, outputSize: 10)

我们有四个内部层:

1.  扁平化操作:这只是接受输入,并将其简化为一行输入数字(一个向量)。

    我们的数据集在内部给我们一张 28x28 像素的图片,这只是将它转换成一行 784 像素长的数字。

    接下来,我们有三个**密集的**层,这是一种特殊类型的神经网络,称为* *全连接* *层。第一个从我们的初始输入(例如,展平的 784x1 向量)到 512 个节点,如下所示。

2.  密集层:784(前面的输入)到 512 个节点。

3.  另一个密集层:512 个节点再到 512 个节点。

4.  一个输出层:512 个节点到 10 个节点(位数,09)。

    最后是一个转发函数,这就是我们的神经网络逻辑神奇之处。我们实际上采取的输入,运行它通过展平,密度 1,密度 2 和输出层产生我们的结果。

所以我们的

return input.sequenced(through: flatten, inputLayer, hiddenLayer, outputLayer)


然后是实际接收输入并通过这四层进行映射的调用。接下来我们将查看实际的培训循环,以了解所有这些是如何实际发生的,但 swift 对于 tensorflow 的魔力很大一部分在于这几行。我们稍后会更详细地讨论这里发生了什么,但从概念上讲,这个功能只不过是按顺序应用前面的四层。

## 全局变量(3)

这些行只是设置一些我们将要使用的不同工具:

let batchSize = 128
let epochCount = 12
var model = MLP()
let optimizer = SGD(for: model, learningRate: 0.1)
let dataset = MNIST(batchSize: batchSize)

前两行设置了几个全局变量:我们的 batchSize(我们每次要查看多少个 MNIST 示例)和 epochCount(我们要对数据集进行的遍历次数)。

下一行初始化我们的模型,这是我们之前讨论过的。

第四行初始化我们的优化器,稍后我们会详细讨论。

最后一行设置了我们的数据集处理程序。

下一行通过循环我们的数据开始了我们的实际训练过程:

for (epoch, epochBatches) in dataset.training.prefix(epochCount).enumerated()  {

现在我们可以进入实际的训练循环了!

## 训练循环:更新(4)

这是我们训练循环的实际核心。从概念上讲,我们将拍摄一组图片或**批**并将每张图片显示给第一组输入密集节点,这将**激发* *并转到下一组隐藏的密集节点,这将* *激发* *并转到最终的密集节点输出集。然后,我们将获取网络最后一层的所有输出,选择最大的一个,并查看它。如果这个节点和我们给它的原始输入是同一个数,那么我们会给网络一个**奖励* *,告诉它增加对结果的信心。如果这个答案是错误的,那么我们会给网络一个**负奖励* *并告诉它降低对结果的信心。通过使用数千个样本重复这一过程,我们的网络可以学习准确预测它从未见过的输入。

  Context.local.learningPhase = .training
  for batch in epochBatches {
    let (images, labels) = (batch.data, batch.label)
    let (_, gradients) = valueWithGradient(at: model) { model -> Tensor<Float> in
      let logits = model(images)
      return softmaxCrossEntropy(logits: logits, labels: labels)
    }
    optimizer.update(&model, along: gradients)
  }

这在引擎盖下是如何工作的?一点点微积分和我们所有的数据混合在一起。对于每个训练示例,我们获取原始像素值(图像数据),然后获取相应的标签(图片的实际数量)。然后,我们通过计算模型对 X 的预测值来确定模型的* 梯度 ,然后使用一个名为 softmaxCrossEntropy 的函数来查看我们的预测值与实际值 y 的比较情况。从概念上讲,softmax 只是获取一组输入,然后将它们的结果归一化为一个百分比。这在数学上可能有点复杂,因此转换数字以使用自然对数 e,然后除以指数之和,具有在任意输入中保持一致和易于在计算机上评估的有用的双重属性。然后,我们更新**模型 的方向,使其与应该出现的方向略有不同(如果正确,则更倾向于正确的方向,如果不正确,则远离)。我们的学习率决定了我们每次应该走多远(例如,因为我们的学习率是 0.1,所以我们每次只走网络认为正确的方向的 10%)。在调用所有这些的 for 循环中,我们将对我们的所有数据重复这个过程(一遍)多轮,或**个时期 *。

训练循环:准确性(5)

接下来,我们在我们的测试数据上运行我们的模型,并计算它在它尚未看到的图像上正确的频率(但我们知道正确的答案)。那么,精确度是什么意思,我们如何计算它?我们的代码如下所示:

Context.local.learningPhase = .inference var testLossSum: Float = 0 var testBatchCount = 0 var correctGuessCount = 0 var totalGuessCount = 0 for batch in dataset.validation { let (images, labels) = (batch.data, batch.label) let logits = model(images) testLossSum += softmaxCrossEntropy(logits: logits, labels: labels).scalarized() testBatchCount += 1

let correctPredictions = logits.argmax(squeezingAxis: 1) .== labels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.data.shape[0]

}

let accuracy = Float(correctGuessCount) / Float(totalGuessCount) print( """ [Epoch (epoch + 1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount)) """ )

在与我们的训练数据集类似的过程中,我们简单地获取测试输入图像,通过我们的模型运行它们,然后将我们的结果与我们知道的正确答案进行比较。然后,我们计算正确答案的数量除以图像的总数,得出我们的准确率。我们最后的几行只是打印出每次通过数据集的各种数字,或**纪元* *,因此我们可以看到我们的损失是否在减少(例如,网络每次通过都变得更加准确)。

演示细分(低级)

好了,我们已经在高层次上完成了 MNIST 的例子。现在让我们来看一下我们正在调用的一些函数,并更深入地探索我们简单的训练循环。

完全连接的神经网络层

完全连接的层构成了我们网络的主干,因此值得花些时间来了解它们。在较高层次上,输入数据集中的每组结点都映射到输出数据集中。然后,网络的每条边都有一个由我们的训练函数更新的权重。然后每个节点的数学就是字面上的[权重]*[输入]+[偏差],输出节点的值就是这个数学函数的结果。Weight是我们将在这个节点的输入上放置多少值,然后bias是一个常量值,无论发生什么情况都分配给这个节点。这两者的价值将在我们的培训中学习。我们用矩阵数学来表示我们的变量,这就是为什么每个值都在括号里。

对于单个节点来说,前面的数学很容易理解,但神经网络的真正魔力来自许多这些节点的共同作用。大致来说,每个神经元学习输入的一个部分或**特征,然后通过与其他神经元合作,它们共同学习产生我们想要的结果所需的一组权重。所有这些工作的第二个要素是我们将多个层结合在一起。这些节点不是独立地学习它们的值,而是从其他也在更新的节点学习。这意味着,通过结合一起工作来确定何时触发的想法,神经元一起工作来找到最有效的方式来表示输入数据。

请注意,我们在这里非常宽松地使用了 learn 这个词。前面的数学都是正确的,但是人们经常认为这个过程比实际存在的要聪明得多。我认为思考这个问题的最佳方式是简单地将您的输入数据视为半相关样本的集合(例如,分布),然后神经网络是将该分布缩减为极小表示的一种方式。我们将继续探索理解这一关键概念的不同方式。

ReLU 是一个足够简单的函数,可以用数学来解释:relu(x) = max(0,x)。这意味着我们返回原始值,然后对于所有小于零的值,我们只返回一个零。这里还有其他的选择(我们将在下一章讨论),特别是 sigmoid 函数,但是由于 ReLU 产生了很好的结果并且很容易评估(并且通过扩展很快),它已经成为了你在实践中会发现的事实上的标准激活函数。

优化器如何工作

为了继续前面的想法,我们的目标是试图找到一组神经元,它们将一起激发来表示我们的数据。因此,在高层次上,我们将向我们的网络展示我们的数据,然后计算我们的模型与我们的理论结果有多远,然后尝试在下一次将我们的网络稍微移近更正确。这个过程就是我们的优化器所做的。如果我们的网络猜测正确,并且正朝着正确的方向前进,那么我们告诉它继续前进。如果我们的网络猜错了,并且正朝着错误的方向前进,那么我们告诉它继续朝着相反的方向前进。

表示这一点的最简单的方法是考虑试图找到一条曲线的最小值,比如 y = x².我们可以随便在曲线上取任意一点,在附近的另一点(可以说一步之遥)计算结果。那么两种可能性中的任何一种都会发生:要么我们离基地越来越远(例如,向错误的方向移动),要么我们离基地越来越近。然后,对于我们的下一步,我们可以继续朝着同一个方向前进,或者改变我们的路线。不管怎样,我们最终会接近曲线的底部。

继续前面的想法,我们的方法有几个问题。第一种是当我们的步长过大时。离底部越远,这将收敛得越快,但当我们接近底部时,我们最终将处于从一边跳到另一边的状态。另一方面,选择的步长太小,需要很长时间才能达到最小值,但这通常不是太大的问题(如果达到了,就达到了)。下一个技巧是添加所谓的动量(或二阶梯度)。基本思想是,我们不完全改变每一步的速度,而是保持先前的运动(例如,我们每一步只增加 10%的方向变化)。

优化器+神经网络

前面的想法就是所谓的凸优化。然而,在处理神经网络时,事情就有点棘手了。首先,根据定义,我们正在为每个神经元更新一个优化函数,因此这个问题爆发为在超维空间中处理许多不同的函数。对计算机来说,这只不过是一个非常大的数学问题,但对人类来说,不再有一个好的方法来可视化正在发生的事情。这是一个很大的数学开放领域,叫做非凸优化。

第二个问题更简单:对于我们的数学问题,我们很容易计算出我们是否在朝着正确的方向前进,因为我们知道正确的答案是什么。神经网络中的一个非常大的问题(特别是对于更高级的领域)是为我们的问题找到正确的目标函数。在本书中,我们将主要使用 softmax 交叉熵损失。对于图像识别的问题,这很容易通过将我们的答案与已知结果进行比较来表示(例如,我们只是对事情进行正确与否的分级)。但是,在神经网络的更高级应用中,构建自定义损失函数是一个有趣的问题,你应该知道。

Swift for Tensorflow

前面的文本涵盖了神经网络部分。现在,让我们看看 swift for tensorflow 的用武之地。从数学的角度来看,提到的方法有望简单到足以理解。将它应用于我们的神经网络问题,以扩展到更大问题的方式,问题更加复杂。最大的问题是,对于现实世界的网络来说,在内存中跟踪我们所有的梯度使得更新它们变得更加简单和快速。第二个是当手工构建这些模型时,很容易引入一些微妙的错误,这些错误会在以后造成问题。用于 tensorflow 的 Swift 使用 Swift 的类型系统来要求层协议,正如我们前面看到的。基本的想法很简单,我们要确保每个模型都执行这个协议。然后,我们可以向模型中添加新的部分,只要他们比理论上扩展这个协议,所述部分的任何任意组合都可以工作。实施这一层协议迫使我们,程序员,保持我们的函数链正确,并且通过扩展允许编译器以任何它想要的方式来模拟我们的梯度。那么,通过扩展,编译器可以为我们手头的任何硬件设备输出代码。这就是我们将 swift 用于 tensorflow 的原因:获得我们网络的编译时检查,以及使用平台特定优化在许多不同硬件后端上运行我们模型的能力。

支线任务

为了理解发生了什么,您可以对代码进行一些简单的调整:

  • 尝试使密集层变小或变大(例如,将 inputLayer、hiddenLayer 和 outputLayer 中的 512 更改为 128 或 1024),然后再次运行以查看这如何影响结果。

  • 尝试将历元数增加到 30,并将学习速率降低到 0.001,看看较小的步长如何仍能收敛到相同的结果。

概述

我们已经了解了如何与一个名为 MNIST 的简单数据集进行交互,该数据集由从 0 到 9 的灰度手绘数字组成,共有 10 个类别。我们已经构建了一个简单的一维神经网络(称为多层感知器* *),使用 swift for tensorflow 对 MNIST 数字进行分类。我们已经了解了如何使用一种称为随机梯度下降* 的统计技术,在每次看到新图像时更新我们的神经网络,以产生越来越好的结果。我们建立了一个基本但功能性的训练循环,多次遍历数据集,或**个时期 *,从最初的随机状态(本质上是猜测)训练我们的神经网络,最终能够识别 90%以上的数字。

从概念上讲,这是本书最难的一章。从字面上看,我们未来要做的一切只是采取同样的基本方法,并不断改进。在前进之前,花些时间把提到的所有事情都记下来。接下来,我们将向我们构建的神经网络添加一些卷积,以产生我们的第一个卷积神经网络。

二、MNIST:2D 神经网络

在本章中,我们将通过添加卷积来修改我们的一维神经网络,以产生我们的第一个实际卷积(2D)神经网络,并使用它来再次分类黑白(例如,MNIST)图像。

回旋

卷积是计算机视觉理论的一个深入领域。在高层次上,我们可能会想到获取一个输入图像并产生另一个输出图像:

[cat] --> [magic black box] --> [dog]

概括地说,对于任何输入图像,都有一种方法可以将其转换为目标图像。在最简单的层面上,我们可以破坏源图像(例如,乘以零),然后插入目标图像(例如,添加其像素):

[cat] --> 0 * [cat] + [dog] --> [dog]

然后,我们可以使用简单的数学方法对中间步骤进行建模:

```a[X] + b```py

这块数学叫做内核。这是一个卷积,尽管不是非常有用。

广义地说,对于宇宙中的每一幅图像,我们都可以想出一个内核来将其转换成我们想要的任何其他图像。推而广之,你能想到的任何东西都有一个内核。

一般来说,这是计算机视觉中非常非常深入的研究领域,这里可以做许多不同的事情。

3x3 附加模糊示例

接下来,我们来看一个稍微复杂一点的例子,一个 3x3 的加法模糊。实际的内核如下所示:

[ 1, 1, 1 ]
[ 1, 1, 1 ]
[ 1, 1, 1 ]

这个卷积将会对输入图像产生一个简单的模糊。这是通过为输入图像中的每个 3x3 像素块创建一个输出像素来实现的,它是我们所看到的 9 个像素的总和。然后,通过使用 1 步的步幅跨过输入图像的行,我们最终得到模糊的最终图像,因为每个输出像素不仅具有来自原始对应像素的信息,还具有来自其邻居的信息。我们所有的产出都比我们开始时的数字要大。我们应用最后一个简单的步骤,通过将所有值除以 9 来产生与原始图像相似的值,从而对结果进行**归一化* *处理。

3x3 高斯模糊示例

下一点你不需要 100%理解,我们只是试图建立在概念上。

我们可以改变 3x3 的数据,并保持相同的操作,以产生更复杂的东西。这里有一个我们可以使用的略有不同的乘法内核:

[1/16, 1/8, 1/16]
[1/8, 1/4, 1/8]
[1/16, 1/8, 1/16]

然后我们可以用和之前一样的基本方法得到不同的结果。这里,我们利用矩阵乘法来保留更多的中心像素和更远的像素。在 3x3 大小的情况下,很难看出这个例子和我们的第一个例子之间的差异,但如果你可以想象构建上述矩阵的更大版本,这就是在 Photoshop 等图像编辑程序中产生更大高斯模糊的数学。

组合 3x3 卷积–Sobel 滤波器示例

对于卷积可以做什么的更高级的例子,让我们看一下将这些内核操作中的两个结合在一起以产生所谓的 Sobel 滤波器。还是那句话,你不需要 100%理解这个。

我们的第一个内核看起来像这样:

[1, 0, -1]
[2, 0, -2]
[1, 0, -1]

我们的第二个内核是这样的:

[1, 2, 1]
[0, 0, 0]
[-1, -2, -1]

然后我们把它们和我们的输入图像组合在一起,就像这样,一个接一个:

[A] x [B] = [C]

结果很有趣;所发生的是相似的像素被乘以零(例如黑色),但是具有显著差异的像素集合被乘以无穷大(例如白色)。因此,用几个基本的卷积核,我们制作了一个边缘检测器!现在让我们避免陷入更深的回旋兔子洞。只要知道这是一个很深很深的领域,很多事情都有可能。

3x3 大步走

非常宽泛地说,我们实际上不会构建我们自己的卷积。相反,我们要让神经网络来学习它们!为此,我们只需要关注一个关键概念,即在这些 3x3 块中检查我们的图像的过程。这叫做大步走,这是一个需要理解的非常重要的概念。基本上,神经网络将学习在飞行中进行自己的卷积,然后使用它们来更好地理解我们的输入数据,然后每一步都将稍微更新它们以改善其结果。别担心,刚开始会有点头脑不稳定。让网络学习一些,然后我们可以看看他们如何在现实世界的例子中工作。

填料

“相同”填充和“有效”填充是卷积中会遇到的两种填充形式。在我们的前几章中,我们将使用“相同”的填充,但“有效”是 swift for tensorflow 中 2D 卷积运算符的默认设置,因此您需要理解这两者。

Valid 也许更容易理解。每一步都向前推进,直到卷积的远边缘碰到输入图像的边缘,然后停止。这意味着根据定义,这种卷积类型将产生比输入图像更小的输出(1x1 滤波器的特殊情况除外)。“相同”填充扩展输入数据的边缘以继续在输入图像上工作,直到步幅的前沿达到输入图像的界限。

这意味着“相同”填充(当使用步长为 1 时)将产生与输入图像大小相同的输出图像。在接下来的几章中,我们将使用相同的填充跳转到一些更复杂的模型,所以现在专注于理解它。

Maxpool(最大池)

您需要理解的另一个关键概念是最大池。我们要做的就是取每组 4 个输入像素,以两个为一组的步幅跨过我们的图像,并通过选择最大值将其转换为单个输出。对于区域,我们只需找到最大的像素,并将其作为我们的输出。

2D·MNIST 模型

如果我们把这两个概念放在一起,重新审视 MNIST 问题,我们实际上可以通过改变我们对数据建模的方式来显著提高我们的质量。我们将采用相同的 784,但我们将把它视为实际图像,因此它现在将是 28x28 像素。我们将通过两层 3x3 卷积,一个最大池操作来运行它,然后我们将保持我们相同的密集连接层和十个类别的输出。

密码

这是实际的 swift 代码。我已经从前面的例子,并添加了一个顶部的卷积堆栈。然后,我们将输入通过卷积层,然后发送到与之前相同的输出和密集连接层。这将运行一段时间,最终我们将在 MNIST 数据集上达到大约 98%的准确率。因此,通过简单地改变我们对输入数据建模的方式,改用卷积,我们就能够在这个玩具问题上将错误率降低一半。此外,卷积比我们的密集层更容易评估,所以随着我们的数据集开始变大,我们仍然可以继续使用这种方法。

import Datasets import TensorFlow

struct CNN: Layer { var conv1a = Conv2D(filterShape: (3, 3, 1, 32), padding: .same, activation: relu) var conv1b = Conv2D(filterShape: (3, 3, 32, 32), padding: .same, activation: relu) var pool1 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

var flatten = Flatten() var inputLayer = Dense(inputSize: 14 * 14 * 32, outputSize: 512, activation: relu) var hiddenLayer = Dense(inputSize: 512, outputSize: 512, activation: relu) var outputLayer = Dense(inputSize: 512, outputSize: 10)

@differentiable public func forward(_ input: Tensor) -> Tensor { let convolutionLayer = input.sequenced(through: conv1a, conv1b, pool1) return convolutionLayer.sequenced(through: flatten, inputLayer, hiddenLayer, outputLayer) } }

let batchSize = 128 let epochCount = 12 var model = CNN() let optimizer = SGD(for: model, learningRate: 0.1) let dataset = MNIST(batchSize: batchSize)

print("Starting training...")

for (epoch, epochBatches) in dataset.training.prefix(epochCount).enumerated() { Context.local.learningPhase = .training for batch in epochBatches { let (images, labels) = (batch.data, batch.label) let (_, gradients) = valueWithGradient(at: model) { model -> Tensor in let logits = model(images) return softmaxCrossEntropy(logits: logits, labels: labels) } optimizer.update(&model, along: gradients) }

Context.local.learningPhase = .inference var testLossSum: Float = 0 var testBatchCount = 0 var correctGuessCount = 0 var totalGuessCount = 0 for batch in dataset.validation { let (images, labels) = (batch.data, batch.label) let logits = model(images) testLossSum += softmaxCrossEntropy(logits: logits, labels: labels).scalarized() testBatchCount += 1

let correctPredictions = logits.argmax(squeezingAxis: 1) .== labels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.data.shape[0]

}

let accuracy = Float(correctGuessCount) / Float(totalGuessCount) print( """ [Epoch (epoch + 1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount)) """ ) }


您应该会得到如下所示的输出:

Loading resource: train-images-idx3-ubyte Loading resource: train-labels-idx1-ubyte Loading resource: t10k-images-idx3-ubyte Loading resource: t10k-labels-idx1-ubyte Starting training...
[Epoch 1] Accuracy: 9657/10000 (0.9657) Loss: 0.11145979
[Epoch 2] Accuracy: 9787/10000 (0.9787) Loss: 0.06319246
[Epoch 3] Accuracy: 9834/10000 (0.9834) Loss: 0.05008082
[Epoch 4] Accuracy: 9860/10000 (0.986) Loss: 0.041191828
[Epoch 5] Accuracy: 9847/10000 (0.9847) Loss: 0.04551203
[Epoch 6] Accuracy: 9856/10000 (0.9856) Loss: 0.04516899
[Epoch 7] Accuracy: 9890/10000 (0.989) Loss: 0.036287367
[Epoch 8] Accuracy: 9860/10000 (0.986) Loss: 0.043286547
[Epoch 9] Accuracy: 9878/10000 (0.9878) Loss: 0.037299085
[Epoch 10] Accuracy: 9877/10000 (0.9877) Loss: 0.042443674
[Epoch 11] Accuracy: 9884/10000 (0.9884) Loss: 0.043763407

[Epoch 12] Accuracy: 9890/10000 (0.989) Loss: 0.038426008

### 支线任务

LeNet 是解决 MNIST 问题的经典方法,始于 1998 年。我们使用稍微不同的架构来简化以后向更高级模型的过渡,但是您应该看看这篇文章。

>基于梯度的学习应用于文档识别

yann.lecun.com/exdb/publis…


## 概述

我们已经通过一些不同的例子研究了卷积是如何工作的。我们已经了解了**跨步**和*填充* *如何跨越输入图像。然后,我们看了**maxpool**,这是一个减少数据量的简单操作。然后,我们使用两对 3×3 卷积和一个最大池运算,在上一章的多层感知器的基础上,构建了第一个用于图像识别的卷积神经网络。运行与之前相同的训练循环,我们能够减少简单网络中的误差量,通过改变我们对输入数据的建模方式来提高我们的准确性。接下来,让我们看看如何扩展我们相同的基本方法来处理彩色图像和真实世界的数据。


# 三、CIFAR:分块 2D 神经网络

在这一章中,我们将看看如何堆叠多层卷积来扩大我们的网络,以解决一个更加现实的问题,即区分动物和车辆的彩色图片,称为 CIFAR。


## CIFAR 数据集

我们将何去何从?让我们来处理一个稍微大一点、复杂一点的问题。这是一个名为 CIFAR 的数据集。这是一套彩色图片集。所以我们有猫、狗、动物以及人类交通工具——汽车和卡车的图片。我们有十个类别。现在,我们将处理颜色数据,因此我们有一个 RGB 组件。


## 颜色

从神经网络的角度来看,颜色并没有你想象的那么复杂。从概念上讲,我们只需从 MNIST 网络中提取第一个 3×3 卷积,如下所示:

```py
var conv1a = Conv2D<Float>(filterShape: (3, 3, 1, 32), padding: .same, activation: relu)

我们只需将输入层数增加到 3 层,如下所示:

var conv1a = Conv2D<Float>(filterShape: (3, 3, 3, 32), padding: .same, activation: relu)

这是怎么回事?实际上,在我们的 MNIST 示例中,我们是将颜色作为灰度值(例如,Int/255.0)来处理的,所以现在我们将为每个颜色分量(例如,红、绿、蓝)设置三个灰度通道。对于我们的卷积运算,这只是添加了更多的数据供我们处理,但我们只是使用了与之前相同的过程。

故障

对于 CIFAR,我们可以采用之前使用的相同的基本方法,并将其扩展以解决这个问题。因此,我们将简单地采用我们的颜色输入数据——三个通道,32x32 像素。我们将通过两组卷积、一个最大池、另外两组卷积、一个最大池和同样的两个紧密连接的层来运行它,然后我们将有十个输出类别。

密码

这是这个模型的样子。除了添加另一叠卷积,我们什么也没做,但现在我们正在处理彩色和真实世界的照片。

import Datasets import TensorFlow

struct CIFARModel: Layer { var conv1a = Conv2D(filterShape: (3, 3, 3, 32), padding: .same, activation: relu) var conv1b = Conv2D(filterShape: (3, 3, 32, 32), padding: .same, activation: relu) var pool1 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

var conv2a = Conv2D(filterShape: (3, 3, 32, 64), padding: .same, activation: relu) var conv2b = Conv2D(filterShape: (3, 3, 64, 64), padding: .same, activation: relu) var pool2 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

var flatten = Flatten() var inputLayer = Dense(inputSize: 8 * 8 * 64, outputSize: 512, activation: relu) var hiddenLayer = Dense(inputSize: 512, outputSize: 512, activation: relu) var outputLayer = Dense(inputSize: 512, outputSize: 10)

@differentiable func forward(_ input: Tensor) -> Tensor { let conv1 = input.sequenced(through: conv1a, conv1b, pool1) let conv2 = conv1.sequenced(through: conv2a, conv2b, pool2) return conv2.sequenced(through: flatten, inputLayer, hiddenLayer, outputLayer) } }

let batchSize = 128 let epochCount = 12 var model = CIFARModel() let optimizer = SGD(for: model, learningRate: 0.1) let dataset = CIFAR10(batchSize: batchSize)

print("Starting training...")

for (epoch, epochBatches) in dataset.training.prefix(epochCount).enumerated() { Context.local.learningPhase = .training for batch in epochBatches { let (images, labels) = (batch.data, batch.label) let (_, gradients) = valueWithGradient(at: model) { model -> Tensor in let logits = model(images) return softmaxCrossEntropy(logits: logits, labels: labels) } optimizer.update(&model, along: gradients) }

Context.local.learningPhase = .inference var testLossSum: Float = 0 var testBatchCount = 0 var correctGuessCount = 0 var totalGuessCount = 0 for batch in dataset.validation { let (images, labels) = (batch.data, batch.label) let logits = model(images) testLossSum += softmaxCrossEntropy(logits: logits, labels: labels).scalarized() testBatchCount += 1

let correctPredictions = logits.argmax(squeezingAxis: 1) .== labels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.data.shape[0]

}

let accuracy = Float(correctGuessCount) / Float(totalGuessCount) print( """ [Epoch (epoch + 1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount)) """ ) }

结果

使用这个简单的卷积堆栈,我们的简单模型可以达到 70%以上的精度。这不会很快赢得奖项,但这种基本方法是可行的。您应该会看到类似这样的结果:

... [Epoch 1] Accuracy: 4938/10000 (0.4938) Loss: 1.403413 [Epoch 2] Accuracy: 5828/10000 (0.5828) Loss: 1.1972797 [Epoch 3] Accuracy: 6394/10000 (0.6394) Loss: 1.0232711 [Epoch 4] Accuracy: 6857/10000 (0.6857) Loss: 0.92201495 [Epoch 5] Accuracy: 6951/10000 (0.6951) Loss: 0.9035831 [Epoch 6] Accuracy: 6778/10000 (0.6778) Loss: 1.0228367 [Epoch 7] Accuracy: 7082/10000 (0.7082) Loss: 0.95399994 [Epoch 8] Accuracy: 7088/10000 (0.7088) Loss: 1.0445035 [Epoch 9] Accuracy: 7117/10000 (0.7117) Loss: 1.1742744 [Epoch 10] Accuracy: 7183/10000 (0.7183) Loss: 1.347533 [Epoch 11] Accuracy: 7045/10000 (0.7045) Loss: 1.4588598 [Epoch 12] Accuracy: 7132/10000 (0.7132) Loss: 1.5338957

支线任务

研究颜色在现实世界中如何工作以及我们如何感知光是一个有趣的领域。你应该看看 CYMK(例如,打印色彩理论),然后如何压缩视频(例如,YUV)色彩空间。光源,无论是人造的(如灯泡,发光二极管),监视器(电视/电脑),还是天然的(如火,星星),都会导致各种有趣的差异(氢光谱,哈勃常数)。

概述

我们已经从灰度跳到了彩色,并切换到了一个稍大、更复杂的数据集,称为CIFAR,但除此之外,我们与上一章相同的方法大致相同。为了更好地对我们的图像进行分类,我们添加了另一个**块* *的卷积。然后,我们使用上一章中的这些多重卷积块和第一章中的相同全连接网络来对现实世界物体的彩色图像进行分类(尽管由于它们很小,看起来有点困难)。接下来,我们将构建同一网络的更大版本,以处理更大的图像和更多的数据。

四、VGG 网络

在本章中,我们将通过制作一个更大版本的 CIFAR 网络,从 2014 年开始建设 VGG,这是一个最先进的网络。

背景:ImageNet

MNIST 和 CIFAR 是受欢迎的,在学术研究中经常被引用的数据集,作为新想法的试验台,但在过去几年中,人们越来越多地达到了在它们之上建立网络的实际限制。我们的下一个数据集是 ImageNet,这是一个流行的真实世界数据集,用于构建和训练图像识别和对象检测网络。ImageNet 有一千个类别,所以我们在本书的其余部分将使用的网络将能够支持更大的分类问题。数据集本身是从互联网上搜集的大约 130 万张图片。在数据方面,训练数据集约为 147GB,另外还有 7GB 的测试和验证文件。如果你去 ImageNet 网站(如 http://www.image-net.org )你可以浏览一些类别,它们的名字像“n01440764”如果您将这些数字与 synnet 文件进行比较,您可以找出每个类别对应的内容。

获取图像

这曾经是一件稍微复杂的事情,但是最近 swift-models 存储库为 ImageNet 数据集添加了一个很好的数据加载器,您可以在您的系统上使用它。但是,请注意,您将需要几百千兆字节的空闲磁盘空间来处理文件(提取、转换等)。).话虽如此,对于我们的目的来说,ImageNet 有点大,因此我们将使用一个子集,以免达到我们的计算机和 swift for tensorflow 的极限。

Imagenette 数据集

Imagenette 是 fast.ai 的杰瑞米·霍华德 ImageNet 的子集,旨在使测试计算机视觉网络更容易。具体是以下十大类:tench、英式 springer、卡带播放器、链锯、教堂、法国号、垃圾车、气泵、高尔夫球、降落伞。

还有第二个更难的版本 Imagenette,另一个子集的十个类别,称为 Imagewoof,这是具体的以下十个犬种类别:澳大利亚梗,边境梗,萨摩耶,小猎犬,西施犬,英国猎狐犬,罗得西亚脊背犬,野狗,金毛猎犬,老英国牧羊犬。

我们可以从 swift-models 存储库中加载这两个数据集,并在您的培训脚本中交换它们。使用 swift-models loader 的好处在于,它可以自动下载、提取实际 ImageNet 图像(具有半随机大小)并将其批量调整为可预测的输入大小(例如,224 x 224 像素)。

日期增加

一般来说,图像识别/深度神经网络中一个非常重要的主题是**数据增强* *,我们在本书中基本上会跳过它,因为我想避免让这个领域的新手感到事情复杂。但是,在继续之前,让我们在这里简单地讨论一下。

我们可以想象增加我们的神经网络的规模,直到我们有一个“完美”的神经网络,对于我们展示的每一幅图像,它都给我们正确的结果。关键的概念是,我们使用的优化函数试图最小化我们给它的数据集的损失。所以,我们对这个“完美”网络的优化函数已经到了零(它从不出错),正如我们所希望的那样。听起来很棒,让我们写一篇论文并收集我们的奖品吧!

但是等等!在我们这样做之前,我们可能会尝试,比如说,水平翻转我们的猫图片,然后将这个新图片交给我们的神经网络。会发生什么?基本上,我们正在向我们的神经网络展示一幅前所未见的画面,所以结果充其量是半随机的。结果可能是,我们翻转的猫的“最接近”输入图像(在神经网络的搜索空间中)是一张狗的图片,因此当我们的网络看到这张新图片时,它会说“狗”。

然后,数据扩充(以及一般的训练深度神经网络)的基本思想是确保我们不会过度拟合(例如,过于接近我们的训练数据集)以至于我们无法* 概括 *(例如,对我们从未见过的数据正确地做出新的预测)。有几个基本方法:

  1. 收集更多数据!你不会在学术竞赛/目的中看到这种情况,但许多现实世界的机器学习涉及到获取或构建更大的数据集,以确保我们的网络不是真的在猜测新的条件,而是已经“看到”了一个相当类似的例子。同样,另一个常见的问题是只给我们的网络提供灰色猫的图片,然后当它看到橙色猫时,它不知道该怎么办。如果都是同一只猫,那么有很多例子对我们没有太大帮助!这个问题的另一个常见版本是获得与我们想要最终分类的不同的训练示例,例如,从互联网上训练照片,然后尝试将它们应用到现实世界的相机输入。只要有可能,就使用最终要测试的相同数据。同样,只要你能,收集更多的数据!

  2. 数据扩充:我们可以使用计算机对我们的数据进行各种修改,以增加我们总体的样本数量。一些常见的例子:

    • 我们可以将图像从左向右翻转。

    • 改变我们的亮度(伽玛)。

    • 旋转我们的图像。

    • 随机裁剪(切掉图片的边缘)。

    • 随机缩放(使我们的图片变大,然后切掉现在更大的边缘)。

通常,这些方法也会结合起来,以确保神经网络也能获得尽可能多的训练数据变量。重要的一点是,这些方法经常变得特定于领域。或者说,我们可以翻转猫/狗的图片,但不能翻转字母表中的字母!

  1. 给我们的网络增加噪音:另一个极其重要的方面是给我们的运营增加噪音,以确保我们的网络不会过于依赖特定的输入/图像。这是一项非常有价值的技术,通过使我们的网络对噪声具有鲁棒性来提高现实世界的性能。有一个重要的相关研究领域叫做对抗性输入,它试图通过引入细微的噪声来欺骗分类器,从而欺骗网络。

这里有一些关于这个主题的有趣论文,你可以看看:

退出:防止神经网络过度拟合的简单方法

> www.cs.toronto.edu/~hinton/absps/JMLRdropout.pdf

这是一篇重要的论文,你应该知道。在训练我们的模型时,通过随机修剪(删除)密集节点,得到的网络概括得更好。另一个有趣的效果是,这也加快了网络速度。

混淆:超越经验风险最小化

> https://arxiv.org/abs/1710.09412

大致来说,我们正在训练我们的网络识别图像,并在它们得到正确答案时给予奖励。该论文随机组合两个输入图像(例如,50%狗和 50%猫->新图片),并奖励网络猜测相应的答案(例如,50%狗和 50%猫标签)。这个简单的调整显著提高了网络的泛化能力。

改进了带剪切块的卷积神经网络的正则化

> https://arxiv.org/abs/1708.04552

这个想法类似于 mixup,但是我们是通过剪切和粘贴图像来创建我们的目标图像的,并且有类似的改进效果。一般来说,有时在更高层次上解决这个问题是很重要的,确保我们不要试图让我们的网络太深,以至于最终总是试图追求“完美”的解决方案,而是学习足够的知识,以便能够在非测试环境中做得很好。这是一个微妙的领域,人们经常陷入追逐“完美”的参数集,但他们的网络在处理新数据时表现不佳。这是一个我们可以花很多时间的领域。我们将在本书的后面重新讨论这个问题。

利用光彩造型修护发膏

现在,让我们进入第一个真正用于图像识别的最先进的卷积神经网络。VGG 代表视觉几何小组,这是英国牛津大学的一个计算机视觉/数学相关研究人员小组。

“用于大规模图像识别的非常深的卷积网络”

> https://arxiv.org/abs/1409.1556

他们制作了一组网络(以他们的小组命名),在 2014 年的 ILSVRC 竞赛中排名第二,仅次于 GoogLeNet。

然而,不要让这吓到你,因为他们的方法在技术上并不比我们目前所看到的 MNIST 和 CIFAR 网络更复杂。他们通过堆叠我们在过去几章中看到的相同的卷积组来建立他们的大型神经网络。他们的网络与我们之前构建的网络完全相同:两组 3x3 卷积、一个最大池、另外两组 3x3 卷积和一个最大池。接下来,他们继续堆叠层,并添加三组 3x3 卷积、一个最大池、三组 3x3 卷积、一个最大池、三组 3x3 卷积和一个最大池。最后,对于输出层,他们使用两个由 4096 个完全连接的节点组成的大型层(可以说,使他们的网络能够了解更多信息),最后有一个由一千个节点组成的输出层来映射到每个 ImageNet 类别。

这个网络之所以叫 VGG16,是因为它有(输入)[2 + 2 + 3 + 3 + 3] + 2(全连接神经网络)+ 1(输出)层。出于我们的目的,我们将只在最后使用 10 个输出节点(例如,为什么我们的 classCount init 参数是 10),以处理我们较小的 Imagenette 数据集,但其他方面都是相同的。

密码

首先,让我们看看我们的训练循环,它应该看起来非常熟悉我们的 CIFAR 和 MNIST 训练循环。唯一真正的区别是,现在我们正在处理一个更大的数据集。我们的下一个网络对训练有点挑剔,所以我们使用具有较小学习速率(更新步长值)的 SGD 来确保它正确训练,不会“跳跃”太多。

import Datasets import TensorFlow

struct VGG16: Layer { var conv1a = Conv2D(filterShape: (3, 3, 3, 64), padding: .same, activation: relu) var conv1b = Conv2D(filterShape: (3, 3, 64, 64), padding: .same, activation: relu) var pool1 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

var conv2a = Conv2D(filterShape: (3, 3, 64, 128), padding: .same, activation: relu) var conv2b = Conv2D(filterShape: (3, 3, 128, 128), padding: .same, activation: relu) var pool2 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

var conv3a = Conv2D(filterShape: (3, 3, 128, 256), padding: .same, activation: relu) var conv3b = Conv2D(filterShape: (3, 3, 256, 256), padding: .same, activation: relu) var conv3c = Conv2D(filterShape: (3, 3, 256, 256), padding: .same, activation: relu) var pool3 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

var conv4a = Conv2D(filterShape: (3, 3, 256, 512), padding: .same, activation: relu) var conv4b = Conv2D(filterShape: (3, 3, 512, 512), padding: .same, activation: relu) var conv4c = Conv2D(filterShape: (3, 3, 512, 512), padding: .same, activation: relu) var pool4 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

var conv5a = Conv2D(filterShape: (3, 3, 512, 512), padding: .same, activation: relu) var conv5b = Conv2D(filterShape: (3, 3, 512, 512), padding: .same, activation: relu) var conv5c = Conv2D(filterShape: (3, 3, 512, 512), padding: .same, activation: relu) var pool5 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

var flatten = Flatten() var inputLayer = Dense(inputSize: 512 * 7 * 7, outputSize: 4096, activation: relu) var hiddenLayer = Dense(inputSize: 4096, outputSize: 4096, activation: relu) var outputLayer = Dense(inputSize: 4096, outputSize: 10)

@differentiable public func forward(_ input: Tensor) -> Tensor { let conv1 = input.sequenced(through: conv1a, conv1b, pool1) let conv2 = conv1.sequenced(through: conv2a, conv2b, pool2) let conv3 = conv2.sequenced(through: conv3a, conv3b, conv3c, pool3) let conv4 = conv3.sequenced(through: conv4a, conv4b, conv4c, pool4) let conv5 = conv4.sequenced(through: conv5a, conv5b, conv5c, pool5) return conv5.sequenced(through: flatten, inputLayer, hiddenLayer, outputLayer) } }

let batchSize = 32 let epochCount = 10

let dataset = Imagenette(batchSize: batchSize, inputSize: .resized320, outputSize: 224) var model = VGG16() let optimizer = SGD(for: model, learningRate: 0.002, momentum: 0.9)

print("Starting training...")

for (epoch, epochBatches) in dataset.training.prefix(epochCount).enumerated() { Context.local.learningPhase = .training for batch in epochBatches { let (images, labels) = (batch.data, batch.label) let (_, gradients) = valueWithGradient(at: model) { model -> Tensor in let logits = model(images) return softmaxCrossEntropy(logits: logits, labels: labels) } optimizer.update(&model, along: gradients) }

Context.local.learningPhase = .inference var testLossSum: Float = 0 var testBatchCount = 0 var correctGuessCount = 0 var totalGuessCount = 0 for batch in dataset.validation { let (images, labels) = (batch.data, batch.label) let logits = model(images) testLossSum += softmaxCrossEntropy(logits: logits, labels: labels).scalarized() testBatchCount += 1

let correctPredictions = logits.argmax(squeezingAxis: 1) .== labels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.label.shape[0]

}

let accuracy = Float(correctGuessCount) / Float(totalGuessCount) print( """ [Epoch (epoch+1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount)) """ ) }

结果

在 Imagenette 数据集上运行此网络应该会产生如下结果:

[Epoch 1 ] Accuracy: 125/500 (0.25) Loss: 2.290163 [Epoch 2 ] Accuracy: 170/500 (0.34) Loss: 1.8886051 [Epoch 3 ] Accuracy: 205/500 (0.41) Loss: 1.6971107 [Epoch 4 ] Accuracy: 243/500 (0.486) Loss: 1.5611153 [Epoch 5 ] Accuracy: 257/500 (0.514) Loss: 1.43015 [Epoch 6 ] Accuracy: 290/500 (0.58) Loss: 1.2774785 [Epoch 7 ] Accuracy: 67/500 (0.534) Loss: 1.3170111 [Epoch 8 ] Accuracy: 309/500 (0.618) Loss: 1.1680012 [Epoch 9 ] Accuracy: 299/500 (0.598) Loss: 1.403522 [Epoch 10] Accuracy: 303/500 (0.606) Loss: 1.40440996

内存使用

使用 VGG16,您可能会达到系统的内存限制。请记住,您可能需要将批处理大小更改为 16(甚至更少),以便将数据集干净地放入 GPU 的内存中。一个很好的做法是启动一个作业,然后使用 tmux 打开一个新的 shell 会话,并运行“nvidia-smi -l 5 ”,观察设备在作业开始时如何填充内存。

在我们深入探讨之前,让我们来看看您在某个时间点通常会遇到的另一个重要问题,这肯定是 VGG 的问题,即针对 tensorflow 的 swift 内存不足。将您的批处理大小设置为 128,运行您的代码,并等待一会儿:

Fatal error: OOM when allocating tensor with shape[128,64,224,224] and type float on /job:localhost/replica:0/task:0/device:GPU:0 by allocator GPU_0_bfc: file / home/skoonce/swift/swift-source/tensorflow-swift-apis/Sources/ TensorFlow/Bindings/EagerExecution.swift, line 300 Current stack trace: 0 libswiftCore.so 0x00007fcb746f6c40 swift_reportError + 5 0 1 libswiftCore.so 0x00007fcb74767590 _swift_stdlib_reportF atalErrorInFile + 115 2 libswiftCore.so 0x00007fcb7445c53e + 14554

22 3 libswiftCore.so 0x00007fcb7445c147 + 14544

33 libswiftCore.so 0x00007fcb745fc310 valueWithPullback<A, B>(at:in:) + 106 34 libswiftTensorFlow.so 0x00007fcb74bb9e20 valueWithGradient<A, B>(at:in:) + 1073 35 VGG-Imagewoof 0x000055a5370311ed + 46453 57 36 libc.so.6 0x00007fcb5d6d6ab0 libc_start_main + 231 37 VGG-Imagewoof 0x000055a536bf90ba + 2213$0 Illegal instruction (core dumped)

我们在这里使用的 Imagenette 数据集使用了大约 16GB 的主内存。如果你有一个 8GB 内存的 GPU,你可能需要在接下来的几章中减少你的批处理大小,以避免可怕的 OOM(见前面的文本)。将其切成两半会使您更容易根据需要处理更大的数据集,但是对于一些较大的网络,您可能需要使用更小的批处理大小。

我鼓励您尝试不同的批量大小,并对每个批量运行 nvidia-smi,以了解这些概念之间的关系。在我看来,这是一项需要掌握的重要技能,因为它将使您能够针对具有更多/更少内存的设备来扩展和缩减您的工作负载。特别是 tensorflow 的 Swift 目前有点“globby ”,因为它似乎以数千兆字节的增量抓取东西,所以使用 s4tf 学习这一点不会像使用其他机器学习框架那样容易,但知道如何为您的设备调整工作负载是一项宝贵的技能,您在该领域(以及其他软件包)还需要一段时间。

模型重构

在某个时候,我们将会触及我们通过简单地复制和粘贴更多层来产生越来越大的神经网络所能完成的极限。现在是一个很好的时机来看看我们如何通过使用稍微复杂一点的编程方法来扩展我们的方法。首先,让我们做一些重构,并了解如何将多个层结合在一起,以减少重复代码的数量。

带子块的 VGG16

这是怎么回事?基本上,我们正在构建一些更小的块,这样我们就可以减少主网络中重复代码的数量。由于我们所有的 VGG 网络块看起来都一样(N ^ 3x 3 层+一个最大池),我们可以通过编程来定义它们。

struct VGGBlock2: Layer { var conv1a: Conv2D var conv1b: Conv2D var pool1 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

init(featureCounts: (Int, Int)) { conv1a = Conv2D(filterShape: (3, 3, featureCounts.0, featureCounts.1), padding: .same, activation: relu) conv1b = Conv2D(filterShape: (3, 3, featureCounts.1, featureCounts.1), padding: .same, activation: relu) }

@differentiable public func forward(_ input: Tensor) -> Tensor { return input.sequenced(through: conv1a, conv1b, pool1) } }

struct VGGBlock3: Layer { var conva: Conv2D var convb: Conv2D var convc: Conv2D var pool = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

init(featureCounts: (Int, Int)) { conva = Conv2D(filterShape: (3, 3, featureCounts.0, featureCounts.1), padding: .same, activation: relu) convb = Conv2D(filterShape: (3, 3, featureCounts.1, featureCounts.1), padding: .same, activation: relu) convc = Conv2D(filterShape: (3, 3, featureCounts.1, featureCounts.1), padding: .same, activation: relu) }

@differentiable public func forward(_ input: Tensor) -> Tensor { return input.sequenced(through: conva, convb, convc, pool) } }

struct VGG16: Layer { var layer1 = VGGBlock2(featureCounts: (3, 64)) var layer2 = VGGBlock2(featureCounts: (64, 128)) var layer3 = VGGBlock3(featureCounts: (128, 256)) var layer4 = VGGBlock3(featureCounts: (256, 512)) var layer5 = VGGBlock3(featureCounts: (512, 512))

var flatten = Flatten() var inputLayer = Dense(inputSize: 512 * 7 * 7, outputSize: 4096, activation: relu) var hiddenLayer = Dense(inputSize: 4096, outputSize: 4096, activation: relu) var outputLayer = Dense(inputSize: 4096, outputSize: 10)

@differentiable public func forward(_ input: Tensor) -> Tensor { let backbone = input.sequenced(through: layer1, layer2, layer3, layer4, layer5) return backbone.sequenced(through: flatten, inputLayer, hiddenLayer, outputLayer) } }

支线任务

从现代标准来看,AlexNet 的结构有些非正统,所以我在本书中有意跳过了它,但由于历史原因,它是一篇值得一读的重要论文。

使用深度卷积神经网络的 ImageNet 分类

> https://papers.nips.cc/paper/4824-imagenet-classification- with-deep-convolutional-neural-networks.pdf

inception v1(Google net 现在更为人所知的名称)比 VGG 有更好的表现,但它是一个更复杂的模型。这篇论文在历史上很重要,但我建议你先掌握剩余网络。

通过卷积更深入

> https://arxiv.org/abs/1409.4842

概述

今天,VGG 不像我们马上要看的其他网络那样受欢迎,但它肯定仍在使用,尽管已经有五年的历史了。这种网络仍然可以在图像处理环境中看到,例如风格转换和作为对象检测网络的基础。你还会经常在特定领域的图像识别问题中看到经过重新训练的 VGG 网络,比如人脸识别。祝贺你成功来到这里。你已经成功地复制了你的第一篇学术论文!接下来,让我们看看如何稍微修改我们的网络,以产生更好的结果。

五、ResNet 34

在本章中,我们将探讨如何改造 VGG 网络主干网,以生产 ResNet 34,即 2015 年的网络。回顾我们过去的几章,我们的 2D MNIST、CIFAR 和 VGG 网络之间的区别只是 3×3 卷积的块数。但是,为什么要在这一点上停下来呢?让我们建立更大的网络!接下来,我们将看看 ResNet 系列网络,从 ResNet 34 开始。

从概念上讲,我们将从一个与我们刚才看到的 VGG 网络相似的基础开始。如果我们的 VGG 网络的主干可以被认为是 VGG16 的[2,2,3,3,3],那么 ResNet 34 的主干是[6,8,12,6],每个块由成对的 3x3 卷积组成,与我们之前看到的网络完全相同。然而,我们将增加一个更重要的概念,叫做跳过连接。

跳过连接

可以说,残留网络的魔力在于添加了所谓的残留层或跳过连接。

用于图像识别的深度残差学习

> https://arxiv.org/abs/1512.03385

基本思想是,我们添加一组额外的路径,从每组层跳到输出节点。这可以在网络级很简单地实现,只需将每组输入层添加到输出步骤的模块中。

这通常表示为网络一侧的一组层。

噪音

从概念上讲,VGG 模式的问题不在于我们不能建立越来越大的网络。如果我们有足够的 GPU 内存,我们当然可以在一段时间内复制/粘贴我们的块!VGG 式网络的主要局限是噪音。每个卷积都是破坏性的操作。如果每个卷积只丢失了很小一部分信息,比如 0.1%,那么 16 或 19 层上的效果就会开始复合,因为该效果会在每层中重新应用。

因此,ResNet 的第一个真正的技巧只是这些跳过连接将每组层的输入添加到最终层的输出。这为网络提供了更多的数据,以便为最终的预测步骤找到正确的卷积层组合。ResNet 的第二大绝招在最后。由于我们通过网络发送更多的数据,我们可以停止使用完全连接的层,而是使用平均池步骤来产生最终输出。

与我们之前一起启动的节点相比,这里的神经网络正在以与我们其他卷积相同的方式有效地学习这个输出层,这比完全连接的节点的计算成本低得多。这意味着评估我们的网络突然变得非常,非常便宜。因此,尽管我们在网络中增加了更多层,跳过连接意味着我们通过网络发送更多数据,但整个网络的评估速度实际上比我们的 VGG 网络快得多。

首先,我们的参数总数显著下降(大约是参数总数的四分之一)。此外,与我们完全连接的层相比,这种添加操作实际上非常便宜。这意味着网络更小更快。

ResNet 的第一层是 7x7 卷积,但这只是为了将我们的输入分解成更小的网络。最近的研究表明,有更好的方法来实现输入/头层(我们将在第十二章中讨论),所以请注意这可能不是最好的方法。话虽如此,由于当时的硬件限制,这是一种降低输入大小的廉价好方法,以便卷积神经网络能够完成其工作。

批量标准化

批量标准化:通过减少内部协变量变化来加速深度网络训练

> https://arxiv.org/abs/1502.03167

批量标准化是一项重要的培训技术,您应该知道。从概念上讲,它的工作原理是根据最近看到的数据的标准偏差对图层的输出进行归一化。当使用随机小批(我们的训练循环正在做的)时,这具有平滑梯度空间的有用属性,以便使我们的反向传播运行得更有效。因此,网络收敛更加顺畅,更新过程也快了一个数量级。从技术上讲,这个过程还会在训练过程中引入一些噪声,因此它有时也被认为是一种正则化技术。

密码

这将是我们第一个使用多个代码块的大型网络。第一个(head)块略有不同,因此我们有特定的逻辑来处理输入,然后所有其他内容都经过中间层,中间层是以编程方式生成的。这是一种模式,我们将从这里一次又一次地看到。

import Datasets import TensorFlow

struct ConvBN: Layer { var conv: Conv2D var norm: BatchNorm

init( filterShape: (Int, Int, Int, Int), strides: (Int, Int) = (1, 1), padding: Padding = .valid ) { self.conv = Conv2D(filterShape: filterShape, strides: strides, padding: padding) self.norm = BatchNorm(featureCount: filterShape.3) }

@differentiable public func forward(_ input: Tensor) -> Tensor { return input.sequenced(through: conv, norm) } }

struct ResidualBasicBlock: Layer { var layer1: ConvBN var layer2: ConvBN

init( featureCounts: (Int, Int, Int, Int), kernelSize: Int = 3, strides: (Int, Int) = (1, 1) ) { self.layer1 = ConvBN( filterShape: (kernelSize, kernelSize, featureCounts.0, featureCounts.1), strides: strides, padding: .same) self.layer2 = ConvBN( filterShape: (kernelSize, kernelSize, featureCounts.1, featureCounts.3), strides: strides, padding: .same) }

@differentiable public func forward(_ input: Tensor) -> Tensor { return layer2(relu(layer1(input))) } }

struct ResidualBasicBlockShortcut: Layer { var layer1: ConvBN var layer2: ConvBN var shortcut: ConvBN

init(featureCounts: (Int, Int, Int, Int), kernelSize: Int = 3) { self.layer1 = ConvBN( filterShape: (kernelSize, kernelSize, featureCounts.0, featureCounts.1), strides: (2, 2), padding: .same) self.layer2 = ConvBN( filterShape: (kernelSize, kernelSize, featureCounts.1, featureCounts.2), strides: (1, 1), padding: .same) self.shortcut = ConvBN( filterShape: (1, 1, featureCounts.0, featureCounts.3), strides: (2, 2), padding: .same) }

@differentiable public func forward(_ input: Tensor) -> Tensor { return layer2(relu(layer1(input))) + shortcut(input) } }

struct ResNet34: Layer { var l1: ConvBN var maxPool: MaxPool2D

var l2a = ResidualBasicBlock(featureCounts: (64, 64, 64, 64)) var l2b = ResidualBasicBlock(featureCounts: (64, 64, 64, 64)) var l2c = ResidualBasicBlock(featureCounts: (64, 64, 64, 64))

var l3a = ResidualBasicBlockShortcut(featureCounts: (64, 128, 128, 128)) var l3b = ResidualBasicBlock(featureCounts: (128, 128, 128, 128)) var l3c = ResidualBasicBlock(featureCounts: (128, 128, 128, 128)) var l3d = ResidualBasicBlock(featureCounts: (128, 128, 128, 128))

var l4a = ResidualBasicBlockShortcut(featureCounts: (128, 256, 256, 256)) var l4b = ResidualBasicBlock(featureCounts: (256, 256, 256, 256)) var l4c = ResidualBasicBlock(featureCounts: (256, 256, 256, 256)) var l4d = ResidualBasicBlock(featureCounts: (256, 256, 256, 256)) var l4e = ResidualBasicBlock(featureCounts: (256, 256, 256, 256)) var l4f = ResidualBasicBlock(featureCounts: (256, 256, 256, 256))

var l5a = ResidualBasicBlockShortcut(featureCounts: (256, 512, 512, 512)) var l5b = ResidualBasicBlock(featureCounts: (512, 512, 512, 512)) var l5c = ResidualBasicBlock(featureCounts: (512, 512, 512, 512))

var avgPool: AvgPool2D var flatten = Flatten() var classifier: Dense

init() { l1 = ConvBN(filterShape: (7, 7, 3, 64), strides: (2, 2), padding: .same) maxPool = MaxPool2D(poolSize: (3, 3), strides: (2, 2)) avgPool = AvgPool2D(poolSize: (7, 7), strides: (7, 7)) classifier = Dense(inputSize: 512, outputSize: 10) }

@differentiable public func forward(_ input: Tensor) -> Tensor { let inputLayer = maxPool(relu(l1(input))) let level2 = inputLayer.sequenced(through: l2a, l2b, l2c) let level3 = level2.sequenced(through: l3a, l3b, l3c, l3d) let level4 = level3.sequenced(through: l4a, l4b, l4c, l4d, l4e, l4f) let level5 = level4.sequenced(through: l5a, l5b, l5c) return level5.sequenced(through: avgPool, flatten, classifier) } }

let batchSize = 32 let epochCount = 30

let dataset = Imagenette(batchSize: batchSize, inputSize: .resized320, outputSize: 224) var model = ResNet34() let optimizer = SGD(for: model, learningRate: 0.002, momentum: 0.9)

print("Starting training...")

for (epoch, epochBatches) in dataset.training.prefix(epochCount).enumerated() { Context.local.learningPhase = .training for batch in epochBatches { let (images, labels) = (batch.data, batch.label) let (_, gradients) = valueWithGradient(at: model) { model -> Tensor in let logits = model(images) return softmaxCrossEntropy(logits: logits, labels: labels) } optimizer.update(&model, along: gradients) }

Context.local.learningPhase = .inference var testLossSum: Float = 0 var testBatchCount = 0 var correctGuessCount = 0 var totalGuessCount = 0 for batch in dataset.validation { let (images, labels) = (batch.data, batch.label) let logits = model(images) testLossSum += softmaxCrossEntropy(logits: logits, labels: labels).scalarized() testBatchCount += 1

let correctPredictions = logits.argmax(squeezingAxis: 1) .== labels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.label.shape[0]

}

let accuracy = Float(correctGuessCount) / Float(totalGuessCount) print( """ [Epoch (epoch+1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount)) """ ) }


### 结果

由于使用了更简单的卷积输出和残差块,该网络收敛得非常好,并且比以前的 VGG 网络训练/评估快得多。

Starting training... [Epoch 1] Accuracy: 217/500 (0.434) Loss: 2.118794 [Epoch 2] Accuracy: 194/500 (0.388) Loss: 2.0524213 [Epoch 3] Accuracy: 295/500 (0.59) Loss: 1.4818325 [Epoch 4] Accuracy: 177/500 (0.354) Loss: 2.1035159 [Epoch 5] Accuracy: 327/500 (0.654) Loss: 1.0758021 [Epoch 6] Accuracy: 278/500 (0.556) Loss: 1.680953 [Epoch 7] Accuracy: 327/500 (0.654) Loss: 1.3363588 [Epoch 8] Accuracy: 348/500 (0.696) Loss: 1.107703 [Epoch 9] Accuracy: 284/500 (0.568) Loss: 1.9379689 [Epoch 10] Accuracy: 350/500 (0.7) Loss: 1.2561296 [Epoch 11] Accuracy: 288/500 (0.576) Loss: 1.995267 [Epoch 12] Accuracy: 353/500 (0.706) Loss: 1.2237265 [Epoch 13] Accuracy: 342/500 (0.684) Loss: 1.4842949 [Epoch 14] Accuracy: 374/500 (0.748) Loss: 1.385373 [Epoch 15] Accuracy: 313/500 (0.626) Loss: 2.0999825 [Epoch 16] Accuracy: 368/500 (0.736) Loss: 1.1946388 [Epoch 17] Accuracy: 370/500 (0.74) Loss: 1.2470249 [Epoch 18] Accuracy: 382/500 (0.764) Loss: 1.1730658 [Epoch 19] Accuracy: 390/500 (0.78) Loss: 1.1377627 [Epoch 20] Accuracy: 392/500 (0.784) Loss: 1.0375359 [Epoch 21] Accuracy: 371/500 (0.742) Loss: 1.3912839 [Epoch 22] Accuracy: 379/500 (0.758) Loss: 1.2445369 [Epoch 23] Accuracy: 384/500 (0.768) Loss: 1.1650964 [Epoch 24] Accuracy: 365/500 (0.73) Loss: 1.4282515 [Epoch 25] Accuracy: 361/500 (0.722) Loss: 1.4129665 [Epoch 26] Accuracy: 376/500 (0.752) Loss: 1.3693335 [Epoch 27] Accuracy: 364/500 (0.728) Loss: 1.4527073 [Epoch 28] Accuracy: 376/500 (0.752) Loss: 1.3168014 [Epoch 29] Accuracy: 363/500 (0.726) Loss: 1.6024143 [Epoch 30] Accuracy: 383/500 (0.766) Loss: 1.1949569


### 支线任务

这超出了本书的范围,但是这种方法已经被证明可以扩展到非常大的网络。千层 ResNet 网络已经建立并在 CIFAR 数据集上成功训练。这种方法的一个稍微不同的变体叫做高速公路网络,也值得一看。这种跳跃连接方法自然地有助于将不同的块组合在一起,并且是许多现代神经网络方法的基础,这些方法使用残差网络将定制的块类型组合在一起,以解决越来越大的问题。

>高速公路网络

arxiv.org/abs/1505.00…


## 概述

我们已经了解了如何堆叠类似于 VGG 网络的卷积组,以构建更大的卷积网络。然后,通过在我们的层组之间添加剩余跳跃连接,我们可以使这种方法抵抗噪声,并且结果可以达到比以前更高的精确度。接下来,我们将看看如何稍微修改这种方法,以产生更好的结果!


# 六、ResNet 50

ResNet 50 对你来说是一个至关重要的网络。这是该领域许多学术研究的基础。许多不同的论文将他们的结果与 ResNet 50 基线进行比较,这是一个有价值的参考点。此外,我们可以轻松下载已经在 ImageNet 数据集上训练过的 ResNet 50 网络的权重,并修改最后几层(称为**再训练**或* *迁移学习* *)以快速生成模型来解决新问题。对于大多数问题,这是最好的着手方式,而不是试图发明新的网络或技术。构建一个定制的数据集,并使用数据扩充技术对其进行扩展,将比构建一个新的架构更有意义。

继续我们上一章末尾的思路,剩余网络的真正力量在于它允许我们以低廉的成本建立、评估和训练更大的网络。因此,我们不再需要坚持使用 3×3 卷积,而是可以开始引入不同的细胞类型。所以,让我们建造更强大的东西。我们将看看如何修改 ResNet 34 以产生 ResNet 50,这是一个在这个领域中你会反复遇到的坚固的现代架构。


## 瓶颈区块

我们将要介绍的是所谓的瓶颈块。从概念上讲,我们将从两个 3x3 卷积到一个堆栈,看起来像这样:1x1,3x3,1x1。从数学的角度来看,这实际上不如我们迄今为止使用的 3x3 方法强大。瓶颈模块允许我们做的第二件事是运行更多的瓶颈模块,因为实施 1x1 层更便宜。因此,使用这些瓶颈层,我们可以运行四倍多的过滤器,这就是为什么我认为它们最终会更强大。或者,换句话说,它们在技术上不那么强大,但在计算上也更便宜。这意味着我们可以在不显著增加计算预算的情况下使用更多的块,例如,完整的瓶颈块比两个块的 ResNet 34 3x3 堆栈大约贵 5%。因此,通过简单地替换这些单元,该网络能够产生比我们的 ResNet 34 网络更精确的结果。这是一个我们将在接下来的章节中深入探讨的概念。


## 密码

这个网络和 Resnet 34 之间唯一真正的区别是将事物转换成使用瓶颈层,然后将较大的参数输入到中间阶段。

```py

import Datasets import TensorFlow

struct ConvBN: Layer { var conv: Conv2D var norm: BatchNorm

init( filterShape: (Int, Int, Int, Int), strides: (Int, Int) = (1, 1), padding: Padding = .valid ) { self.conv = Conv2D(filterShape: filterShape, strides: strides, padding: padding) self.norm = BatchNorm(featureCount: filterShape.3) }

@differentiable public func forward(_ input: Tensor) -> Tensor { return input.sequenced(through: conv, norm) } }

struct ResidualConvBlock: Layer { var layer1: ConvBN var layer2: ConvBN var layer3: ConvBN var shortcut: ConvBN

init( featureCounts: (Int, Int, Int, Int), kernelSize: Int = 3, strides: (Int, Int) = (2, 2) ) { self.layer1 = ConvBN( filterShape: (1, 1, featureCounts.0, featureCounts.1), strides: strides) self.layer2 = ConvBN( filterShape: (kernelSize, kernelSize, featureCounts.1, featureCounts.2), padding: .same) self.layer3 = ConvBN(filterShape: (1, 1, featureCounts.2, featureCounts.3)) self.shortcut = ConvBN( filterShape: (1, 1, featureCounts.0, featureCounts.3), strides: strides, padding: .same) }

@differentiable public func forward(_ input: Tensor) -> Tensor { let tmp = relu(layer2(relu(layer1(input)))) return relu(layer3(tmp) + shortcut(input)) } }

struct ResidualIdentityBlock: Layer { var layer1: ConvBN var layer2: ConvBN var layer3: ConvBN

init(featureCounts: (Int, Int, Int, Int), kernelSize: Int = 3) { self.layer1 = ConvBN(filterShape: (1, 1, featureCounts.0, featureCounts.1)) self.layer2 = ConvBN( filterShape: (kernelSize, kernelSize, featureCounts.1, featureCounts.2), padding: .same) self.layer3 = ConvBN(filterShape: (1, 1, featureCounts.2, featureCounts.3)) }

@differentiable public func forward(_ input: Tensor) -> Tensor { let tmp = relu(layer2(relu(layer1(input)))) return relu(layer3(tmp) + input) } }

struct ResNet50: Layer { var l1: ConvBN var maxPool: MaxPool2D

var l2a = ResidualConvBlock(featureCounts: (64, 64, 64, 256), strides: (1, 1)) var l2b = ResidualIdentityBlock(featureCounts: (256, 64, 64, 256)) var l2c = ResidualIdentityBlock(featureCounts: (256, 64, 64, 256))

var l3a = ResidualConvBlock(featureCounts: (256, 128, 128, 512)) var l3b = ResidualIdentityBlock(featureCounts: (512, 128, 128, 512)) var l3c = ResidualIdentityBlock(featureCounts: (512, 128, 128, 512)) var l3d = ResidualIdentityBlock(featureCounts: (512, 128, 128, 512))

var l4a = ResidualConvBlock(featureCounts: (512, 256, 256, 1024)) var l4b = ResidualIdentityBlock(featureCounts: (1024, 256, 256, 1024)) var l4c = ResidualIdentityBlock(featureCounts: (1024, 256, 256, 1024)) var l4d = ResidualIdentityBlock(featureCounts: (1024, 256, 256, 1024)) var l4e = ResidualIdentityBlock(featureCounts: (1024, 256, 256, 1024)) var l4f = ResidualIdentityBlock(featureCounts: (1024, 256, 256, 1024))

var l5a = ResidualConvBlock(featureCounts: (1024, 512, 512, 2048)) var l5b = ResidualIdentityBlock(featureCounts: (2048, 512, 512, 2048)) var l5c = ResidualIdentityBlock(featureCounts: (2048, 512, 512, 2048))

var avgPool: AvgPool2D var flatten = Flatten() var classifier: Dense

init() { l1 = ConvBN(filterShape: (7, 7, 3, 64), strides: (2, 2), padding: .same) maxPool = MaxPool2D(poolSize: (3, 3), strides: (2, 2)) avgPool = AvgPool2D(poolSize: (7, 7), strides: (7, 7)) classifier = Dense(inputSize: 2048, outputSize: 10) }

@differentiable public func forward(_ input: Tensor) -> Tensor { let inputLayer = maxPool(relu(l1(input))) let level2 = inputLayer.sequenced(through: l2a, l2b, l2c) let level3 = level2.sequenced(through: l3a, l3b, l3c, l3d) let level4 = level3.sequenced(through: l4a, l4b, l4c, l4d, l4e, l4f) let level5 = level4.sequenced(through: l5a, l5b, l5c) return level5.sequenced(through: avgPool, flatten, classifier) } }

let batchSize = 32 let epochCount = 30

let dataset = Imagenette(batchSize: batchSize, inputSize: .resized320, outputSize: 224) var model = ResNet50() let optimizer = SGD(for: model, learningRate: 0.002, momentum: 0.9)

print("Starting training...")

for (epoch, epochBatches) in dataset.training.prefix(epochCount).enumerated() { Context.local.learningPhase = .training for batch in epochBatches { let (images, labels) = (batch.data, batch.label) let (_, gradients) = valueWithGradient(at: model) { model -> Tensor in let logits = model(images) return softmaxCrossEntropy(logits: logits, labels: labels) } optimizer.update(&model, along: gradients) }

Context.local.learningPhase = .inference var testLossSum: Float = 0 var testBatchCount = 0 var correctGuessCount = 0 var totalGuessCount = 0 for batch in dataset.validation { let (images, labels) = (batch.data, batch.label) let logits = model(images) testLossSum += softmaxCrossEntropy(logits: logits, labels: labels).scalarized() testBatchCount += 1

let correctPredictions = logits.argmax(squeezingAxis: 1) .== labels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.label.shape[0]

}

let accuracy = Float(correctGuessCount) / Float(totalGuessCount) print( """ [Epoch (epoch+1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount)) """ ) }

结果

通过以上设置,您应该能够在 Imagenette 上获得 75%以上的准确率,而无需任何数据扩充:

[Epoch 20] Accuracy: 362/500 (0.724) Loss: 1.4309547
[Epoch 21] Accuracy: 315/500 (0.63)  Loss: 2.2550986
[Epoch 22] Accuracy: 372/500 (0.744) Loss: 1.4735502
[Epoch 23] Accuracy: 345/500 (0.69)  Loss: 1.9369599
[Epoch 24] Accuracy: 359/500 (0.718) Loss: 2.0183568
[Epoch 25] Accuracy: 337/500 (0.674) Loss: 2.2227683
[Epoch 26] Accuracy: 369/500 (0.738) Loss: 1.4570786
[Epoch 27] Accuracy: 380/500 (0.76)  Loss: 1.3399329
[Epoch 28] Accuracy: 377/500 (0.754) Loss: 1.4157851
[Epoch 29] Accuracy: 357/500 (0.714) Loss: 1.8361444
[Epoch 30] Accuracy: 377/500 (0.754) Loss: 1.3033926

Side Quest: ImageNet

下面是我们如何在 ImageNet 数据集上使用 Swift for TensorFlow、随机梯度下降和 TrainingLoop API 训练 ResNet50 网络:

import Datasets import ImageClassificationModels import TensorFlow import TrainingLoop

// XLA mode can't load ImageNet, need to use eager mode to limit memory use let device = Device.defaultTFEager let dataset = ImageNet(batchSize: 32, outputSize: 224, on: device) var model = ResNet(classCount: 1000, depth: .resNet50)

// 0.1 for 30, .01 for 30, .001 for 30 let optimizer = SGD(for: model, learningRate: 0.1, momentum: 0.9) public func scheduleLearningRate<L: TrainingLoopProtocol>( _ loop: inout L, event: TrainingLoopEvent ) throws where L.Opt.Scalar == Float { if event == .epochStart { guard let epoch = loop.epochIndex else { return } if epoch > 30 { loop.optimizer.learningRate = 0.01 } if epoch > 60 { loop.optimizer.learningRate = 0.001 } if epoch > 80 { loop.optimizer.learningRate = 0.0001 } } }

var trainingLoop = TrainingLoop( training: dataset.training, validation: dataset.validation, optimizer: optimizer, lossFunction: softmaxCrossEntropy, metrics: [.accuracy], callbacks: [scheduleLearningRate])

try! trainingLoop.fit(&model, epochs: 90, on: device)

值得注意的是,swift-models 导入引入了 ResNet v1.5,这是实践中更常见的 ResNet 变体。关键区别在于 2x2 步幅从每组的第一个 ConvBN 移动到第二个 con vbn。来自 He 等人的另一篇论文是“深度剩余网络中的身份映射”( https://arxiv.org/abs/1603.05027 ),其有时被称为 ResNet v2 或预激活 ResNet,关键区别在于,在卷积运算之前完成批量归一化/激活步骤,并且去除了每个组中的最终激活。

概述

我们采用了上一章的 ResNet 34 模型,并通过添加瓶颈块对其进行了轻微修改。我们的 3x3 + 3x3 卷积已经被 1x1、3x3、1x1 风格的方法所取代,其中最后的 1x1 卷积具有四倍的层数。这使得我们的网络更大,从而提高了结果。不过,重要的是,这种方法的评估成本也很低,因此我们在计算方面以大致相同的成本获得了改进的结果。

这种剩余法可以与这一领域的许多其他方法相结合。不同组的卷积方法(称为**单元* *)可以使用残差堆栈结合在一起,以解决不同的问题。许多大规模强化学习技术(AlphaZero 是一个显著的例子)使用大量卷积层和残差网络。

如果你只从这本书里学一个网络,我觉得这是最适合你了解的一个。我们实际上已经花了六章来构建这种方法。接下来,我们将考察一些特定于移动设备的网络,尝试提供与我们的 ResNet 50 网络大致相似的结果,但在规模和复杂性方面成本显著降低。接下来,我们将尝试大幅缩减网络规模,以便构建能够在资源受限环境中运行的网络。

七、SqueezeNet

在接下来的几章中,我们将探讨专为运行在移动设备(主要是手机)上而设计的卷积神经网络。许多研究已经进入使用越来越大的计算机集群来构建更复杂的模型,以尝试和提高 ImageNet 问题的准确性。手机/边缘设备是机器学习的一个领域,尚未被深入探索,但在我看来极其重要。我们的直接目标是让设备在现实世界的设备上工作,但对我来说,特别有趣的是,在寻找降低高端方法的复杂性以实现更简单的方法的过程中,我们可以发现允许我们建立更大网络的技术。

基于上一章中瓶颈层的概念,我们将牺牲一些网络结果的质量来生产 SqueezeNet,这是一种可以在计算能力有限的设备上运行的微型神经网络,如电话。

SqueezeNet

几年前,康奈尔大学发表了一篇讨论 SqueezeNet 和 AlexNet 级精度的论文。

SqueezeNet: AlexNet 级精度,参数减少 50 倍,模型大小小于 0.5MB

> https://arxiv.org/abs/1602.07360

本文的目的是尽可能减小网络的规模。

有些技术不适用于现代手机,但许多想法对你来说是有价值的。从概念上来说,SqueezeNet 做的关键事情是使用上一章的瓶颈模块的一个更激进的版本,叫做 fire 模块。

消防模块

每个 fire 模块接受输入并将其压缩(例如,在开始时应用 1×1 卷积),然后以两种不同的方式将其扩展(例如,并行的 3×3 conv 和 1×1 conv),然后将这两个扩展层的结果连接在一起以产生最终结果。从概念上讲,在块的第二部分可以从中学习之前,数据会显著减少。这是一个破坏性的操作,但另一方面,它大大减少了网络中的参数数量。

密集连接的卷积网络

> https://arxiv.org/abs/1608.06993

将多组结果连接在一起是通过网络传递信息的一种有趣方式。Densenet 是同年晚些时候发表的一篇论文,该论文采用了 ResNet 网络方法,并使用 concat 运算符代替 add 运算来产生一个新的最先进的网络(尽管计算成本极高)。我们稍后将再次讨论这个想法。

由于我们已经减少了通过网络的数据量,SqueezeNet 做的另一件事是无用的 maxpool 操作,所以我们减少了这种破坏性的操作。

深度压缩

接下来,SqueezeNet 的作者应用了另一篇论文中的技术,使模型尽可能小:

深度压缩:通过剪枝、训练量化和霍夫曼编码压缩深度神经网络

> https://arxiv.org/abs/1510.00149

理解剪枝和量化作为模型压缩技术的一般概念是至关重要的。作者在顶层所做的具体优化对理解也是有价值的,但不是必需的。

模型修剪

我们可以做的另一件事是让模型运行得更快,这叫做网络修剪。从概念上讲,神经网络遵循 Zipf 定律的一种变体,而我们 20%的网络激活产生了 80%的结果。因此,如果我们愿意牺牲准确性,我们可以通过丢弃除了最受欢迎的节点之外的所有节点来轻松地构建一个明显更小的网络,这被称为稀疏化或修剪。

“深度压缩”论文采用了这一思想,但是在执行稀疏步骤之后重新训练网络。有趣的是,通过执行这个再训练步骤,我们可以得到一个和输入网络一样精确的最终网络。然后,通过应用 CRC 压缩方案(本文的特定方法),我们可以得到一个参数数量级更少的网络。

模型量化

接下来,我们可以将 32 位浮点数转换成 8 位整数权重,以便将它们的大小再减小 4 倍。这是一个非常常见的步骤,当生产更小的模型以在支持量化数学的设备上运行时,以及生产显著更小的模型以通过互联网发送到移动和嵌入式设备时。在最简单的模型量化形式中,浮点数表示为+–~10³⁸ 的范围,而整数 8 数学的范围为–128 到 127,我们只需将较大的浮点数映射到它们最接近的归一化整数。然而,这种方法的问题是,减少网络可用空间的过程通常是破坏性的,因此最终的网络在之后不能很好地工作(例如,事情工作得更快,但准确性显著降低)。

然而,如果我们有先见之明,将我们的网络最终将被量化的知识纳入其中,那么我们就可以修改我们的训练过程(技术术语是量化感知训练)来利用这一事实。与之前的模型修剪步骤类似,“深度压缩”论文对网络进行了量化,并再次对其进行了训练,以使量化过程的结果最小化。通过这样做,他们能够消除任何精度下降,但最终仍然得到一个明显更小的模型。

SqueezeNet 论文的最后一步是利用霍夫曼编码,这是一种无损压缩方案。结果,他们能够进一步压缩量子化网络。

尺寸度量

因此,在高水平上,这个网络产生的网络在 ImageNet 上的准确性与 AlexNet 相同,Alex net 是 2012 年最先进的计算机视觉网络。通过应用他们的模型压缩技术,他们能够将 AlexNet 的大小从 240MB 减少到 6.9MB,而没有损失准确性。通过使用 fire 模块来生产 SqueezeNet,他们能够在 ImageNet 数据集上实现与 AlexNet 相同的准确性,模型为 4.8MB,提高了 50 倍。然后,他们能够将他们的模型压缩技术应用于该模型,以产生. 47MB(不到半兆字节)的量化版本,但仍具有与原始模型和 AlexNet 相当的准确性。从概念上讲,SqueezeNet 能够实现与 AlexNet 相同质量的结果,而使用的参数减少了 510 倍,这是一个令人印象深刻的成就。

SqueezeNet 1.0 和 1.1 的区别

文献中有两个版本的 SqueezeNet,即 1.0 版和 1.1 版。两者的主要区别在于第一层,1.0 版使用 7x7 步距和 96 个滤镜,而 1.1 版使用 3x3 步距和 64 个滤镜。

密码

以下是 1.1 的演示。其中,1.1 版模型将其最大池层移至堆栈的更高层(例如,在第 1、3、5 层,而不是第 1、4、8 层)。这就产生了一个具有相同精度的网络,而运算量却减少了约 2.4 倍(例如,1.0 的运算量为 1.72 Gflops/image,而 1.1 的运算量为 0.72 Gflops/image)。

import TensorFlow

public struct Fire: Layer { public var squeeze: Conv2D public var expand1: Conv2D public var expand3: Conv2D

public init( inputFilterCount: Int, squeezeFilterCount: Int, expand1FilterCount: Int, expand3FilterCount: Int ) { squeeze = Conv2D( filterShape: (1, 1, inputFilterCount, squeezeFilterCount), activation: relu) expand1 = Conv2D( filterShape: (1, 1, squeezeFilterCount, expand1FilterCount), activation: relu) expand3 = Conv2D( filterShape: (3, 3, squeezeFilterCount, expand3FilterCount), padding: .same, activation: relu) }

@differentiable public func forward(_ input: Tensor) -> Tensor { let squeezed = squeeze(input) let expanded1 = expand1(squeezed) let expanded3 = expand3(squeezed) return expanded1.concatenated(with: expanded3, alongAxis: -1) } }

public struct SqueezeNet: Layer { public var inputConv = Conv2D( filterShape: (3, 3, 3, 64), strides: (2, 2), padding: .same, activation: relu) public var maxPool1 = MaxPool2D(poolSize: (3, 3), strides: (2, 2)) public var fire2 = Fire( inputFilterCount: 64, squeezeFilterCount: 16, expand1FilterCount: 64, expand3FilterCount: 64) public var fire3 = Fire( inputFilterCount: 128, squeezeFilterCount: 16, expand1FilterCount: 64, expand3FilterCount: 64) public var maxPool3 = MaxPool2D(poolSize: (3, 3), strides: (2, 2)) public var fire4 = Fire( inputFilterCount: 128, squeezeFilterCount: 32, expand1FilterCount: 128, expand3FilterCount: 128) public var fire5 = Fire( inputFilterCount: 256, squeezeFilterCount: 32, expand1FilterCount: 128, expand3FilterCount: 128) public var maxPool5 = MaxPool2D(poolSize: (3, 3), strides: (2, 2)) public var fire6 = Fire( inputFilterCount: 256, squeezeFilterCount: 48, expand1FilterCount: 192, expand3FilterCount: 192) public var fire7 = Fire( inputFilterCount: 384, squeezeFilterCount: 48, expand1FilterCount: 192, expand3FilterCount: 192) public var fire8 = Fire( inputFilterCount: 384, squeezeFilterCount: 64, expand1FilterCount: 256, expand3FilterCount: 256) public var fire9 = Fire( inputFilterCount: 512, squeezeFilterCount: 64, expand1FilterCount: 256, expand3FilterCount: 256) public var outputConv: Conv2D public var avgPool = AvgPool2D(poolSize: (13, 13), strides: (1, 1))

public init(classCount: Int = 10) { outputConv = Conv2D(filterShape: (1, 1, 512, classCount), strides: (1, 1), activation: relu) }

@differentiable public func forward(_ input: Tensor) -> Tensor { let convolved1 = input.sequenced(through: inputConv, maxPool1) let fired1 = convolved1.sequenced(through: fire2, fire3, maxPool3, fire4, fire5) let fired2 = fired1.sequenced(through: maxPool5, fire6, fire7, fire8, fire9) let output = fired2.sequenced(through: outputConv, avgPool) return output.reshaped(to: [input.shape[0], outputConv.filter.shape[3]]) } }

训练循环

将前面的代码放入名为 SqueezeNet.swift 的文件中,然后添加一个名为 main.swift 的训练循环:

import Datasets import TensorFlow

let batchSize = 128 let epochCount = 100

let dataset = Imagenette(batchSize: batchSize, inputSize: .resized320, outputSize: 224) var model = SqueezeNet()

let optimizer = SGD(for: model, learningRate: 0.0001, momentum: 0.9); print("sgd") //let optimizer = RMSProp(for: model, learningRate: 0.0001); print ("rmsprop") //let optimizer = Adam(for: model, learningRate: 0.0001); print ("adam")

print("Starting training...")

for (epoch, epochBatches) in dataset.training.prefix(epochCount).enumerated() { Context.local.learningPhase = .training for batch in epochBatches { let (images, labels) = (batch.data, batch.label) let (_, gradients) = valueWithGradient(at: model) { model -> Tensor in let logits = model(images) return softmaxCrossEntropy(logits: logits, labels: labels) } optimizer.update(&model, along: gradients) }

Context.local.learningPhase = .inference var testLossSum: Float = 0 var testBatchCount = 0 var correctGuessCount = 0 var totalGuessCount = 0 for batch in dataset.validation { let (images, labels) = (batch.data, batch.label) let logits = model(images) testLossSum += softmaxCrossEntropy(logits: logits, labels: labels).scalarized() testBatchCount += 1

let correctPredictions = logits.argmax(squeezingAxis: 1) .== labels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.label.shape[0]

}

let accuracy = Float(correctGuessCount) / Float(totalGuessCount) print( """ [Epoch (epoch+1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount)) """ ) }

展望未来,我们将只是更换型号,并以这种方式运行。如果你需要自定义训练参数,我会在这里注明。

结果

运行您的模型,您应该会得到如下结果:

... [Epoch 95 ] Accuracy: 79/500 (0.158) Loss: 2.3003228 [Epoch 96 ] Accuracy: 78/500 (0.156) Loss: 2.3002906 [Epoch 97 ] Accuracy: 78/500 (0.156) Loss: 2.300246 [Epoch 98 ] Accuracy: 79/500 (0.158) Loss: 2.3002024 [Epoch 99 ] Accuracy: 78/500 (0.156) Loss: 2.3001637 [Epoch 100] Accuracy: 80/500 (0.16) Loss: 2.3001184

为什么我们的网络性能很差?我们之所以只有 16%的准确率,是因为 SqueezeNet 是一个极难训练的网络。基本的 SGD 通常可以用于构建模型,但是要精确地训练这个模型,需要在优化器方面采用稍微复杂一点的方法。

> SGD + momentum + (optional) Nesterov smoothing

到目前为止,我们实际上还没有使用普通的 SGD 我们一直用 SGD +动量,也就是所谓的二阶方法。通过跟踪网络当前移动的路径,然后根据矢量的惯性(物理学)进行更新,我们有更高的概率不会被随机更新分散注意力。内斯特罗夫动量(可以用标志“nesterov: true”启用)通过数学平滑结合这两者的函数来改进这一过程。

RMSProp

> www.cs.toronto.edu/~tijmen/csc321/slides/ lecture_slides_lec6.pdf

这是从杰佛瑞·辛顿之前的课堂笔记中出现的,后来被亚历克斯·格雷夫斯写在了一篇论文中(见 https://arxiv.org/abs/1308.0850 )。在概念上,我们通过存储搜索空间的每个向量的梯度来代替 SGD +动量过程,然后更新过程是这些平方梯度的指数递减和。由于我们跟踪多个向量,这在处理稀疏网络时做得很好。

亚当

> https://arxiv.org/abs/1412.6980

亚当和它的变体可以被粗略地描述为将动量的概念与梯度空间的跟踪相结合,试图获得两个世界的最佳结果。不严格地说,在某些情况下,任何一种形式都难以融合。对于基于动量的方法,这发生在梯度搜索子空间非常颠簸的时候。同样,跟踪梯度可能会遇到所谓的消失梯度效应,搜索过程开始变得越来越慢,最终完全停止。

无论如何,选择一个非 SGD 方法来启用、运行网络,您的准确性应该会显著提高:

[Epoch 96 ] Accuracy: 378/500 (0.756) Loss: 0.7979737 [Epoch 97 ] Accuracy: 369/500 (0.738) Loss: 0.8244314 [Epoch 98 ] Accuracy: 387/500 (0.774) Loss: 0.74936193 [Epoch 99 ] Accuracy: 377/500 (0.754) Loss: 0.7717642 [Epoch 100] Accuracy: 379/500 (0.758) Loss: 0.7441697

在本书的其余部分,我们将继续使用 SGD 和一个小的(例如,0.002)学习率,但你应该知道前面的优化器,在基本随机方法失败的情况下尝试。

支线任务

如果你对优化感兴趣,Sebastian Ruder 有一篇不错的博文,你应该看看:

https://ruder.io/optimizing-gradient-descent/

概述

我们已经研究了 SqueezeNet,这是一个来自 2016 年的计算机视觉网络,与我们迄今为止研究的网络相比,它提供了很好的结果,周期和参数数量明显减少。然后,我们看了一些优化器调整,有时需要训练这些较小的网络。接下来,让我们看看一些围绕手机可用硬件设计的架构。

八、MobileNet v1

有一些有趣的尝试,让更小的模型运行在设备后 SqueezeNet。我们需要的是一个专门为移动设备设计的模型。谷歌的一组研究人员开发的东西被称为 MobileNet,这是一个重要的网络家族,我们将花几章时间来了解它。在高层次上,我们将使用深度方向可分卷积来产生一个比 SqueezeNet 更精确的网络,它在移动电话硬件上运行良好。

MobileNet (v1)

MobileNets:用于移动视觉应用的高效卷积神经网络

> https://arxiv.org/abs/1704.04861

专为在移动硬件上运行而设计的模型,更好地利用了参数+数据空间。

空间可分卷积

让我们再来看看我们的 Sobel 滤波器,在我们介绍卷积的那一章。在那里,我们把它看作是两个 3×3 的矩阵运算。但是如果我们在数学上聪明,我们可以把它简化为一个[3x1]和[1x3]的乘法。

这给了我们同样的结果,但有额外的属性,它可以更便宜地计算。我们的[3x 3][3x 3]组合最终需要 9 次运算,而我们的[3x 1][1x 3]只需要 6 次运算,减少了 33%!然而,并不是所有的内核都可以这样分解。

深度方向回旋

我们可以利用图像数据中的一个关键属性:颜色。我们有三个通道——红色、绿色、蓝色——每次评估我们的神经网络时,我们都在运行相同的过滤操作集。

我们可以为输入图像的每个区域创建单独的卷积滤波器组,通过颜色通道组合在一起。在学术设置中,通道也被称为深度,因此这些被称为深度方向卷积。你需要知道的另一种方法是增加滤波器输出的数量,称为通道乘法器。

逐点卷积

这只是谜题的一半;我们仍然需要将我们的渠道数据重新组合在一起。在关于 SqueezeNet 的上一章中,我们讨论了如何在应用 3×3 卷积之前,将 1×1 卷积放入堆栈中,以显著减少数据量。从概念上讲,这被称为逐点卷积,因为所有通道输入数据都要通过它。通过使用这些逐点卷积,我们可以将减少的数据空间映射回我们期望的最终过滤器大小。然后,我们只需要增加逐点操作符的数量,以匹配我们想要的输出过滤器的数量。

从概念上讲,我们获取输入图像并运行多组深度方向卷积,然后使用一堆小的点方向卷积将它们组合回我们想要的输出形状。滤波器的这种组合称为深度方向可分离卷积,是该网络性能的关键。我们已经获得了 SqueezeNet 压缩方法的大部分好处,但破坏性比 SqueezeNet 小。此外,我们现在使用更便宜的运算,因为深度可分卷积可以在移动硬件中加速。

ReLU 6

到目前为止,我们已经为我们的模型使用了一个 ReLU 激活函数,如下所示:

relu(x) = max(features, 0)

当构建我们知道将要量化的模型时,限制 ReLU 层的输出并通过扩展迫使网络从一开始就使用较小的数字是有价值的。因此,我们简单地为我们的 ReLU 激活引入一个上限函数,如下所示:

relu6(x) = min(max(features, 0), 6)

现在,我们可以简化我们的输出逻辑,以利用这一减少的空间。

使用这种方法减少 MAC 的示例

代表性深度神经网络架构的基准分析

> https://arxiv.org/abs/1810.00736

这篇文章的第 3 页有一张漂亮的图表,直观地展示了这些网络之间的差异。从概念上讲,我们有一个比 SqueezeNet 稍大的网络,但我们有一个与 ResNet 18(早期的 ResNet 34 的较小版本)相当的顶级精度。如果你想知道我们接下来要去哪里,看看 VGG16 与 MobileNet v2 的对比。

密码

这个网络比我们的 SqueezeNet 方法使用了更多类型的层,但产生了明显更好的结果,因为它们在计算上更便宜。这是我们将会反复看到的事情。

import TensorFlow

public struct ConvBlock: Layer { public var zeroPad = ZeroPadding2D(padding: ((0, 1), (0, 1))) public var conv: Conv2D public var batchNorm: BatchNorm

public init(filterCount: Int, strides: (Int, Int)) { conv = Conv2D( filterShape: (3, 3, 3, filterCount), strides: strides, padding: .valid) batchNorm = BatchNorm(featureCount: filterCount) }

@differentiable public func forward(_ input: Tensor) -> Tensor { let convolved = input.sequenced(through: zeroPad, conv, batchNorm) return relu6(convolved) } }

public struct DepthwiseConvBlock: Layer { @noDerivative let strides: (Int, Int) @noDerivative public let zeroPad = ZeroPadding2D(padding: ((0, 1), (0, 1)))

public var dConv: DepthwiseConv2D public var batchNorm1: BatchNorm public var conv: Conv2D public var batchNorm2: BatchNorm

public init( filterCount: Int, pointwiseFilterCount: Int, strides: (Int, Int) ) { self.strides = strides dConv = DepthwiseConv2D( filterShape: (3, 3, filterCount, 1), strides: strides, padding: strides == (1, 1) ? .same : .valid) batchNorm1 = BatchNorm( featureCount: filterCount) conv = Conv2D( filterShape: ( 1, 1, filterCount, pointwiseFilterCount ), strides: (1, 1), padding: .same) batchNorm2 = BatchNorm(featureCount: pointwiseFilterCount) }

@differentiable public func forward(_ input: Tensor) -> Tensor { var convolved1: Tensor if self.strides == (1, 1) { convolved1 = input.sequenced(through: dConv, batchNorm1) } else { convolved1 = input.sequenced(through: zeroPad, dConv, batchNorm1) } let convolved2 = relu6(convolved1) let convolved3 = relu6(convolved2.sequenced(through: conv, batchNorm2)) return convolved3 } }

public struct MobileNetV1: Layer { @noDerivative let classCount: Int @noDerivative let scaledFilterShape: Int

public var convBlock1: ConvBlock public var dConvBlock1: DepthwiseConvBlock public var dConvBlock2: DepthwiseConvBlock public var dConvBlock3: DepthwiseConvBlock public var dConvBlock4: DepthwiseConvBlock public var dConvBlock5: DepthwiseConvBlock public var dConvBlock6: DepthwiseConvBlock public var dConvBlock7: DepthwiseConvBlock public var dConvBlock8: DepthwiseConvBlock public var dConvBlock9: DepthwiseConvBlock public var dConvBlock10: DepthwiseConvBlock public var dConvBlock11: DepthwiseConvBlock public var dConvBlock12: DepthwiseConvBlock public var dConvBlock13: DepthwiseConvBlock public var avgPool = GlobalAvgPool2D() public var dropoutLayer: Dropout public var outputConv: Conv2D

public init( classCount: Int = 10, dropout: Double = 0.001 ) { self.classCount = classCount scaledFilterShape = Int(1024.0 * 1.0)

convBlock1 = ConvBlock(filterCount: 32, strides: (2, 2))
dConvBlock1 = DepthwiseConvBlock(
  filterCount: 32,
  pointwiseFilterCount: 64,
  strides: (1, 1))
dConvBlock2 = DepthwiseConvBlock(
  filterCount: 64,
  pointwiseFilterCount: 128,
  strides: (2, 2))
dConvBlock3 = DepthwiseConvBlock(
  filterCount: 128,
  pointwiseFilterCount: 128,
  strides: (1, 1))
dConvBlock4 = DepthwiseConvBlock(
  filterCount: 128,
  pointwiseFilterCount: 256,
  strides: (2, 2))
dConvBlock5 = DepthwiseConvBlock(
  filterCount: 256,
  pointwiseFilterCount: 256,
  strides: (1, 1))
dConvBlock6 = DepthwiseConvBlock(
  filterCount: 256,
  pointwiseFilterCount: 512,
  strides: (2, 2))
dConvBlock7 = DepthwiseConvBlock(
  filterCount: 512,
  pointwiseFilterCount: 512,
  strides: (1, 1))
dConvBlock8 = DepthwiseConvBlock(
  filterCount: 512,
  pointwiseFilterCount: 512,
  strides: (1, 1))
dConvBlock9 = DepthwiseConvBlock(
  filterCount: 512,
  pointwiseFilterCount: 512,
  strides: (1, 1))
dConvBlock10 = DepthwiseConvBlock(
  filterCount: 512,
  pointwiseFilterCount: 512,
  strides: (1, 1))
dConvBlock11 = DepthwiseConvBlock(
  filterCount: 512,
  pointwiseFilterCount: 512,
  strides: (1, 1))
dConvBlock12 = DepthwiseConvBlock(
  filterCount: 512,
  pointwiseFilterCount: 1024,
  strides: (2, 2))
dConvBlock13 = DepthwiseConvBlock(
  filterCount: 1024,
  pointwiseFilterCount: 1024,
  strides: (1, 1))

dropoutLayer = Dropout<Float>(probability: dropout)
outputConv = Conv2D<Float>(
  filterShape: (1, 1, scaledFilterShape, classCount),
  strides: (1, 1),
  padding: .same)

}

@differentiable public func forward(_ input: Tensor) -> Tensor { let convolved = input.sequenced( through: convBlock1, dConvBlock1, dConvBlock2, dConvBlock3, dConvBlock4) let convolved2 = convolved.sequenced( through: dConvBlock5, dConvBlock6, dConvBlock7, dConvBlock8, dConvBlock9) let convolved3 = convolved2.sequenced( through: dConvBlock10, dConvBlock11, dConvBlock12, dConvBlock13, avgPool ).reshaped(to: [ input.shape[0], 1, 1, scaledFilterShape, ]) let convolved4 = convolved3.sequenced(through: dropoutLayer, outputConv) return convolved4.reshaped(to: [input.shape[0], classCount]) } }

结果

我们的结果与之前的 Resnet 50 网络相当,但这个网络总体上更小,并且可以在运行时更快地进行评估,因此对于移动设备来说是一个坚实的改进。

Starting training...
[Epoch 1 ] Accuracy: 50/500  (0.1)   Loss: 2.5804458
[Epoch 2 ] Accuracy: 262/500 (0.524) Loss: 1.5034955
[Epoch 3 ] Accuracy: 224/500 (0.448) Loss: 1.928577
[Epoch 4 ] Accuracy: 286/500 (0.572) Loss: 1.4074985
[Epoch 5 ] Accuracy: 306/500 (0.612) Loss: 1.3206513
[Epoch 6 ] Accuracy: 334/500 (0.668) Loss: 1.0112444
[Epoch 7 ] Accuracy: 362/500 (0.724) Loss: 0.8360394
[Epoch 8 ] Accuracy: 343/500 (0.686) Loss: 1.0489439
[Epoch 9 ] Accuracy: 317/500 (0.634) Loss: 1.6159635
[Epoch 10] Accuracy: 338/500 (0.676) Loss: 1.0420185
[Epoch 11] Accuracy: 354/500 (0.708) Loss: 1.0034739
[Epoch 12] Accuracy: 358/500 (0.716) Loss: 0.9746185
[Epoch 13] Accuracy: 344/500 (0.688) Loss: 1.152486
[Epoch 14] Accuracy: 365/500 (0.73)  Loss: 0.96197647
[Epoch 15] Accuracy: 353/500 (0.706) Loss: 1.2438473
[Epoch 16] Accuracy: 367/500 (0.734) Loss: 1.044013
[Epoch 17] Accuracy: 365/500 (0.73)  Loss: 1.1098087
[Epoch 18] Accuracy: 352/500 (0.704) Loss: 1.3609929
[Epoch 19] Accuracy: 376/500 (0.752) Loss: 1.2861694
[Epoch 20] Accuracy: 376/500 (0.752) Loss: 1.0280938
[Epoch 21] Accuracy: 369/500 (0.738) Loss: 1.1655327
[Epoch 22] Accuracy: 369/500 (0.738) Loss: 1.1702954
[Epoch 23] Accuracy: 363/500 (0.726) Loss: 1.151112
[Epoch 24] Accuracy: 378/500 (0.756) Loss: 0.94088197

[Epoch 25] Accuracy: 386/500 (0.772) Loss: 1.03443
[Epoch 26] Accuracy: 379/500 (0.758) Loss: 1.1582794
[Epoch 27] Accuracy: 384/500 (0.768) Loss: 1.1210178
[Epoch 28] Accuracy: 377/500 (0.754) Loss: 1.136668
[Epoch 29] Accuracy: 382/500 (0.764) Loss: 1.2300915
[Epoch 30] Accuracy: 381/500 (0.762) Loss: 1.0231776

概述

我们已经研究了 MobileNet,这是一个来自 2017 年的重要计算机视觉网络,它大量使用深度方向可分离卷积,以便以显著降低的尺寸和计算预算产生与 ResNet 18(我们的 ResNet 34 网络的较小版本)相当的结果。我们可以用现在的硬件在手机上以接近实时的速度(例如,50 毫秒/预测速度)运行这个程序。接下来,让我们看看如何稍微调整我们的 MobileNet 网络,以产生更好的结果。