TensorFlow-CNN-与-Swift-教程-全-
TensorFlow CNN 与 Swift 教程(全)
一、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: "https://github.com/tensorflow/swift-models.git", .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
var hiddenLayer = Den se
var outputLayer = Dense
@differentiable
public func forward(_ input: 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
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 个节点(位数,0–9)。
最后是一个转发函数,这就是我们的神经网络逻辑神奇之处。我们实际上采取的输入,运行它通过展平,密度 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
var conv1b = Conv2D
var pool1 = MaxPool2D
var flatten = Flatten
var inputLayer = Dense
var hiddenLayer = Dense
var outputLayer = Dense
@differentiable
public func forward(_ input: 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
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 年。我们使用稍微不同的架构来简化以后向更高级模型的过渡,但是您应该看看这篇文章。
>基于梯度的学习应用于文档识别
## 概述
我们已经通过一些不同的例子研究了卷积是如何工作的。我们已经了解了**跨步**和*填充* *如何跨越输入图像。然后,我们看了**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
var conv1b = Conv2D
var pool1 = MaxPool2D
var conv2a = Conv2D
var conv2b = Conv2D
var pool2 = MaxPool2D
var flatten = Flatten
var inputLayer = Dense
var hiddenLayer = Dense
var outputLayer = Dense
@differentiable
func forward(_ input: 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
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 像素)。
日期增加
一般来说,图像识别/深度神经网络中一个非常重要的主题是**数据增强* *,我们在本书中基本上会跳过它,因为我想避免让这个领域的新手感到事情复杂。但是,在继续之前,让我们在这里简单地讨论一下。
我们可以想象增加我们的神经网络的规模,直到我们有一个“完美”的神经网络,对于我们展示的每一幅图像,它都给我们正确的结果。关键的概念是,我们使用的优化函数试图最小化我们给它的数据集的损失。所以,我们对这个“完美”网络的优化函数已经到了零(它从不出错),正如我们所希望的那样。听起来很棒,让我们写一篇论文并收集我们的奖品吧!
但是等等!在我们这样做之前,我们可能会尝试,比如说,水平翻转我们的猫图片,然后将这个新图片交给我们的神经网络。会发生什么?基本上,我们正在向我们的神经网络展示一幅前所未见的画面,所以结果充其量是半随机的。结果可能是,我们翻转的猫的“最接近”输入图像(在神经网络的搜索空间中)是一张狗的图片,因此当我们的网络看到这张新图片时,它会说“狗”。
然后,数据扩充(以及一般的训练深度神经网络)的基本思想是确保我们不会过度拟合(例如,过于接近我们的训练数据集)以至于我们无法* 概括 *(例如,对我们从未见过的数据正确地做出新的预测)。有几个基本方法:
-
收集更多数据!你不会在学术竞赛/目的中看到这种情况,但许多现实世界的机器学习涉及到获取或构建更大的数据集,以确保我们的网络不是真的在猜测新的条件,而是已经“看到”了一个相当类似的例子。同样,另一个常见的问题是只给我们的网络提供灰色猫的图片,然后当它看到橙色猫时,它不知道该怎么办。如果都是同一只猫,那么有很多例子对我们没有太大帮助!这个问题的另一个常见版本是获得与我们想要最终分类的不同的训练示例,例如,从互联网上训练照片,然后尝试将它们应用到现实世界的相机输入。只要有可能,就使用最终要测试的相同数据。同样,只要你能,收集更多的数据!
-
数据扩充:我们可以使用计算机对我们的数据进行各种修改,以增加我们总体的样本数量。一些常见的例子:
-
我们可以将图像从左向右翻转。
-
改变我们的亮度(伽玛)。
-
旋转我们的图像。
-
随机裁剪(切掉图片的边缘)。
-
随机缩放(使我们的图片变大,然后切掉现在更大的边缘)。
-
通常,这些方法也会结合起来,以确保神经网络也能获得尽可能多的训练数据变量。重要的一点是,这些方法经常变得特定于领域。或者说,我们可以翻转猫/狗的图片,但不能翻转字母表中的字母!
- 给我们的网络增加噪音:另一个极其重要的方面是给我们的运营增加噪音,以确保我们的网络不会过于依赖特定的输入/图像。这是一项非常有价值的技术,通过使我们的网络对噪声具有鲁棒性来提高现实世界的性能。有一个重要的相关研究领域叫做对抗性输入,它试图通过引入细微的噪声来欺骗分类器,从而欺骗网络。
这里有一些关于这个主题的有趣论文,你可以看看:
退出:防止神经网络过度拟合的简单方法
> 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
var conv1b = Conv2D
var pool1 = MaxPool2D
var conv2a = Conv2D
var conv2b = Conv2D
var pool2 = MaxPool2D
var conv3a = Conv2D
var conv3b = Conv2D
var conv3c = Conv2D
var pool3 = MaxPool2D
var conv4a = Conv2D
var conv4b = Conv2D
var conv4c = Conv2D
var pool4 = MaxPool2D
var conv5a = Conv2D
var conv5b = Conv2D
var conv5c = Conv2D
var pool5 = MaxPool2D
var flatten = Flatten
var inputLayer = Dense
var hiddenLayer = Dense
var outputLayer = Dense
@differentiable
public func forward(_ input: 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
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
22
3 libswiftCore.so 0x00007fcb7445c147
33 libswiftCore.so 0x00007fcb745fc310 valueWithPullback<A, B>(at:in:) + 106
34 libswiftTensorFlow.so 0x00007fcb74bb9e20 valueWithGradient<A, B>(at:in:) + 1073
35 VGG-Imagewoof 0x000055a5370311ed
57
36 libc.so.6 0x00007fcb5d6d6ab0
libc_start_main + 231
37 VGG-Imagewoof 0x000055a536bf90ba
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
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
return input.sequenced(through: conv1a, conv1b, pool1)
}
}
struct VGGBlock3: Layer {
var conva: Conv2D
var convb: Conv2D
var convc: Conv2D
var pool = MaxPool2D
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
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
var hiddenLayer = Dense
var outputLayer = Dense
@differentiable
public func forward(_ input: 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
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
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
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
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
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 数据集上成功训练。这种方法的一个稍微不同的变体叫做高速公路网络,也值得一看。这种跳跃连接方法自然地有助于将不同的块组合在一起,并且是许多现代神经网络方法的基础,这些方法使用残差网络将定制的块类型组合在一起,以解决越来越大的问题。
>高速公路网络
## 概述
我们已经了解了如何堆叠类似于 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
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
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
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
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
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
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
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
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
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
public init(classCount: Int = 10) {
outputConv = Conv2D(filterShape: (1, 1, 512, classCount), strides: (1, 1), activation: relu)
}
@differentiable
public func forward(_ input: 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
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
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
}
@differentiable
public func forward(_ input: 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
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
}
@differentiable
public func forward(_ input: 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
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 网络,以产生更好的结果。
九、MobileNet v2
在这一章中,我们将看看如何修改我们的 MobileNet v1 方法来产生 MobileNet v2,它稍微更精确并且计算成本更低。该网络于 2018 年问世,并交付了 v1 架构的改进版本。
MobileNetV2:反向残差和线性瓶颈
> https://arxiv.org/abs/1801.04381
Google 团队在本文中介绍的关键概念是反向剩余块和线性瓶颈层,所以让我们看看它们是如何工作的。
反向残差块
在我们之前的 ResNet 50 瓶颈块中,我们在每个组的初始层中通过 1x1 卷积来传递输入层,这减少了此时的数据。在将数据通过昂贵的 3×3 卷积后,我们使用 1×1 卷积来扩展滤波器的数量。
在反向残差块中,这是 MobileNet v2 所使用的,我们改为使用初始的 1x1 卷积来增加我们的网络深度,然后应用 MobileNet v1 的深度方向卷积,然后使用 1x1 卷积来压缩我们的网络。
反向跳跃连接
在我们的 ResNet 网络中,我们应用了跳过连接(例如,add 操作)将数据从输入层传递到输出层。MobileNet v2 以一种稍微不同的方式来实现这一点,只在输入和输出数量相同的块上执行这一操作(例如,不是每个堆栈的第一个块,而是其余块之间的块)。这意味着这个网络不像原来的 ResNet 那样连接紧密,通过的数据也更少,但从另一方面来说,评估它要便宜得多。
线性瓶颈层
下一个微妙的调整与我们的反向跳跃连接有关。在原始的 ResNet 网络中,我们将 ReLU 激活函数应用于瓶颈层的组合输出和输入。有趣的是,MobileNet v2 的作者发现我们可以消除这种激活功能,提高网络的性能。这种激活就变成了一个线性函数,所以他们称这个结果为线性瓶颈函数。
密码
对于这个网络,我们将使用我们的块操作符来生成子图层(例如,InvertedBottleneckBlockStack)。从概念上来说,与我们的 MobileNet v1 架构的主要区别在于,我们在残差块中添加了深度方向的 conv,以及我们计算每一遍梯度的反向方法。
import TensorFlow
public struct InitialInvertedBottleneckBlock: Layer {
public var dConv: DepthwiseConv2D
public var batchNormDConv: BatchNorm
public var conv2: Conv2D
public var batchNormConv: BatchNorm
public init(filters: (Int, Int)) {
dConv = DepthwiseConv2D
filterShape: (3, 3, filters.0, 1),
strides: (1, 1),
padding: .same)
conv2 = Conv2D
filterShape: (1, 1, filters.0, filters.1),
strides: (1, 1),
padding: .same)
batchNormDConv = BatchNorm(featureCount: filters.0)
batchNormConv = BatchNorm(featureCount: filters.1)
}
@differentiable
public func forward(_ input: Tensor
let depthwise = relu6(batchNormDConv(dConv(input)))
return batchNormConv(conv2(depthwise))
}
}
public struct InvertedBottleneckBlock: Layer {
@noDerivative public var addResLayer: Bool
@noDerivative public var strides: (Int, Int)
@noDerivative public let zeroPad = ZeroPadding2D
public var conv1: Conv2D
public var batchNormConv1: BatchNorm
public var dConv: DepthwiseConv2D
public var batchNormDConv: BatchNorm
public var conv2: Conv2D
public var batchNormConv2: BatchNorm
public init(
filters: (Int, Int),
depthMultiplier: Int = 6,
strides: (Int, Int) = (1, 1)
) {
self.strides = strides
self.addResLayer = filters.0 == filters.1 && strides == (1, 1)
let hiddenDimension = filters.0 * depthMultiplier
conv1 = Conv2D<Float>(
filterShape: (1, 1, filters.0, hiddenDimension),
strides: (1, 1),
padding: .same)
dConv = DepthwiseConv2D<Float>(
filterShape: (3, 3, hiddenDimension, 1),
strides: strides,
padding: strides == (1, 1) ? .same : .valid)
conv2 = Conv2D<Float>(
filterShape: (1, 1, hiddenDimension, filters.1),
strides: (1, 1),
padding: .same)
batchNormConv1 = BatchNorm(featureCount: hiddenDimension)
batchNormDConv = BatchNorm(featureCount: hiddenDimension)
batchNormConv2 = BatchNorm(featureCount: filters.1)
}
@differentiable
public func forward(_ input: Tensor
let pointwise = relu6(batchNormConv1(conv1(input)))
var depthwise: Tensor
if self.strides == (1, 1) {
depthwise = relu6(batchNormDConv(dConv(pointwise)))
} else {
depthwise = relu6(batchNormDConv(dConv(zeroPad(pointwise))))
}
let pointwiseLinear = batchNormConv2(conv2(depthwise))
if self.addResLayer {
return input + pointwiseLinear
} else {
return pointwiseLinear
}
}
}
public struct InvertedBottleneckBlockStack: Layer {
var blocks: [InvertedBottleneckBlock] = []
public init(
filters: (Int, Int),
blockCount: Int,
initialStrides: (Int, Int) = (2, 2)
) {
self.blocks = [
InvertedBottleneckBlock(
filters: (filters.0, filters.1),
strides: initialStrides)
]
for _ in 1..<blockCount {
self.blocks.append(
InvertedBottleneckBlock(
filters: (filters.1, filters.1))
)
}
}
@differentiable
public func forward(_ input: Tensor
return blocks.differentiableReduce(input) { 0) }
}
}
public struct MobileNetV2: Layer {
@noDerivative public let zeroPad = ZeroPadding2D
public var inputConv: Conv2D
public var inputConvBatchNorm: BatchNorm
public var initialInvertedBottleneck: InitialInvertedBottleneckBlock
public var residualBlockStack1: InvertedBottleneckBlockStack
public var residualBlockStack2: InvertedBottleneckBlockStack
public var residualBlockStack3: InvertedBottleneckBlockStack
public var residualBlockStack4: InvertedBottleneckBlockStack
public var residualBlockStack5: InvertedBottleneckBlockStack
public var invertedBottleneckBlock16: InvertedBottleneckBlock
public var outputConv: Conv2D
public var outputConvBatchNorm: BatchNorm
public var avgPool = GlobalAvgPool2D
public var outputClassifier: Dense
public init(classCount: Int = 10) {
inputConv = Conv2D
filterShape: (3, 3, 3, 32),
strides: (2, 2),
padding: .valid)
inputConvBatchNorm = BatchNorm(
featureCount: 32)
initialInvertedBottleneck = InitialInvertedBottleneckBlock(
filters: (32, 16))
residualBlockStack1 = InvertedBottleneckBlockStack(filters: (16, 24), blockCount: 2)
residualBlockStack2 = InvertedBottleneckBlockStack(filters: (24, 32), blockCount: 3)
residualBlockStack3 = InvertedBottleneckBlockStack(filters: (32, 64), blockCount: 4)
residualBlockStack4 = InvertedBottleneckBlockStack(
filters: (64, 96), blockCount: 3,
initialStrides: (1, 1))
residualBlockStack5 = InvertedBottleneckBlockStack(filters: (96, 160), blockCount: 3)
invertedBottleneckBlock16 = InvertedBottleneckBlock(filters: (160, 320))
outputConv = Conv2D<Float>(
filterShape: (1, 1, 320, 1280),
strides: (1, 1),
padding: .same)
outputConvBatchNorm = BatchNorm(featureCount: 1280)
outputClassifier = Dense(inputSize: 1280, outputSize: classCount)
}
@differentiable
public func forward(_ input: Tensor
let convolved = relu6(input.sequenced(through: zeroPad, inputConv, inputConvBatchNorm))
let initialConv = initialInvertedBottleneck(convolved)
let backbone = initialConv.sequenced(
through: residualBlockStack1, residualBlockStack2, residualBlockStack3,
residualBlockStack4, residualBlockStack5)
let output = relu6(outputConvBatchNorm(outputConv(invertedBottleneckBlock16(backbone))))
return output.sequenced(through: avgPool, outputClassifier)
}
}
### 结果
使用相同的训练循环和基本设置,该网络的性能优于我们的 MobileNet v1 架构。
Starting training...
[Epoch 1 ] Accuracy: 50/500 (0.1) Loss: 3.0107288
[Epoch 2 ] Accuracy: 276/500 (0.552) Loss: 1.4318728
[Epoch 3 ] Accuracy: 324/500 (0.648) Loss: 1.2038971
[Epoch 4 ] Accuracy: 337/500 (0.674) Loss: 1.1165649
[Epoch 5 ] Accuracy: 347/500 (0.694) Loss: 0.9973701
[Epoch 6 ] Accuracy: 363/500 (0.726) Loss: 0.9118728
[Epoch 7 ] Accuracy: 310/500 (0.62) Loss: 1.2533528
[Epoch 8 ] Accuracy: 372/500 (0.744) Loss: 0.797099
[Epoch 9 ] Accuracy: 368/500 (0.736) Loss: 0.8001915
[Epoch 10] Accuracy: 350/500 (0.7) Loss: 1.1580966
[Epoch 11] Accuracy: 372/500 (0.744) Loss: 0.84680176
[Epoch 12] Accuracy: 358/500 (0.716) Loss: 1.1446275
[Epoch 13] Accuracy: 388/500 (0.776) Loss: 0.90346915
[Epoch 14] Accuracy: 394/500 (0.788) Loss: 0.82173353
[Epoch 15] Accuracy: 365/500 (0.73) Loss: 0.9974839
[Epoch 16] Accuracy: 359/500 (0.718) Loss: 1.2463648
[Epoch 17] Accuracy: 333/500 (0.666) Loss: 1.5243211
[Epoch 18] Accuracy: 390/500 (0.78) Loss: 0.8723967
[Epoch 19] Accuracy: 383/500 (0.766) Loss: 1.0088551
[Epoch 20] Accuracy: 372/500 (0.744) Loss: 1.1002765
[Epoch 21] Accuracy: 392/500 (0.784) Loss: 0.9233314
[Epoch 22] Accuracy: 395/500 (0.79) Loss: 0.9421617
[Epoch 23] Accuracy: 367/500 (0.734) Loss: 1.1607682
[Epoch 24] Accuracy: 372/500 (0.744) Loss: 1.1685853
[Epoch 25] Accuracy: 375/500 (0.75) Loss: 1.1443601
[Epoch 26] Accuracy: 389/500 (0.778) Loss: 1.0197723
[Epoch 27] Accuracy: 392/500 (0.784) Loss: 1.0215062
[Epoch 28] Accuracy: 387/500 (0.774) Loss: 1.1886547
[Epoch 29] Accuracy: 400/500 (0.8) Loss: 0.9691738
[Epoch 30] Accuracy: 383/500 (0.766) Loss: 1.1193326
## 概述
我们已经研究了 MobileNet v2,这是一个从 2018 年开始的最先进的网络,用于在计算能力有限的设备(如电话)上执行图像识别。接下来,让我们看看如何通过强化学习获得更好的结果!
# 十、EfficientNet
EfficientNet 是图像识别的最新技术。我怀疑这种情况会永远保持下去,但我不相信它会被轻易取代。它是该领域多年研究的成果,结合了多种不同的技术。我对这个网络特别感兴趣的是,我们看到为移动设备开发的技术在更大的计算机视觉社区中有应用。或者说,为资源受限的设备构建模型的研究正在推动云计算的发展,而历史上的情况恰恰相反。
在高层次上,EfficientNet 是使用 MobileNetV2 的反向剩余块作为架构类型并结合 MnasNet 搜索策略创建的。这些较小的块在 MnasNet 创建时并不存在,通过使用它们,研究人员能够找到一组显著改进的网络。此外,在给定初始起点的情况下,他们能够找到一组可靠的可扩展试探法来构建更大的网络,这是我们在本章前面看到的进化策略的关键限制。
此外,研究人员还添加了其他论文中的两个重要概念:swish 激活功能和 SE(挤压和激发)块。
## 嗖嗖
ReLU 函数,我们在第一章介绍过,并不是唯一尝试过的激活函数。无论是在数学层面还是硬件层面,它们的实现都非常简单,性能也非常好,可以说经受住了时间的考验。
>搜索激活功能
```py
> https://arxiv.org/abs/1710.05941
本文探讨了各种可选的激活函数,并发现 swish 函数(本文中发现的)在网络中使用时会产生更好的结果。
Swish 在数学上被定义为
```f(x)=x·sigmoid(βx)```py
```sigmoid(y)=1/(1+e^(-y))```py
将这两者结合在一起有一个有趣的特性,即在零附近略微变负,而大多数传统的激活函数总是> =零。从概念上讲,这产生了更平滑的梯度空间,并且通过扩展使得网络更容易学习底层数据分布,这转化为提高的准确性。在其他强化学习问题场景中,Swish 已经被证明可以提高性能,所以它是一个重要的激活功能,你应该知道。
从实现的角度来看,swish 有一些限制,即它比简单的 ReLU 使用更多的内存。我们将在下一章回到这一点。
SE(挤压+激励)块
这是一篇来自 2017 年牛津视觉几何小组(例如,制作 VGG 的人)的有趣论文,该论文赢得了当年的 ImageNet 竞赛。
挤压和激励网络
> https://arxiv.org/abs/1709.01507
从概念上讲,我们可能会认为我们的神经网络实际上正在学习的是一组特征。然后,当网络看到符合特定特征集合的图片时,我们训练它激发特定的神经元。为了让事情更上一层楼,避免随机激活,理想情况下,对于每个特征图,我们可以定义一种主神经元,决定该特征是否应该作为一个整体激活。
这大致就是挤压和激励块的概念。通过获取特征输入并将其显著减少(在某些情况下减少到单个像素),我们允许网络对每个块进行某种训练,以教会自己在给定特定输入的情况下是否应该触发。这产生了最先进的结果,但也是计算昂贵的。
EfficientNet 使用了一个更简单的变体,它基于两个卷积的组合,以更低的计算成本产生类似的结果。
密码
请注意 squeeze 和 excite 模块,以及它们如何用于提高卷积模块的结果。通过这一添加,该主干的其余部分与 MobileNet v2 非常相似。还要看看 MBConvBlockStack 生成器参数的细微差别,我们将在下一章中看到更多。
import TensorFlow
struct InitialMBConvBlock: Layer {
@noDerivative var hiddenDimension: Int
var dConv: DepthwiseConv2D
var batchNormDConv: BatchNorm
var seAveragePool = GlobalAvgPool2D
var seReduceConv: Conv2D
var seExpandConv: Conv2D
var conv2: Conv2D
var batchNormConv2: BatchNorm
init(filters: (Int, Int), width: Float) {
let filterMult = filters
self.hiddenDimension = filterMult.0
dConv = DepthwiseConv2D
filterShape: (3, 3, filterMult.0, 1),
strides: (1, 1),
padding: .same)
seReduceConv = Conv2D
filterShape: (1, 1, filterMult.0, 8),
strides: (1, 1),
padding: .same)
seExpandConv = Conv2D
filterShape: (1, 1, 8, filterMult.0),
strides: (1, 1),
padding: .same)
conv2 = Conv2D
filterShape: (1, 1, filterMult.0, filterMult.1),
strides: (1, 1),
padding: .same)
batchNormDConv = BatchNorm(featureCount: filterMult.0)
batchNormConv2 = BatchNorm(featureCount: filterMult.1)
}
@differentiable
func forward(_ input: Tensor
let depthwise = swish(batchNormDConv(dConv(input)))
let seAvgPoolReshaped = seAveragePool(depthwise).reshaped(to: [
input.shape[0], 1, 1, self.hiddenDimension,
])
let squeezeExcite =
depthwise
* sigmoid(seExpandConv(swish(seReduceConv(seAvgPoolReshaped))))
return batchNormConv2(conv2(squeezeExcite))
}
}
struct MBConvBlock: Layer {
@noDerivative var addResLayer: Bool
@noDerivative var strides: (Int, Int)
@noDerivative let zeroPad = ZeroPadding2D
@noDerivative var hiddenDimension: Int
var conv1: Conv2D
var batchNormConv1: BatchNorm
var dConv: DepthwiseConv2D
var batchNormDConv: BatchNorm
var seAveragePool = GlobalAvgPool2D
var seReduceConv: Conv2D
var seExpandConv: Conv2D
var conv2: Conv2D
var batchNormConv2: BatchNorm
init(
filters: (Int, Int),
width: Float,
depthMultiplier: Int = 6,
strides: (Int, Int) = (1, 1),
kernel: (Int, Int) = (3, 3)
) {
self.strides = strides
self.addResLayer = filters.0 == filters.1 && strides == (1, 1)
let filterMult = filters
self.hiddenDimension = filterMult.0 * depthMultiplier
let reducedDimension = max(1, Int(filterMult.0 / 4))
conv1 = Conv2D<Float>(
filterShape: (1, 1, filterMult.0, hiddenDimension),
strides: (1, 1),
padding: .same)
dConv = DepthwiseConv2D<Float>(
filterShape: (kernel.0, kernel.1, hiddenDimension, 1),
strides: strides,
padding: strides == (1, 1) ? .same : .valid)
seReduceConv = Conv2D<Float>(
filterShape: (1, 1, hiddenDimension, reducedDimension),
strides: (1, 1),
padding: .same)
seExpandConv = Conv2D<Float>(
filterShape: (1, 1, reducedDimension, hiddenDimension),
strides: (1, 1),
padding: .same)
conv2 = Conv2D<Float>(
filterShape: (1, 1, hiddenDimension, filterMult.1),
strides: (1, 1),
padding: .same)
batchNormConv1 = BatchNorm(featureCount: hiddenDimension)
batchNormDConv = BatchNorm(featureCount: hiddenDimension)
batchNormConv2 = BatchNorm(featureCount: filterMult.1)
}
@differentiable
func forward(_ input: Tensor
let piecewise = swish(batchNormConv1(conv1(input)))
var depthwise: Tensor
if self.strides == (1, 1) {
depthwise = swish(batchNormDConv(dConv(piecewise)))
} else {
depthwise = swish(batchNormDConv(dConv(zeroPad(piecewise))))
}
let seAvgPoolReshaped = seAveragePool(depthwise).reshaped(to: [
input.shape[0], 1, 1, self.hiddenDimension,
])
let squeezeExcite =
depthwise
* sigmoid(seExpandConv(swish(seReduceConv(seAvgPoolReshaped))))
let piecewiseLinear = batchNormConv2(conv2(squeezeExcite))
if self.addResLayer {
return input + piecewiseLinear
} else {
return piecewiseLinear
}
}
}
struct MBConvBlockStack: Layer {
var blocks: [MBConvBlock] = []
init(
filters: (Int, Int),
width: Float,
initialStrides: (Int, Int) = (2, 2),
kernel: (Int, Int) = (3, 3),
blockCount: Int,
depth: Float
) {
let blockMult = blockCount
self.blocks = [
MBConvBlock(
filters: (filters.0, filters.1), width: width,
strides: initialStrides, kernel: kernel)
]
for _ in 1..<blockMult {
self.blocks.append(
MBConvBlock(
filters: (filters.1, filters.1),
width: width, kernel: kernel))
}
}
@differentiable
func forward(_ input: Tensor
return blocks.differentiableReduce(input) { 0) }
}
}
public struct EfficientNet: Layer {
@noDerivative let zeroPad = ZeroPadding2D
var inputConv: Conv2D
var inputConvBatchNorm: BatchNorm
var initialMBConv: InitialMBConvBlock
var residualBlockStack1: MBConvBlockStack
var residualBlockStack2: MBConvBlockStack
var residualBlockStack3: MBConvBlockStack
var residualBlockStack4: MBConvBlockStack
var residualBlockStack5: MBConvBlockStack
var residualBlockStack6: MBConvBlockStack
var outputConv: Conv2D
var outputConvBatchNorm: BatchNorm
var avgPool = GlobalAvgPool2D
var dropoutProb: Dropout
var outputClassifier: Dense
public init(
classCount: Int = 1000,
width: Float = 1.0,
depth: Float = 1.0,
resolution: Int = 224,
dropout: Double = 0.2
) {
inputConv = Conv2D
filterShape: (3, 3, 3, 32),
strides: (2, 2),
padding: .valid)
inputConvBatchNorm = BatchNorm(featureCount: 32)
initialMBConv = InitialMBConvBlock(filters: (32, 16), width: width)
residualBlockStack1 = MBConvBlockStack(
filters: (16, 24), width: width,
blockCount: 2, depth: depth)
residualBlockStack2 = MBConvBlockStack(
filters: (24, 40), width: width,
kernel: (5, 5), blockCount: 2, depth: depth)
residualBlockStack3 = MBConvBlockStack(
filters: (40, 80), width: width,
blockCount: 3, depth: depth)
residualBlockStack4 = MBConvBlockStack(
filters: (80, 112), width: width,
initialStrides: (1, 1), kernel: (5, 5), blockCount: 3, depth: depth)
residualBlockStack5 = MBConvBlockStack(
filters: (112, 192), width: width,
kernel: (5, 5), blockCount: 4, depth: depth)
residualBlockStack6 = MBConvBlockStack(
filters: (192, 320), width: width,
initialStrides: (1, 1), blockCount: 1, depth: depth)
outputConv = Conv2D<Float>(
filterShape: (
1, 1,
320, 1280
),
strides: (1, 1),
padding: .same)
outputConvBatchNorm = BatchNorm(featureCount: 1280)
dropoutProb = Dropout<Float>(probability: dropout)
outputClassifier = Dense(inputSize: 1280, outputSize: classCount)
}
@differentiable
public func forward(_ input: Tensor
let convolved = swish(input.sequenced(through: zeroPad, inputConv, inputConvBatchNorm))
let initialBlock = initialMBConv(convolved)
let backbone = initialBlock.sequenced(
through: residualBlockStack1, residualBlockStack2,
residualBlockStack3, residualBlockStack4, residualBlockStack5, residualBlockStack6)
let output = swish(backbone.sequenced(through: outputConv, outputConvBatchNorm))
return output.sequenced(through: avgPool, dropoutProb, outputClassifier)
}
}
结果
这个网络训练得非常好,在没有添加任何数据增强技术的情况下,比我们迄今为止看到的任何网络都具有更高的准确性。
Starting training...
[Epoch 1 ] Accuracy: 50/500 (0.1) Loss: 3.919964
[Epoch 2 ] Accuracy: 315/500 (0.63) Loss: 1.1730766
[Epoch 3 ] Accuracy: 340/500 (0.68) Loss: 1.042603
[Epoch 4 ] Accuracy: 382/500 (0.764) Loss: 0.7738381
[Epoch 5 ] Accuracy: 358/500 (0.716) Loss: 0.8867168
[Epoch 6 ] Accuracy: 397/500 (0.794) Loss: 0.7941174
[Epoch 7 ] Accuracy: 384/500 (0.768) Loss: 0.7910826
[Epoch 8 ] Accuracy: 375/500 (0.75) Loss: 0.9265955
[Epoch 9 ] Accuracy: 395/500 (0.79) Loss: 0.7806258
[Epoch 10] Accuracy: 389/500 (0.778) Loss: 0.8921993
[Epoch 11] Accuracy: 393/500 (0.786) Loss: 0.913636
[Epoch 12] Accuracy: 395/500 (0.79) Loss: 0.8772738
[Epoch 13] Accuracy: 396/500 (0.792) Loss: 0.819137
[Epoch 14] Accuracy: 393/500 (0.786) Loss: 0.7435807
[Epoch 15] Accuracy: 418/500 (0.836) Loss: 0.6915679
[Epoch 16] Accuracy: 404/500 (0.808) Loss: 0.79288286
[Epoch 17] Accuracy: 405/500 (0.81) Loss: 0.8690043
[Epoch 18] Accuracy: 404/500 (0.808) Loss: 0.89440507
[Epoch 19] Accuracy: 409/500 (0.818) Loss: 0.85941887
[Epoch 20] Accuracy: 408/500 (0.816) Loss: 0.8633226
[Epoch 21] Accuracy: 404/500 (0.808) Loss: 0.7646436
[Epoch 22] Accuracy: 411/500 (0.822) Loss: 0.8865621
[Epoch 23] Accuracy: 424/500 (0.848) Loss: 0.6812671
[Epoch 24] Accuracy: 402/500 (0.804) Loss: 0.8662841
[Epoch 25] Accuracy: 425/500 (0.85) Loss: 0.7081538
[Epoch 26] Accuracy: 423/500 (0.846) Loss: 0.7106852
[Epoch 27] Accuracy: 411/500 (0.822) Loss: 0.88567644
[Epoch 28] Accuracy: 410/500 (0.82) Loss: 0.8509838
[Epoch 29] Accuracy: 409/500 (0.818) Loss: 0.85791296
[Epoch 30] Accuracy: 416/500 (0.832) Loss: 0.76689
高效网络变体
一旦我们有了这个基础,我们就可以使用我们改进的图像识别网络来解决不同领域的其他相关问题。
EfficientNet[B1-8]
回顾我们在上一章对网络架构搜索功能的探索,这类方法的问题在于,试图扩大搜索功能很困难,因为没有一个清晰的系统来扩大搜索功能。
作者在本文中介绍的是他们的基本(B0)网络的一组缩放试探法,使平滑缩放能够产生越来越大的网络。不严格地说,我们可以说大型网络的每一步都需要大量的计算。然后,只要有大量的计算时间运行,我们就可以持续地构建大型网络。因此,这是高效网络的变体,可以通过简单地将我们之前的网络与我们在本书中迄今为止看到的各种网络进行比较来产生。
随机扩增
RandAugment:实用的自动数据扩充,减少了搜索空间
> https://arxiv.org/abs/1909.13719
我们在前一章中简要讨论了数据扩充,我提到这是一个活跃的研究领域。本文结合了各种增强技术(例如,翻转、旋转、缩放等。)与强化学习算法一起使用,以便在应用于数据集时找到数据扩充过滤器的最佳(用最小的集合对准确度的最大影响)组合。然后,他们对 ImageNet 数据集运行这种学习到的算法,然后在其上训练 EfficientNet 变体,以产生显著的(~ 4–5%!)仅使用计算时间的改进的网络集。
吵闹的学生
嘈杂学生的自我培训提高了 ImageNet 分类
> https://arxiv.org/abs/1911.04252
接下来,**网络提炼* *是构建小型网络的一个有趣的研究领域。大致来说,我们将一个大型网络作为教师,然后训练一个较小的学生网络,在给定相同输入和来自教师的每个答案的反馈的情况下,对较大的网络给出类似的响应。例如,一旦一种更大的方法在 GPU 集群上证明了自己,这在为资源有限的设备构建网络方面具有有趣的应用。这是自然语言处理中感兴趣的大领域,其中大型网络(例如 BERT)已经实现了最先进的性能,但是太大而不能用于日常问题解决。
网络蒸馏已经被用来使网络变小,但是它能被用来使网络变大吗?粗略地说,这篇论文采用了数据增强技术,并使用它们来使学生的输入更加嘈杂,但仍然要求学生网络给出与老师的答案相匹配的答案。通过在老师身上迭代训练一个更大的学生,然后用训练过的学生代替老师,他们能够建立一个更大的网络,能够产生甚至比脸书 2019 年 10 亿张图片的 Instagram 语料库更准确的 ImageNet 结果(见 https://arxiv.org/abs/1905.00546
)。
效率检测
EfficientDet:可扩展且高效的对象检测
> https://arxiv.org/abs/1911.09070
我们在本书中没有谈到目标检测网络,但许多方法的基本思想是使用已知良好的现有图像识别网络(称为主干),然后我们可以在最后添加目标检测输出层(称为* 头部 *)。这种方法实现了一种很好的混合和匹配风格的技术,在这种技术中,我们可以使用具有多个不同主干或数据增强策略的同一个头来找到特定问题的最佳解决方案。
因此,我们采用 EfficientNet,添加一个定制的对象检测头,应用我们的缩放技术,瞧,我们有一个对象检测(以及一些其他调整,语义分段)网络,具有最先进的性能。
概述
我们看了 EfficientNet,这是图像识别的最新技术。我们已经了解了如何使用 EfficientNet 基础在相关领域构建最先进的方法。接下来,让我们看看如何将这些想法带回移动设备领域。
十一、MobileNetV3
在本章中,我们将了解 MobileNetV3,它通过降低网络的复杂性,在移动硬件上提供了 EfficientNet 的优化版本。该模型主要基于 EfficientNet 的搜索策略,具有特定于移动设备的参数空间目标。
这是移动模型的当前艺术状态,但在这一点上,我们深深陷入了争论什么硬件在运行的领域,使得 1:1 模型比较变得困难。制造商越来越多地推出定制硬件,每种设备的运行方式都会略有不同。不过,另一方面,可以给 EfficientNet 搜索算法一个任意的起点(例如,知道它将在什么硬件上运行),然后为该设备生成一个优化的网络。我相信这越来越是未来的发展方向:随着越来越多新的人工智能硬件变得可用,网络将被定制为在特定设备上运行。
首先,让我们看看 swish 和 sigmoid 激活函数的一些特定于移动设备的变体,我们可以用它们来加速对我们的网络的评估。
硬嗖嗖和硬乙状结肠
在上一章中,我们讨论了如何使用 swish 和 sigmoid 作为激活函数,使网络能够学习到更准确的结果。不过,在运行时,这些函数在内存方面比我们的 ReLU 激活函数要昂贵得多。MobileNet 作者介绍了我们的 sigmoid 函数的 relu6 变体:
hardSigmoid(x) = relu6(x + 3)/6
hardSwish(x) = x * hardSigmoid(x)
以便减少运行网络所需的内存量并简化运行时间。
然而,他们发现他们无法在不牺牲性能的情况下简单地将此应用于所有节点。我们一会儿会回到这个话题。
移除一半网络的挤压和激励(SE)块逻辑
同样,EfficientNet 的 SEBlock 逻辑也很强大,但是在移动设备上这是一个昂贵的操作。然而,他们发现他们可以在不牺牲性能的情况下为某些层移除这一点。我们一会儿会再回到这个话题。
自定义标题
作者为他们的输出层实现了一个定制的 head 逻辑,我认为这很有趣。本质上,他们使用一对卷积来代替 EfficientNet 中使用的密集输出神经网络层。从技术角度来看,这种方法不如密集方法精确,但在移动设备上实现起来更简单、更快。
超参数
最后,作者结合前面的文章,大量使用了高效网络搜索策略。从概念上讲,他们给搜索算法提供了前面提到的构建模块,并运行一组 TPU,让强化学习发挥它的魔力。由此,他们产生了两个不同的网络,MobileNetV3-大型和 MobileNetV3-小型,由于前面的限制,这两个网络略有不同。例如,虽然两种变体都在网络的后面部分使用 SEBlock,但小型变体在其第二层使用 se block,而大型变体则不使用。每层的滤波器数量完全是为了优化性能而学习的。两个网络在最初几层都使用 ReLU,但在中途就切换到 hardSwish。
表演
综上所述,该网络在 ImageNet 上具有更高的精度,但在有硬件支持的移动设备上,评估时间不到 10ms。然后,作者还使用不同的起始要求(例如,仅允许 3×3 卷积)运行他们的搜索策略,以产生最小的变体,该变体应合理地适应未来,取决于市场上出现的任何新硬件。
密码
让我们来构建 MobileNetV3。这将结合硬件感知网络架构搜索(NAS)和 NetAdapt 算法,以利用这两种方法的优势。这个网络比我们到目前为止看到的网络要复杂得多,但是如果你仔细观察,我想你可以看到它只是我们到目前为止看到的所有技术的组合。需要注意的关键部分是末尾的大量 MBConvBlockStack 参数集合,这些参数生成了细微不同的块,这些块组合在一起可以产生一个既准确又能在移动设备上良好运行的网络。
``
import TensorFlow
public enum ActivationType {
case hardSwish
case relu
}
public struct SqueezeExcitationBlock: Layer {
// https://arxiv.org/abs/1709.01507
public var averagePool = GlobalAvgPool2D<Float>()
public var reduceConv: Conv2D<Float>
public var expandConv: Conv2D<Float>
@noDerivative public var inputOutputSize: Int
public init(inputOutputSize: Int, reducedSize: Int) {
self.inputOutputSize = inputOutputSize
reduceConv = Conv2D<Float>(
filterShape: (1, 1, inputOutputSize, reducedSize),
strides: (1, 1),
padding: .same)
expandConv = Conv2D<Float>(
filterShape: (1, 1, reducedSize, inputOutputSize),
strides: (1, 1),
padding: .same)
}
@differentiable
public func forward(_ input: Tensor<Float>) -> Tensor<Float> {
let avgPoolReshaped = averagePool(input).reshaped(to: [
input.shape[0], 1, 1, self.inputOutputSize,
])
return input
* hardSigmoid(expandConv(relu(reduceConv(avgPoolReshaped))))
}
}
public struct InitialInvertedResidualBlock: Layer {
@noDerivative public var addResLayer: Bool
@noDerivative public var useSELayer: Bool = false
@noDerivative public var activation: ActivationType = .relu
public var dConv: DepthwiseConv2D<Float>
public var batchNormDConv: BatchNorm<Float>
public var seBlock: SqueezeExcitationBlock
public var conv2: Conv2D<Float>
public var batchNormConv2: BatchNorm<Float>
public init(
filters: (Int, Int),
strides: (Int, Int) = (1, 1),
kernel: (Int, Int) = (3, 3),
seLayer: Bool = false,
activation: ActivationType = .relu
) {
self.useSELayer = seLayer
self.activation = activation
self.addResLayer = filters.0 == filters.1 && strides == (1, 1)
let filterMult = filters
let hiddenDimension = filterMult.0 * 1
let reducedDimension = hiddenDimension / 4
dConv = DepthwiseConv2D<Float>(
filterShape: (3, 3, filterMult.0, 1),
strides: (1, 1),
padding: .same)
seBlock = SqueezeExcitationBlock(
inputOutputSize: hiddenDimension, reducedSize: reducedDimension)
conv2 = Conv2D<Float>(
filterShape: (1, 1, hiddenDimension, filterMult.1),
strides: (1, 1),
padding: .same)
batchNormDConv = BatchNorm(featureCount: filterMult.0)
batchNormConv2 = BatchNorm(featureCount: filterMult.1)
}
@differentiable
public func forward(_ input: Tensor<Float>) -> Tensor<Float> {
var depthwise = batchNormDConv(dConv(input))
switch self.activation {
case .hardSwish: depthwise = hardSwish(depthwise)
case .relu: depthwise = relu(depthwise)
}
var squeezeExcite: Tensor<Float>
if self.useSELayer {
squeezeExcite = seBlock(depthwise)
} else {
squeezeExcite = depthwise
}
let piecewiseLinear = batchNormConv2(conv2(squeezeExcite))
if self.addResLayer {
return input + piecewiseLinear
} else {
return piecewiseLinear
}
}
}
public struct InvertedResidualBlock: Layer {
@noDerivative public var strides: (Int, Int)
@noDerivative public let zeroPad = ZeroPadding2D<Float>(padding: ((0, 1), (0, 1)))
@noDerivative public var addResLayer: Bool
@noDerivative public var activation: ActivationType = .relu
@noDerivative public var useSELayer: Bool
public var conv1: Conv2D<Float>
public var batchNormConv1: BatchNorm<Float>
public var dConv: DepthwiseConv2D<Float>
public var batchNormDConv: BatchNorm<Float>
public var seBlock: SqueezeExcitationBlock
public var conv2: Conv2D<Float>
public var batchNormConv2: BatchNorm<Float>
public init(
filters: (Int, Int),
expansionFactor: Float,
strides: (Int, Int) = (1, 1),
kernel: (Int, Int) = (3, 3),
seLayer: Bool = false,
activation: ActivationType = .relu
) {
self.strides = strides
self.addResLayer = filters.0 == filters.1 && strides == (1, 1)
self.useSELayer = seLayer
self.activation = activation
let filterMult = filters
let hiddenDimension = Int(Float(filterMult.0) * expansionFactor)
let reducedDimension = hiddenDimension / 4
conv1 = Conv2D<Float>(
filterShape: (1, 1, filterMult.0, hiddenDimension),
strides: (1, 1),
padding: .same)
dConv = DepthwiseConv2D<Float>(
filterShape: (kernel.0, kernel.1, hiddenDimension, 1),
strides: strides,
padding: strides == (1, 1) ? .same : .valid)
seBlock = SqueezeExcitationBlock(
inputOutputSize: hiddenDimension, reducedSize: reducedDimension)
conv2 = Conv2D<Float>(
filterShape: (1, 1, hiddenDimension, filterMult.1),
strides: (1, 1),
padding: .same)
batchNormConv1 = BatchNorm(featureCount: hiddenDimension)
batchNormDConv = BatchNorm(featureCount: hiddenDimension)
batchNormConv2 = BatchNorm(featureCount: filterMult.1)
}
@differentiable
public func forward(_ input: Tensor<Float>) -> Tensor<Float> {
var piecewise = batchNormConv1(conv1(input))
switch self.activation {
case .hardSwish: piecewise = hardSwish(piecewise)
case .relu: piecewise = relu(piecewise)
}
var depthwise: Tensor<Float>
if self.strides == (1, 1) {
depthwise = batchNormDConv(dConv(piecewise))
} else {
depthwise = batchNormDConv(dConv(zeroPad(piecewise)))
}
switch self.activation {
case .hardSwish: depthwise = hardSwish(depthwise)
case .relu: depthwise = relu(depthwise)
}
var squeezeExcite: Tensor<Float>
if self.useSELayer {
squeezeExcite = seBlock(depthwise)
} else {
squeezeExcite = depthwise
}
let piecewiseLinear = batchNormConv2(conv2(squeezeExcite))
if self.addResLayer {
return input + piecewiseLinear
} else {
return piecewiseLinear
}
}
}
public struct MobileNetV3Large: Layer {
@noDerivative public let zeroPad = ZeroPadding2D<Float>(padding: ((0, 1), (0, 1)))
public var inputConv: Conv2D<Float>
public var inputConvBatchNorm: BatchNorm<Float>
public var invertedResidualBlock1: InitialInvertedResidualBlock
public var invertedResidualBlock2: InvertedResidualBlock
public var invertedResidualBlock3: InvertedResidualBlock
public var invertedResidualBlock4: InvertedResidualBlock
public var invertedResidualBlock5: InvertedResidualBlock
public var invertedResidualBlock6: InvertedResidualBlock
public var invertedResidualBlock7: InvertedResidualBlock
public var invertedResidualBlock8: InvertedResidualBlock
public var invertedResidualBlock9: InvertedResidualBlock
public var invertedResidualBlock10: InvertedResidualBlock
public var invertedResidualBlock11: InvertedResidualBlock
public var invertedResidualBlock12: InvertedResidualBlock
public var invertedResidualBlock13: InvertedResidualBlock
public var invertedResidualBlock14: InvertedResidualBlock
public var invertedResidualBlock15: InvertedResidualBlock
public var outputConv: Conv2D<Float>
public var outputConvBatchNorm: BatchNorm<Float>
public var avgPool = GlobalAvgPool2D<Float>()
public var finalConv: Conv2D<Float>
public var dropoutLayer: Dropout<Float>
public var classiferConv: Conv2D<Float>
public var flatten = Flatten<Float>()
@noDerivative public var lastConvChannel: Int
public init(classCount: Int = 1000, dropout: Double = 0.2) {
inputConv = Conv2D<Float>(
filterShape: (3, 3, 3, 16),
strides: (2, 2),
padding: .same)
inputConvBatchNorm = BatchNorm(
featureCount: 16)
invertedResidualBlock1 = InitialInvertedResidualBlock(
filters: (16, 16))
invertedResidualBlock2 = InvertedResidualBlock(
filters: (16, 24),
expansionFactor: 4, strides: (2, 2))
invertedResidualBlock3 = InvertedResidualBlock(
filters: (24, 24),
expansionFactor: 3)
invertedResidualBlock4 = InvertedResidualBlock(
filters: (24, 40),
expansionFactor: 3, strides: (2, 2), kernel: (5, 5), seLayer: true)
invertedResidualBlock5 = InvertedResidualBlock(
filters: (40, 40),
expansionFactor: 3, kernel: (5, 5), seLayer: true)
invertedResidualBlock6 = InvertedResidualBlock(
filters: (40, 40),
expansionFactor: 3, kernel: (5, 5), seLayer: true)
invertedResidualBlock7 = InvertedResidualBlock(
filters: (40, 80),
expansionFactor: 6, strides: (2, 2), activation: .hardSwish)
invertedResidualBlock8 = InvertedResidualBlock(
filters: (80, 80),
expansionFactor: 2.5, activation: .hardSwish)
invertedResidualBlock9 = InvertedResidualBlock(
filters: (80, 80),
expansionFactor: 184 / 80.0, activation: .hardSwish)
invertedResidualBlock10 = InvertedResidualBlock(
filters: (80, 80),
expansionFactor: 184 / 80.0, activation: .hardSwish)
invertedResidualBlock11 = InvertedResidualBlock(
filters: (80, 112),
expansionFactor: 6, seLayer: true, activation: .hardSwish)
invertedResidualBlock12 = InvertedResidualBlock(
filters: (112, 112),
expansionFactor: 6, seLayer: true, activation: .hardSwish)
invertedResidualBlock13 = InvertedResidualBlock(
filters: (112, 160),
expansionFactor: 6, strides: (2, 2), kernel: (5, 5), seLayer: true,
activation: .hardSwish)
invertedResidualBlock14 = InvertedResidualBlock(
filters: (160, 160),
expansionFactor: 6, kernel: (5, 5), seLayer: true, activation: .hardSwish)
invertedResidualBlock15 = InvertedResidualBlock(
filters: (160, 160),
expansionFactor: 6, kernel: (5, 5), seLayer: true, activation: .hardSwish)
lastConvChannel = 960
outputConv = Conv2D<Float>(
filterShape: (
1, 1, 160, lastConvChannel
),
strides: (1, 1),
padding: .same)
outputConvBatchNorm = BatchNorm(featureCount: lastConvChannel)
let lastPointChannel = 1280
finalConv = Conv2D<Float>(
filterShape: (1, 1, lastConvChannel, lastPointChannel),
strides: (1, 1),
padding: .same)
dropoutLayer = Dropout<Float>(probability: dropout)
classiferConv = Conv2D<Float>(
filterShape: (1, 1, lastPointChannel, classCount),
strides: (1, 1),
padding: .same)
}
@differentiable
public func forward(_ input: Tensor<Float>) -> Tensor<Float> {
let initialConv = hardSwish(
input.sequenced(through: zeroPad, inputConv, inputConvBatchNorm))
let backbone1 = initialConv.sequenced(
through: invertedResidualBlock1,
invertedResidualBlock2, invertedResidualBlock3, invertedResidualBlock4, invertedResidualBlock5)
let backbone2 = backbone1.sequenced(
through: invertedResidualBlock6, invertedResidualBlock7,
invertedResidualBlock8, invertedResidualBlock9, invertedResidualBlock10)
let backbone3 = backbone2.sequenced(
through: invertedResidualBlock11,
invertedResidualBlock12, invertedResidualBlock13, invertedResidualBlock14, invertedResidualBlock15)
let outputConvResult = hardSwish(outputConvBatchNorm(outputConv(backbone3)))
let averagePool = avgPool(outputConvResult).reshaped(to: [
input.shape[0], 1, 1, self.lastConvChannel,
])
let finalConvResult = dropoutLayer(hardSwish(finalConv(averagePool)))
return flatten(classiferConv(finalConvResult))
}
}
public struct MobileNetV3Small: Layer {
@noDerivative public let zeroPad = ZeroPadding2D<Float>(padding: ((0, 1), (0, 1)))
public var inputConv: Conv2D<Float>
public var inputConvBatchNorm: BatchNorm<Float>
public var invertedResidualBlock1: InitialInvertedResidualBlock
public var invertedResidualBlock2: InvertedResidualBlock
public var invertedResidualBlock3: InvertedResidualBlock
public var invertedResidualBlock4: InvertedResidualBlock
public var invertedResidualBlock5: InvertedResidualBlock
public var invertedResidualBlock6: InvertedResidualBlock
public var invertedResidualBlock7: InvertedResidualBlock
public var invertedResidualBlock8: InvertedResidualBlock
public var invertedResidualBlock9: InvertedResidualBlock
public var invertedResidualBlock10: InvertedResidualBlock
public var invertedResidualBlock11: InvertedResidualBlock
public var outputConv: Conv2D<Float>
public var outputConvBatchNorm: BatchNorm<Float>
public var avgPool = GlobalAvgPool2D<Float>()
public var finalConv: Conv2D<Float>
public var dropoutLayer: Dropout<Float>
public var classiferConv: Conv2D<Float>
public var flatten = Flatten<Float>()
@noDerivative public var lastConvChannel: Int
public init(classCount: Int = 1000, dropout: Double = 0.2) {
inputConv = Conv2D<Float>(
filterShape: (3, 3, 3, 16),
strides: (2, 2),
padding: .same)
inputConvBatchNorm = BatchNorm(
featureCount: 16)
invertedResidualBlock1 = InitialInvertedResidualBlock(
filters: (16, 16),
strides: (2, 2), seLayer: true)
invertedResidualBlock2 = InvertedResidualBlock(
filters: (16, 24),
expansionFactor: 72.0 / 16.0, strides: (2, 2))
invertedResidualBlock3 = InvertedResidualBlock(
filters: (24, 24),
expansionFactor: 88.0 / 24.0)
invertedResidualBlock4 = InvertedResidualBlock(
filters: (24, 40),
expansionFactor: 4, strides: (2, 2), kernel: (5, 5), seLayer: true,
activation: .hardSwish)
invertedResidualBlock5 = InvertedResidualBlock(
filters: (40, 40),
expansionFactor: 6, kernel: (5, 5), seLayer: true, activation: .hardSwish)
invertedResidualBlock6 = InvertedResidualBlock(
filters: (40, 40),
expansionFactor: 6, kernel: (5, 5), seLayer: true, activation: .hardSwish)
invertedResidualBlock7 = InvertedResidualBlock(
filters: (40, 48),
expansionFactor: 3, kernel: (5, 5), seLayer: true, activation: .hardSwish)
invertedResidualBlock8 = InvertedResidualBlock(
filters: (48, 48),
expansionFactor: 3, kernel: (5, 5), seLayer: true, activation: .hardSwish)
invertedResidualBlock9 = InvertedResidualBlock(
filters: (48, 96),
expansionFactor: 6, strides: (2, 2), kernel: (5, 5), seLayer: true,
activation: .hardSwish)
invertedResidualBlock10 = InvertedResidualBlock(
filters: (96, 96),
expansionFactor: 6, kernel: (5, 5), seLayer: true, activation: .hardSwish)
invertedResidualBlock11 = InvertedResidualBlock(
filters: (96, 96),
expansionFactor: 6, kernel: (5, 5), seLayer: true, activation: .hardSwish)
lastConvChannel = 576
outputConv = Conv2D<Float>(
filterShape: (
1, 1, 96, lastConvChannel
),
strides: (1, 1),
padding: .same)
outputConvBatchNorm = BatchNorm(featureCount: lastConvChannel)
let lastPointChannel = 1280
finalConv = Conv2D<Float>(
filterShape: (1, 1, lastConvChannel, lastPointChannel),
strides: (1, 1),
padding: .same)
dropoutLayer = Dropout<Float>(probability: dropout)
classiferConv = Conv2D<Float>(
filterShape: (1, 1, lastPointChannel, classCount),
strides: (1, 1),
padding: .same)
}
@differentiable
public func forward(_ input: Tensor<Float>) -> Tensor<Float> {
let initialConv = hardSwish(
input.sequenced(through: zeroPad, inputConv, inputConvBatchNorm))
let backbone1 = initialConv.sequenced(
through: invertedResidualBlock1,
invertedResidualBlock2, invertedResidualBlock3, invertedResidualBlock4, invertedResidualBlock5)
let backbone2 = backbone1.sequenced(
through: invertedResidualBlock6, invertedResidualBlock7,
invertedResidualBlock8, invertedResidualBlock9, invertedResidualBlock10, invertedResidualBlock11)
let outputConvResult = hardSwish(outputConvBatchNorm(outputConv(backbone2)))
let averagePool = avgPool(outputConvResult).reshaped(to: [
input.shape[0], 1, 1, lastConvChannel,
])
let finalConvResult = dropoutLayer(hardSwish(finalConv(averagePool)))
return flatten(classiferConv(finalConvResult))
}
}
结果
该网络将被训练为比 EfficientNet 稍差,但是可以在移动设备上快速评估。此外,生成的网络很小,因此可以很容易地通过网络发送到边缘设备。
Starting training...
[Epoch 1] Accuracy: 50/500 (0.1) Loss: 3.3504734
[Epoch 2] Accuracy: 253/500 (0.506) Loss: 1.4156498
[Epoch 3] Accuracy: 335/500 (0.67) Loss: 1.0543922
[Epoch 4] Accuracy: 326/500 (0.652) Loss: 1.1357045
[Epoch 5] Accuracy: 353/500 (0.706) Loss: 0.9812555
[Epoch 6] Accuracy: 350/500 (0.7) Loss: 0.9210515
[Epoch 7] Accuracy: 380/500 (0.76) Loss: 0.7407557
[Epoch 8] Accuracy: 347/500 (0.694) Loss: 1.038017
[Epoch 9] Accuracy: 343/500 (0.686) Loss: 1.0409927
[Epoch 10] Accuracy: 377/500 (0.754) Loss: 0.8882818
[Epoch 11] Accuracy: 381/500 (0.762) Loss: 0.9374979
[Epoch 12] Accuracy: 383/500 (0.766) Loss: 0.8867029
[Epoch 13] Accuracy: 365/500 (0.73) Loss: 1.3112245
[Epoch 14] Accuracy: 377/500 (0.754) Loss: 0.9881239
[Epoch 15] Accuracy: 386/500 (0.772) Loss: 0.99048007
[Epoch 16] Accuracy: 406/500 (0.812) Loss: 0.78758305
[Epoch 17] Accuracy: 402/500 (0.804) Loss: 0.8263649
[Epoch 18] Accuracy: 407/500 (0.814) Loss: 0.8147187
[Epoch 19] Accuracy: 401/500 (0.802) Loss: 0.8540674
[Epoch 20] Accuracy: 387/500 (0.774) Loss: 0.90144944
[Epoch 21] Accuracy: 404/500 (0.808) Loss: 1.0089223
[Epoch 22] Accuracy: 396/500 (0.792) Loss: 0.97762024
[Epoch 23] Accuracy: 399/500 (0.798) Loss: 0.9001269
[Epoch 24] Accuracy: 389/500 (0.778) Loss: 1.1596041
[Epoch 25] Accuracy: 384/500 (0.768) Loss: 1.235701
[Epoch 26] Accuracy: 396/500 (0.792) Loss: 1.0384445
[Epoch 27] Accuracy: 405/500 (0.81) Loss: 0.9806802
[Epoch 28] Accuracy: 405/500 (0.81) Loss: 0.9442753
[Epoch 29] Accuracy: 411/500 (0.822) Loss: 0.85053337
[Epoch 30] Accuracy: 422/500 (0.844) Loss: 0.8129424
EfficientNet-边缘
同样,我们可以使用 EfficientNet 搜索策略为移动设备构建网络;我们可以用它来为更小的设备构建网络。谷歌已经生产了一系列小型 ASIC 设备(Coral 是品牌名称),称为 EdgeTPU,可以插入你的计算机,让我们在自己的硬件上运行 tensorflow lite 模型。从概念上讲,这些设备的内存空间和计算能力极其有限,但它们是 AI 硬件,就像我们的显卡一样。通过为 EfficientNet 搜索算法提供设备限制,他们能够发现一组最佳网络,在计算能力极其有限的设备上运行。
概述
在过去的几章中,我们已经从小网络发展到大网络,现在我们又回到小网络。这些研究领域都越来越紧密,相互关联。现在让我们来看看如何将它应用到你自己的工作中。
十二、锦囊妙计
在这一章中,我们将研究如何通过结合多种不同的方法来修改我们最初的 ResNet 50 网络,以达到与 EfficientNet 几乎一样准确的结果。
你已经走到这一步了。我们已经从使用神经网络来执行图像识别的最基础发展到了该领域的最新水平。现在请允许我对我的方法提出一些限制条件。首先,我已经在这个领域开辟了一条非常直接的道路,目标是让新人的早期阶段尽可能简单。在这个过程中,我跳过了许多历史、重要的里程碑和大量的研究。有许多我没有提到的不同的论文和方法包含了有趣的想法,你应该看看。简而言之,进步永远不会像我在这里试图展示的那样是线性的。通常有许多随机的方法,错误的开始导致死胡同,并且尝试了许多不同的东西,其中只有一小部分真正起作用。进步通常是丑陋而乏味的。
锦囊妙计
让我们来看一个有时被称为锦囊妙计式方法的例子。一般来说,有人会想出一个新奇的想法,然后发表在报纸上。我们已经看到了十几个这样的例子。然后,各种各样的其他研究人员和团体将试图把它与尽可能多的其他不同方法结合在一起,试图找到一种产生新结果的神奇组合。在高层次上,这可能是 NASNet 的学术版本。经常发生的是,人们发现有其他方法可以得到相同的结果,可以说,最初的研究人员最终陷入了局部极值。
在卷积神经网络中混合组合技术的性能改进
> https://arxiv.org/abs/2001.06268
让我们来看看最近的一篇论文,“在卷积神经网络中复合汇编技术的性能改进”,作为这方面的一个例子。Lee 等人在前几章中采用了我们相同的 ResNet 50 方法,并发现如何修改它,以更低的成本产生几乎与 EfficientNet 一样好的结果。
他们对我们之前看到的基本网络进行了如下调整:
-
用 3×3 步幅 2+3×3+3×3 卷积方法替换了 ResNet 50 的 7×7 磁头
-
从 ResNet 50 模块中的初始 1x1 卷积中移除 2x2 步幅,并将其添加到 3x3 卷积中
-
添加了 Averagepool2d 步骤作为跳过连接卷积层的一部分
-
增加了一个频道关注(CA)操作员
-
选择性内核(SK)块
-
大小网块跳过连接
为了做到这一点,他们使用了以下图像识别纸:
使用卷积神经网络进行图像分类的技巧包
> https://arxiv.org/abs/1812.01187
选择性核心网络
> https://arxiv.org/abs/1903.06586
大-小网络:用于视觉和语音识别的有效多尺度特征表示
> https://arxiv.org/abs/1807.03848
再次使卷积网络保持平移不变
> https://arxiv.org/abs/1904.11486
此外,他们使用以下数据扩充/训练/标准化技术:
通过惩罚置信输出分布来调整神经网络
> https://arxiv.org/abs/1701.06548
自动增强:从数据中学习增强策略
> https://arxiv.org/abs/1805.09501
混淆:超越经验风险最小化
> https://arxiv.org/abs/1710.09412
提取神经网络中的知识
> https://arxiv.org/abs/1503.02531
DropBlock:卷积网络的正则化方法
> https://arxiv.org/abs/1810.12890
从中可以学到什么
对我来说,这就是为什么我不会对研究小组在解决问题上投入越来越多的计算能力感到不安。即使他们的方法可以总结为蛮力,在证明更大规模的方法有效时,他们为个体研究人员打开了大门,使他们能够用更简单的硬件复制他们的结果。
我的经验是事情通常是这样的:
-
许多研究人员试图找到小的新方法,因为他们没有大规模的机器。
-
有人发现了能产生持续改进的东西(例如,人们可以复制他们的结果)。
-
大型研究团队会投入大量计算资源来解决这个问题。几个月后,他们发表了尝试测量的结果。
缩放通常如下所示:
-
原研究员:适马 0.5 改进。
-
10 倍集群:适马 0.85 改进。
-
100 倍集群:适马 0.95 改进。
-
世界上所有的计算能力:适马 0.985 改进。
-
六个月后:有人发现如何用有限的计算资源复制大型集群的工作,然后循环往复。
-
与此同时,许多不知名的小研究人员正在发表被完全忽视的新发现。
-
有人发表了一篇博客文章,然后像病毒一样传播开来,我们又回到了起点。
阅读报纸
要在这个领域取得成功,你需要的关键技能不是最前沿的网络理论或最快的计算机,这两者都很可能在一年内过时。取而代之的是一项永恒的技能,那就是独立阅读论文并跟上进度的能力。当你在论文中遇到你不理解的东西时,你需要能够查阅论文的参考文献,并找出他们的想法是从哪里来的。如果你追溯到足够远的地方,这些参考文献往往会集中在几个关键概念上。学会这些,无论你想做什么,你都会有一个坚实的基础。
保持在曲线后面
数量惊人的论文问世,轰动一时,然后销声匿迹。我发现试图跟上最新的发展很有可能会偏离正题。相反,我的建议是落后趋势几个月。让其他人阅读最新和最伟大的作品,然后等待他们实际证明事情是可行的。在 GitHub 上寻找展示新事物如何工作的代码演示,或者在 Jupyter 笔记本上寻找解释正在发生什么的代码演示。对我来说,这就是为什么你也应该选择其他框架(例如 pytorch ),因为这样你就可以更容易地从不断测试新想法的更广泛的机器学习研究人员社区中吸收知识。
找几个研究人员在 Twitter 上关注,然后看看他们在读什么,谈论什么。让他们为你过滤。也许从博弈论的角度来看,如果每个人都这样做,进步将会停滞不前,但除非你是这些领域的领先研究者,否则陷入死胡同的可能性很高。
举一个不同的例子,我们可以考虑学习、理解和运行这些不同测试数据集的模型所需的工作,如下所示:
-
一分钟
-
CIFAR: 1 小时
-
Imagenette: 1 天
-
ImageNet: 1 个月
这些数字是我瞎编的,不要想太多。那么,对我来说,你应该在小网络上比在大网络上多花一个数量级的钱。在你跳到 CIFAR 之前,你应该做十几次不同的 MNIST;在跳转到 Imagenette 之前,你要做十几次 CIFAR 诸如此类。对于运行一次 ImageNet 所需的计算,你可以在一台基本的计算机上以一千种不同的方式运行 MNIST,但是找到这样做的人是极其罕见的,尽管所需的资源应该是任何人都可以获得的。
对我来说,很难与拥有大型集群的高端研究团队竞争,这些集群拥有最新、最棒的硬件,能够进行大规模的大规模实验。但是我们能超越他们的地方很简单,那就是在一个特定的问题上比其他人做得更深入。大型研究小组的成功也是他们的弱点,因为他们不断寻找新的方法来产生可发表的结果。如果这对你来说不重要,那么你可以比他们花更多的时间在杂草上。通过扩展,你可以发现他们在匆忙获得结果时错过了什么。
我是如何阅读报纸的
通常,我会阅读摘要,并希望对论文内容有一个高层次的理解。我很高兴地承认,我经常阅读摘要和前几段,感觉我不知道到底发生了什么。有时候论文涉及的范围太广,以至于无法用几句话来概括(或者可能不是非常清楚),所以我认为作者和我都有责任。我通常直接跳到图表上,希望这些图表能让我直观地了解这篇论文到底想做什么。如果失败了,我将宣读结论。如果所有这些都失败了,那么我会坐下来,试着略读这篇论文,试图通过这种方式获得高层次的理解。我的基本过程是试图获得高层次的理解,然后进行连续的重读,直到我真正理解了正在发生的事情。
如果我觉得文件很重要,我喜欢把它们打印出来,然后看着它们。在空白处做笔记也是我的一种做法。如果你经常在旅途中,能够在你的笔记本电脑上携带数千份数字形式的作品是很好的,但我已经慢慢积累了一批我认为手边放着很重要的作品。
最后,慢慢来!深度远比广度更有价值。我发现,找到几篇真正有趣的论文并花时间彻底理解它们,比试图涉足一堆随机领域要好得多。
概述
我们已经分解了这个领域的一篇论文,试图通过结合来自学术界的半打其他技术来构建一个高效的网络级性能。我们已经讨论了如何开始自己阅读论文。
十三、回顾 MNIST
二十世纪有许多有趣的发明,但我认为电脑是最重要的一项。每年都有越来越多的计算周期被推向市场,每年都有对计算的兴趣和需求增加。我们可能已经达到了登纳德标度的极限,但还有几十年有趣的改进要做。
后续步骤
以下是我对不久的将来的看法:
-
更多内核
-
更多内存
-
更多带宽
-
更多定制硬件
-
更通用的硬件
核心通常很简单。我们已经达到了硅发展速度的极限,但我们可以继续在设备中制造额外的晶体管。那么最简单的方法就是简单地增加芯片上单个处理器的数量。AMD 最近针对处理器的锐龙小芯片方法表明,这可以持续很长时间。
RAM:如果你愿意的话,现在你可以为云服务器提供万亿字节的内存。在这方面还有很长的路要走。这方面的真正障碍不是内存大小,而是我们的下一个趋势。
带宽:PCI 4 已经上市,人们已经在研究 PCI 5 和 PCI 6。大多数现代系统的真正限制不再是内核,而是它们之间的协调和同步。我们已经达到了原始时钟速度的极限,所以现在关键的技巧是保持内核得到指令和数据。如果 Threadripper 上的每个内核实际上一个周期处理一点数据,那么我们的处理速度会突然超过我们的内存。
定制硬件:苹果的 ARM 处理器、英伟达的 GPU 和谷歌的 TPUv1,以及最近使用 TSMC 晶圆厂的 Cerberas 等新公司正在推动该行业的许多事情。他们正在将巨大的规模经济推向市场,并使人们有可能以低廉的价格租用晶圆厂空间,这反过来又使人们有可能以比以往任何时候都低得多的价格建造定制硅。你可以在软件中制作芯片原型,将设计发送出去,不久之后就可以通过邮件获得结果。这使得全新一代的硬件能够进入市场,我认为我们现在只是看到了可能性的开端。
通用硬件:对我来说,这是能够自己制造芯片的超级有趣的另一面。由于专利问题和交叉许可知识产权的需要,这些年来计算领域的许多进展都停滞不前。有一些开源芯片设计(RISC-V 就是一个很好的例子),您可以使用它们来免费构建一个现代的 64 位处理器。像 LLVM 这样的工具意味着,如果你能为你的架构建立一个导出模块,那么突然间你就能把整个软件生态系统带到你的新设备上。
棘手问题
希望所有给定的想法都不会引起你的争议。现在,我相信如果我们看看这些想法,我们可以看到一些清晰的趋势正在形成。
多核编程并不是一个新概念,但实际使用它才是。二十多年来,它已经在台式电脑上随处可见。话虽如此,实际上很少有软件真正使用了 CPU 上所有可用的能力,大多数程序员仍然停留在单线程编程模型中。大多数现代并行化是通过一次运行大量作业(例如,在一台服务器上托管十几个虚拟机,或者在一个队列中运行 10,000 个作业),而不是通过实际将单个作业适当拆分。
RAM 尤其是深度学习的一个重要限制因素,但这实际上是因为下一个问题,带宽。GPU 用于深度学习的真正力量不是 GPU 本身,而是内部内存/通信总线。速度越来越高的 RAM 是目前生态系统中最昂贵的组件之一,但每一次迭代都允许更多的数据通过 GPU 处理器运行,因此这一块将继续发展。我认为这项技术最终会回到 CPU 的领域,让它们做出更大的贡献。
带宽:GPU - > RAM 内存可以合理地很好地解决上述问题,但是每当我们想要尝试协调多个 GPU 的工作时,我们就又回到了触及 PCI 总线带宽限制的起点。Nvidia 很好地意识到了这一弱点,并竭尽全力为他们的 DGX 系列计算机实现了自定义的 GPU 内部网络堆栈(NCCL)。Habana Labs 的 Gaudi 通过在每个 ASIC 上粘贴一个 100 千兆以太网交换机,简单地取代了所有这些定制的硅和复杂性,以保证每个节点之间的 1tb 通信带宽。Nvidia 最近对交换机硬件制造商 Mellanox 的收购,对我来说也指向了这一未来。EGX A100 将 200Gbps Infiniband 放在每个 GPU 上,因此 PCI 总线不再是一个限制因素,多个卡可以拥有自己的专用背板来相互通信。然后,可以随意实施各种网络拓扑,而不必依赖于定制的通信协议,这意味着这种方法将很容易随着 200 和 400GbE 的上线而扩展。未来使用 800GbE 和 1.6TbE 将这一数字再翻一番应该也是可行的。
自定义运算:除了基本的 MAC 运算(这是当前大多数人工智能硬件的目标),仍然不确定什么样的数学运算在实践中最有用。一方面,你可以说 INT1、INT4、INT8 和 FP16 数学形式的技术方法是使现有操作更小并增加单次处理的数据量的自然延伸。另一方面,你可以在谷歌的 TPU 和英特尔即将推出的加速器中使用 BFloat16 这种务实的方法,通过降低处理缓冲区溢出的复杂性,简化了将 FP32 工作流移植到新设备的过程。Nvidia 的 Ampere 路线图显示,他们通过添加更大版本的 BFloat 方法(例如,支持 INT1、INT4、INT8、FP16、BFloat16、TFloat32、FP32、TFloat64、FP64)来支持基本上所有可能的操作,并将实际实现事情的责任放在编码器上。该平台令人兴奋的地方在于,通过对终端用户可用的操作进行标准化,不再有任何借口不使用定制的 precision 硬件。
通用硬件:对我来说,最有趣的静悄悄的革命是 ARM 芯片组,以及亚马逊最近将该平台用于下一代服务器硬件。通过从回路中去除专有硅,可以实现更高的规模效率。这将需要几年的时间来完全发挥出来,但这是我们将在不久的将来。ARM 和 RISC-V 将跟随前沿平台,悄悄地吸收它们带给市场的任何新创新。与此同时,专利硅技术将不得不与成本更低的商品化创新进行斗争。
TPU 案例研究
所有这些技术都很酷,但从根本上说,为了编写优化的软件,程序员必须提前计划他们的数据和内存访问。就像我之前说过的,我们已经达到了单一数据风格编程的极限,并且越来越需要学习如何拥抱特定于数据流的方法。让我们看看谷歌的 TPU,作为在实践中解决上述问题的一个例子:
-
内核:TPU 使用相当简单的 ASIC 逻辑,并将多个内核放在一个处理包中。然后,他们将许多这样的处理器连接在一起,形成一个环形拓扑结构,形成一个单一的 TPU 单元。
-
对于 RAM,Google 只是在每个单元上投入几百 GB 的 RAM 来简化本地内存访问。
-
带宽:这实际上是 TPU 系统的秘密能力之一。每个 TPU 都安装在一个定制的网络背板上,允许以极快的速度进行 pod 内部通信。多组 TPU 被放在一起,它们共享相同的网络背板,以优化通信。
-
自定义操作:BFloat16 简化了向 TPU 的移植逻辑,但从长远来看,他们正在考虑添加更多的自定义类型。TPUv1 实际上是 INT8,作为一个历史旁白。
-
这也在雷达之外,但每个 TPU 单元都有一个内部处理器,在内部处理许多更复杂的逻辑,以便 TPU 芯片可以专注于原始数学。积极研究的一个领域是,寻找方法在运行中进行预处理,以便芯片本身能够保持供给。
TensorFlow 1 + Pytorch
对我来说,从为 TPU 编写软件的角度来看,第一代 tensorflow 的许多设计决策和限制都是有意义的。对于像 TPU 这样的定制 ASIC 设备,您必须有一个预定义的图形,并且不能在运行中执行任意代码。如果您可以按需访问成千上万个 TPU 内核,那么关键的技巧是将您的代码分解成可以在每个内核上运行的单元,而不是简化整体逻辑。我认为 CUDA 支持是一种事后的想法,但该框架的成功是因为这是人们在现实世界中最有可能使用的实际硬件。谷歌花了很多周期优化 TPU 代码,却发现类似的优化在 CUDA 设备上不起作用,反之亦然。他们试图弥合差距,但越来越多地触及试图让不同的世界一起工作的极限。对于他们的内部工作,他们可以轻松地花钱雇人编写定制的 C++内核来优化运行在大型集群上的软件,但对于谷歌总部以外的人来说,这显然是不切实际的。
Pytorch 在过去几年中作为 Tensorflow 的替代产品迅速流行起来。这很大一部分是因为它允许人们使用内存中的(例如,非静态)图形,这使得调试更加简单(例如,我们可以附加一个调试器并在适当的位置查看网络变量,而不是必须添加日志语句并重复运行)。Tensorflow 2 完全支持这种模式,热切执行是未来的首选方法。同样,用于 Tensorflow 的 Keras Python 包装器已被提升为 Tensorflow 生态系统的成熟部分(例如,它现在是标准库的一部分)。
关于优化,Pytorch 只是走了一条更简单的路线,尽可能快地从高级代码转向 CUDA。这明显更容易优化,因此 Pytorch 团队的优化工作简单得多。然而,它们现在与 CUDA 紧密相连,并且延伸开来,与 Nvidia 能够推向市场的任何硬件紧密相连。他们一直在尝试在 Pytorch 和 CUDA 层之间添加编译器技术,但尽管这是问题所在,我不认为这是解决问题的正确地方。
进入功能编程
那么,对我来说,强迫程序员使用函数式范例是每个人的最终归宿。为了让编译器做出关于如何优化代码的正确决策,他们必须尽可能多地访问关于正在执行的操作的信息。试图生成一个中间代码块,然后对其进行分析以进行优化,可以产生短期的加速效果,但从长期来看,这是徒劳的。几十年的编译器理论告诉我们,无论元编译器有多聪明,它都无法与程序员竞争知道真正需要做什么。
或者,用一个虚构的例子来说,编译器有数千种方法来尝试和优化这个循环:
var i = 0
for n in 1...100000
{
i = i + n
}
print (i)
然而,一个人可以看到我们可以将其简化为
```f(n) = n * (n + 1) / 2```py
运用数学。对我来说,我们使用函数式编程的原因不是它本身更容易,而是通过迫使程序员以更严格的风格编码,我们使编译器更容易为我们做出关于如何实际执行事情的决定。我们现在正在牺牲一点时间,让我们以后的生活更简单。举例来说,我曾经写过很多 C 语言代码,但是我花在调试内存问题上的时间和我试图添加新特性的时间一样多。在这方面,Swift 招致了运行时损失,但另一方面,我有两倍的时间来实现新特性。当您学会信任编译器来捕捉/防止某些类别的错误时,下一个级别的函数式编程就来了,因此您可以专注于问题的核心逻辑,而不是细节。
无论你如何编写核心深度学习逻辑本身,每个人都必须弄清楚如何实际安排他们的工作。为了做到这一点,最好的方法是强迫最终用户使用与他们正在操作的实际数据相匹配的数据原语,并考虑到它将在其上运行的硬件。然后,编译器可以找出将给定数据转换成实际操作的最佳方式。即使您手工实现,这也是编写定制代码失败的地方,因为每次我们的终端硬件发生变化,我们都必须编写新的内核。
Swift + TPU 演示
时间会证明 Swift for Tensorflow 是否是更广泛的机器学习生态系统的前进方向。对谷歌本身来说,我相信这越来越成为他们未来做事的方式。让我们回到我们的第一个机器学习演示,一个应用于 MNIST 数据集的卷积神经网络,并使用 TPU 再次进行。为了将这个演示转换为在 TPU 上运行,从历史上来说,我们需要直接使用 C++或者通过一个高级 API(例如 Keras)来使用 c++,这个 API 对我们隐藏了粗糙的边缘。
要做到这一点,您需要按照 Google Cloud 一章中的说明设置一个远程服务器。你不需要 GPU 或 CUDA,因为你将使用 TPU。之后,您将需要在与您的服务器相同的区域中创建一个 TPU 实例,以便它们可以一起通信。首先确定您将在哪里创建 TPU (v3-8 是您所需要的全部),然后向后工作到您的主机服务器的区域。启动并运行系统后,为云系统设置以下 shell 参数:
export XLA_USE_XRT=1
export XRT_TPU_CONFIG="tpu_worker;0;<TPU_DEVICE_IP>:8470"
export XRT_WORKERS='localservice:0;grpc://
localhost:40934'
export XRT_DEVICE_MAP="TPU:0;/job:localservice/replica:0/ task:0/device:TPU:0"
现在,我们可以运行我们简单的 MNIST CNN 演示,使用 XLA 在 TPU 上运行我们的 swift 代码:
import Datasets
import TensorFlow
struct CNN: Layer {
var conv1a = Conv2D
var conv1b = Conv2D
var pool1 = MaxPool2D
var flatten = Flatten
var inputLayer = Dense
var hiddenLayer = Dense
var outputLayer = Dense
@differentiable
public func forward(_ input: 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()
var optimizer = SGD(for: model, learningRate: 0.1)
let dataset = MNIST(batchSize: batchSize)
let device = Device.defaultXLA
model.move(to: device)
optimizer = SGD(copying: optimizer, to: device)
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 deviceImages = Tensor(copying: images, to: device)
let deviceLabels = Tensor(copying: labels, to: device)
let (_, gradients) = valueWithGradient(at: model) { model -> Tensor
let logits = model(deviceImages)
return softmaxCrossEntropy(logits: logits, labels: deviceLabels)
}
optimizer.update(&model, along: gradients)
LazyTensorBarrier()
}
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 deviceImages = Tensor(copying: images, to: device)
let deviceLabels = Tensor(copying: labels, to: device)
let logits = model(deviceImages)
testLossSum += softmaxCrossEntropy(logits: logits, labels: deviceLabels).scalarized()
testBatchCount += 1
let correctPredictions = logits.argmax(squeezingAxis: 1) .== deviceLabels
correctGuessCount += Int(Tensor<Int32>(correctPredictions).sum().scalarized())
totalGuessCount = totalGuessCount + batch.data.shape[0]
LazyTensorBarrier()
}
let accuracy = Float(correctGuessCount) / Float(totalGuessCount)
print(
"""
[Epoch (epoch + 1)]
Accuracy: (correctGuessCount)/(totalGuessCount) ((accuracy))
Loss: (testLossSum / Float(testBatchCount))
"""
)
}
结果
您应该会看到与我们第二章类似的结果:
Starting training...
[Epoch 1] Accuracy: 9645/10000 (0.9645) Loss: 0.11085216
[Epoch 2] Accuracy: 9745/10000 (0.9745) Loss: 0.078900985
[Epoch 3] Accuracy: 9795/10000 (0.9795) Loss: 0.057063542
[Epoch 4] Accuracy: 9826/10000 (0.9826) Loss: 0.05429901
[Epoch 5] Accuracy: 9857/10000 (0.9857) Loss: 0.042912092
[Epoch 6] Accuracy: 9861/10000 (0.9861) Loss: 0.043906994
[Epoch 7] Accuracy: 9871/10000 (0.9871) Loss: 0.041553106
[Epoch 8] Accuracy: 9840/10000 (0.984) Loss: 0.050182436
[Epoch 9] Accuracy: 9867/10000 (0.9867) Loss: 0.044656143
[Epoch 10] Accuracy: 9872/10000 (0.9872) Loss: 0.040160652
[Epoch 11] Accuracy: 9876/10000 (0.9876) Loss: 0.041967977
[Epoch 12] Accuracy: 9878/10000 (0.9878) Loss: 0.041590735
概述
利用您对 Swift for Tensorflow 的了解,您已经在 TPU(或者根据需要在 CPU 或 GPU 上)上运行了一个定制内核。时间会告诉我们还会支持什么样的后端,但对我来说,这是拥抱这种方法的真正力量,即编写一次代码并在任何地方运行的能力。
十四、你在这里
恭喜你走到这一步!现在,您已经对使用 swift for tensorflow 进行图像识别的卷积神经网络的当前技术状态有了扎实的工作知识。让我们通过回顾过去来展望未来。
计算的历史(短暂而固执己见)
研究…的历史以了解它的未来是有价值的。有许多趋势只有在事后才看得出来。所以,让我们从头开始。硅谷的诞生可以说是二战后军事计算资金的泛滥。军方想要资助各种各样的东西,但是他们不能自己制造,所以他们开始从在硅谷建立的各种实验室购买硬件来制造晶体管。这是硅谷的真正起源,在知道有一个愿意购买极度 beta 技术的买家的情况下,有能力建造奇怪的新东西。
那么,互联网本身就是阿帕网项目的产物,阿帕网项目是由 DARPA 发起的,旨在将各种以前未连接的服务器联网。如果我们可以使用网络将本地的计算机连接在一起,那么将网络延伸几英里是一个相当合理的下一步。但引用梅特卡夫定律,随着每个新节点的加入,网络的价值呈指数增长。有趣的是,在某一点上,向网络添加新节点的价值超过了成本。在这一点上,向网络添加新计算机的过程变得自我维持,然后发展到我们今天所看到的情况。或者更确切地说,我认为在某一点上,发明本身的商业价值超过了启动它的成本,在这一点之后,就不可能停止互联网的发展。可以说,妖怪已经从瓶子里出来了。
在 20 世纪 70 年代,超级计算和人工智能出现了不同的现象。军方资助了该领域的许多不同战略,这些战略开始提出越来越多的古怪主张,以获得更大的份额。一旦人们清楚这些方法中的许多都行不通,人工智能的冬天就来了,当 DARPA 撤销了对这些项目中的许多项目的资助,该领域被迫尝试并保护自己。没有一个富有的捐助者,或者更准确地说,没有一个清晰的商业计划,超级计算和人工智能都陷入了困境。十年后,英国和日本经历了类似的现象。
因此超级计算机竞赛在很大程度上失败了。但是计算机已经证明了它们的普遍价值,因此总体上继续变得越来越便宜。个人计算开始起步,类似的情况也发生了,然而电脑对个人用户的价值超过了成本门槛,因此,个人电脑革命变得可以自我维持。由于对家用电脑的巨大兴趣,20 世纪 80 年代和 90 年代出现了个人电脑革命。我特别感兴趣的是 20 世纪 90 年代末的第三代超级计算浪潮,这在很大程度上是取下商用处理器(其发展速度远远超过专业超级计算制造商的梦想)并使用先进的网络将它们连接在一起以分布式方式解决问题的结果。商品化的通用硬件胜过构建专门的处理器和方法。大多数当前/第四代超级计算都遵循这一趋势,使用商用计算硬件并专注于定制网络以增加进程内通信。
GPU 的历史
所以,为了看另一个浪潮,我们可以考虑视频卡的故事。最初,计算机只能生成单色和基本文本。内存容量增加到可以存储更大量的数据,使得彩色成为可能,分辨率也逐渐提高。在某种程度上,实时光栅化 3D 图形成为可能,3dfx 将第一个真正的 GPU 推向了市场。使用图形编程语言,一个全新的交互体验(又名游戏)世界突然变得可能。因此,为了反映以前的互联网和个人计算浪潮,玩游戏的商业价值在芯片组中创造了一场自我维持的革命,这场革命今天仍在继续。我们今天在图形卡上运行模型的全部原因是由于几十年前视频游戏的流行。
GPU 也即将被商品化所消费。尽管目前新体验市场仍在继续增长,但即使是预算卡也支持 4k 视频等功能,这在几年前是不可想象的。在 GPU 上运行非游戏代码(特别是比特币和深度学习)本身是一项非常近期的创新,为市场注入了新的活力。制造这些设备的公司很快就达到了原始处理的极限,从而使这一切成为可能。他们试图将新的硬件推向市场,同时又不偏离驱动一切的游戏市场太远。这是推动 VR 和 AR 体验的很大一部分。随着 GPU 变得越来越通用,它们越来越多地吸收了越来越多以前仅由 CPU 控制的计算堆栈。
云计算
虚拟机极大地改变了人们与计算的交互方式,即使他们没有意识到这一点。一度,设置和配置服务器需要几天时间;现在几秒钟就能搞定。这使得工作流中的资源按需启动,然后立即丢弃。软件越来越多地在越来越高的抽象层次上运行,这使得全新的方法变得司空见惯。这将产生我们今天甚至无法完全理解的长期影响。世界上最大的计算集群不是超级计算机,而是为云提供商运行数千台虚拟机的托管服务器。
跨越鸿沟
AI 和 ML 都不是新领域。感知器形式的神经网络发明于 1958 年。直到最近,随着计算能力和硬件的进步,它们才变得实际可行。此外,我认为他们终于跨越了知识好奇心的鸿沟,进入了推动大公司底线的领域。因此,他们已经进行了必要的转变,成为一种自我维持的技术,就像给出的例子一样。谷歌明天可能会删除 tensorflow 知识库。英伟达可能会停止发售显卡。但是不管怎样,这些技术将继续被改进和完善,因为它们在行业中有真实的实际使用案例。因此,妖怪已经从瓶子里出来了。没有办法回到人工智能出现之前的世界。无论如何,人工智能带来的收益将被带到每个领域。
计算机视觉
让我们来看看我认为在未来十年中很重要的几个大领域。
直接应用
许多更先进的计算机视觉形式终于看到运行它们所需的硬件和计算能力成为主流。我对实时系统领域特别感兴趣,无论是自动驾驶汽车上的摄像头,还是能够在现场分析医疗数据,甚至只是找到使用手机摄像头的新方法。这个领域现在才刚刚开始被触及。
间接应用
许多不一定与图像相关的有趣问题可以转换成图像,然后使用 CNN 风格的方法解决。历史上,从资源的角度来看,这些技术中的许多都是不切实际的,但是随着越来越多的人工智能专用硬件成为主流,许多以前不可行的方法变得可行。以 AlphaGo 为例,它是一种大规模强化算法,将棋盘游戏 Go 的游戏状态转换为图像表示,然后对其应用一个极其庞大的卷积神经网络。不过,基本方法是使用剩余层和大规模计算构建的卷积神经网络。当普通研究人员获得类似数量的资源时,我认为许多有趣的新方法将在刚刚开始人工智能实验的领域中被发现。
自然语言处理
通过使用大数据方法(例如,从维基百科、扫描书籍和互联网收集的数据语料库),简单的方法突然变得强大,因为它给了机器更多的信息来处理。这反过来会产生直接的财务影响(例如,改进搜索和推荐引擎),因此现在有大量的资源投入到这方面。它最终会变得司空见惯。
强化学习和 GANs
我在短期内对这些领域有些悲观,因为它们似乎仍然需要大量的资源,而且目前仍然没有很多明确的商业应用。话虽如此,我相信从长远来看,这是最有可能推动 AI/ML 发展的领域。现在,计算机视觉的大多数改进都是非常小的增量调整,任何时候一个想法在 RL 的上游显示出前景,那么很快人们就会试图在其他地方使用它。使用合成数据训练神经网络似乎是最有可能在不久的将来成为商业驱动力的领域。超级采样/分辨率正在进入硅领域,并且很明显将会持续下去。
一般模拟
另一个有趣的领域,我认为即将被神经技术革命的是一般的物理模拟。大量的计算能力被有规律地投入到基于物理的复杂交互模拟中。我不看好神经网络直接取代物理模拟,因为原始数学总会有一席之地,但使用网络模拟真实世界的数据集打开了一个有趣的窗口,可以说,能够模拟模拟,并且通过扩展,能够比传统方法更快地建立近似正确的模型。如果基于神经网络的模拟证明了自己,那么传统方法可以作为最后阶段运行,提供两个世界的最佳效果(例如,快速实验和需要时的基本严格性)。网络有脱离现实的危险(例如,模拟错误的东西),但是我相信有领域专家会避免这个问题。
到无限和更远
我的经验是,这个领域作为一个整体,现在并不缺乏想法。arXiv 上每年都有数千篇论文发表,而且提交率还在持续增长。许多其他领域,特别是数学,似乎最终确信深度学习技术会一直存在,并且它们需要跟上潮流,所以许多非常聪明的人正在做这些 hello world 练习,就像你一样。从短期来看,这会造成大量人员流失。人们发表了无数的博客文章,试图解释他们的新想法,并在网上讨论最佳方法。pytorch 或 tensorflow 的每个新的主要版本都以各种令人兴奋的新方式打破了现有的项目。人们对复杂性束手无策,并决定创建一个新的统一系统来做事,瞧,又有了一个新的框架。就在我们说话的时候,这一切正在发生。整个行业正蹒跚地从闪亮的东西走向闪亮的东西。简单的事实是,没有人真正知道正确的前进道路是什么。新技术每天都在被发现,深度学习方法已经汇集了几十个相关领域。神经网络和大数据方法已经在生物学、天文学、物理学和经济学等完全不同的问题上证明了自己。现在每个领域都必须学习计算机科学,否则他们会被那些学习的人甩在后面。
所以让我告诉你 iOS 早期的程序员的故事。到了第二代,苹果允许人们提交应用。有一次大规模的淘金热,人们可以(也确实试图)将世界上几乎所有的东西都运出去。接下来的几年很有趣,因为越来越多的方法最终稳定下来并流行起来。过了一段时间,库和框架变得标准化了。对我来说,所有这些深度学习的喧嚣都是很久以前的相同经历。
为什么是 Swift
Swift 是 iOS 生态系统中一场有趣的革命。Objective-C 正在显示它的年龄,swift 匆忙地把 iOS 程序员带了很长一段路。垃圾收集是这一领域的一种传统方法,在具有大量内存和空闲周期来运行垃圾收集的系统上运行良好。但是在具有严格实时要求的生产系统中,无论是提供 24/7 包处理保证的服务器还是具有准随机使用模式的移动设备,这种方法都不能达到预期的效果。Android 试图通过让制造商在他们的设备上安装越来越多的 RAM 来掩盖这一差距,但这使得设备成本更高,这在现实世界中往往不可行。
LLVM 最初以自动引用计数的形式潜入 iOS,这是 Objective-C 中添加的一个特性,用于计数/跟踪内存周期,并通过扩展能够为开发人员手动添加 malloc 和免费调用。一旦这项技术证明了自己,通过消除程序员日常工作流程中的内存管理,Lattner 等人将目光放得更高。
Swift 被设计成一种现代语言,对于现有的 Objective-C 程序员来说不会显得格格不入,我觉得在这一点上它非常成功。它将函数式编程的思想和概念带入了 iOS 世界,使两个世界之间的沟通变得容易。有一段特定的代码需要 C #原始内存访问?只需直接进入原始内存访问,编译器就可以对整个代码区域进行边界检查。是否已有需要移植到 swift 的 C 库?简单地写一个简单的 API 层来封装你的库。然后,iOS(以及最终的 Mac)的所有系统级通信都被迫通过一个快速的间接层。从短期来看,这是令人痛苦的,因为它迫使编码人员不再能够直接进行系统调用。但是随着时间的推移,这种方法极大地模块化了系统级的代码库,并隔离了许多不同的 bug。
当苹果在吃自己的狗粮时,iOS 开发者也在经历类似的转变。早期涌现了许多开源库,每一个都有自己的权衡和模式。通过转向 swift,这迫使大部分生态系统要么进化,要么停留在过去。然而,反过来,这种转变允许人们专注于更高层次的问题,而不是停留在低层次的细节上。
因此,苹果做了关键的最后一步,将这种语言开源,并向外部开发者完全开放,让他们做出贡献,塑造其未来。任何人都可以投稿,现在已经有数千人投稿了。新的编程语言的产生极其困难。小众语言通常默默无闻。大公司在世界上推广新语言,但这种自上而下的方法通常只有在原始公司推动进步的情况下才有效。
对我来说,swift 的优势是多方面的。对初学者来说是一门简单易学的语言。它有一个致力于其成功的大捐助者(苹果)的支持,但不是技术上的负责人。它有一个开放源码贡献者的多样化生态系统,并且利用几十年来构建 C 库的经验,每天都在解决现实世界中的实际问题。它以一种实用的方式将函数式编程概念带到了过程世界,而不强迫人们完全改变他们做事的方式。
为什么选择 LLVM
然而,Swift 真正的魅力在于它是 LLVM 的原始语言。编译器历来注重生成非常非常快的代码。这对于进步来说是很好的,但是也意味着许多实现追求速度而不是正确地做事,可以这么说。结果是,我们最终用许多不同的编译器为几十台稍有不同的计算机生成稍有不同的代码,然后构建系统变得非常庞大和复杂。生成一种新的编程语言变得非常困难,因为人们一开始就要求性能。
LLVM 重建了编译器理论的基础,并通过重新统一这些领域,催生了新语言的复兴。从高层次上来说,您所要做的就是生成一个 IR,然后 LLVM 可以想出如何让它在您的设备上运行。这意味着现在有很多很多不同的语言在使用 LLVM。直接的结果是,通过使用 LLVM,您可以获得许多不同生态系统的集体改进。
就复杂性而言,这对于程序员来说是一项多一点的工作,但结果是从根本上使编译器有可能做更多的事情。我们已经看到了 LLVM 领域的惊人进步;人们已经展示了在大型集群和其他方法上运行巨大的作业。
机器学习在许多方面仍处于起步阶段。单 GPU 代码是最大的范例。人们为集群写东西,但是大部分时间仍然是非常定制的代码。我们在单指令和单变量代码(CPU 风格的编程)方面有大量的经验,但是从历史上看,单指令多数据代码很难编写。我们最终会为不同的东西手工定制很多内核。这在一般意义上很好,因为程序员可以进行系统调用并获得优化的代码,但这意味着程序员很难轻松利用他们手头的任何硬件。
为什么是 MLIR
最终的结果是,在过去的几年里,机器学习生态系统发生了巨大的变化。每个制造商最终都试图构建库来为他们的硬件提供最佳体验。研究人员试图让 tensorflow 做许多它从未被设计过的事情,因此试图支持每一种排列对谷歌来说都很困难。Pytorch 有效地重建了一个框架,只是为了使生成 CUDA 代码更简单。MLIR 为这两个世界提供了一座便捷的桥梁。硬件制造商可以简单地将注意力集中在为他们的设备生成代码的 IR 上。可以说,编码人员可以用他们喜欢的任何语言编写,然后语言专家只需要找到一种方法将他们的 LLVM AST 转换成 MLIR 语法。然后我们可以梦想一个未来,我们可以使用 swift(或任何支持 LLVM 的语言)代码,并可以为任何我们想要的后端编译它。
为什么 ML 是最重要的领域
未来几十年,机器学习有能力吸收世界上所有的计算能力。这场静悄悄的革命将在几十个领域产生影响。我们越来越容易使用这些工具,让它们能够灵活地扩展到越来越大的计算系统上,人类作为一个整体的长期潜力就越大。大规模计算有能力从根本上做以前不可能做的事情。
集群的规模只会越来越大。但所有这些对扩展的强调都忽略了一个事实,即今天个人可用的计算资源比历史上任何时候都多。如果你现在愿意投入时间和精力,那么随着这些事情的不断改善,你将是第一个能够利用这场革命的人。
为此,你可以走两条路。一种是选择一匹特别的马,不管是硬件还是框架,全力以赴。另一个是专注于帮助它,这样就不会有特定的框架或技术获得对生态系统的控制。让所有这些不同的群体作为一个整体一起工作有可能从根本上彻底改变这个领域。
硬件现在刚刚被弄清楚,但这将在未来几年内发生巨大变化。我承认,这个软件现在有点粗糙。但是机遇从来不会被整齐地包装在一个带蝴蝶结的包裹里。通常情况下,这看起来像是一项艰苦的工作。但是今天做一点点工作会让你为明天带来的一切做好准备。
为什么不
进步是许多许多人几个世纪共同努力的结果,而不是孤立于任何一个地方或时间。通过帮助机器学习变得更加容易,你正在帮助改进工具,这些工具将间接影响数百万其他人的生活。这有可能带来历史上前所未有的进步。
你是谁
你可以等待别人给你带来未来,或者帮助他们建设未来。现在是开始行动的最佳时机。未来就是现在!来加入我们吧!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
2020-10-02 《线性代数》(同济版)——教科书中的耻辱柱