TensorFlow-CNN-与-Swift-教程-二-

119 阅读1小时+

TensorFlow CNN 与 Swift 教程(二)

原文:Convolutional Neural Networks with Swift for Tensorflow

协议:CC BY-NC-SA 4.0

九、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) -> 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(padding: ((0, 1), (0, 1)))

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) -> 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) -> Tensor { return blocks.differentiableReduce(input) { 1(1(0) } } }

public struct MobileNetV2: Layer { @noDerivative public let zeroPad = ZeroPadding2D(padding: ((0, 1), (0, 1))) 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) -> 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) -> 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(padding: ((0, 1), (0, 1))) @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) -> 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) -> Tensor { return blocks.differentiableReduce(input) { 1(1(0) } } }

public struct EfficientNet: Layer { @noDerivative let zeroPad = ZeroPadding2D(padding: ((0, 1), (0, 1))) 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) -> 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,作为在实践中解决上述问题的一个例子:

  1. 内核:TPU 使用相当简单的 ASIC 逻辑,并将多个内核放在一个处理包中。然后,他们将许多这样的处理器连接在一起,形成一个环形拓扑结构,形成一个单一的 TPU 单元。

  2. 对于 RAM,Google 只是在每个单元上投入几百 GB 的 RAM 来简化本地内存访问。

  3. 带宽:这实际上是 TPU 系统的秘密能力之一。每个 TPU 都安装在一个定制的网络背板上,允许以极快的速度进行 pod 内部通信。多组 TPU 被放在一起,它们共享相同的网络背板,以优化通信。

  4. 自定义操作:BFloat16 简化了向 TPU 的移植逻辑,但从长远来看,他们正在考虑添加更多的自定义类型。TPUv1 实际上是 INT8,作为一个历史旁白。

  5. 这也在雷达之外,但每个 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(filterShape: (3, 3, 1, 32), padding: .same, activation: relu) var conv1b = Conv2D(filterShape: (3, 3, 32, 32), padding: .same, activation: relu) var pool1 = MaxPool2D(poolSize: (2, 2), strides: (2, 2))

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

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

let batchSize = 128 let epochCount = 12 var model = CNN() 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 in 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 是最重要的领域

未来几十年,机器学习有能力吸收世界上所有的计算能力。这场静悄悄的革命将在几十个领域产生影响。我们越来越容易使用这些工具,让它们能够灵活地扩展到越来越大的计算系统上,人类作为一个整体的长期潜力就越大。大规模计算有能力从根本上做以前不可能做的事情。

集群的规模只会越来越大。但所有这些对扩展的强调都忽略了一个事实,即今天个人可用的计算资源比历史上任何时候都多。如果你现在愿意投入时间和精力,那么随着这些事情的不断改善,你将是第一个能够利用这场革命的人。

为此,你可以走两条路。一种是选择一匹特别的马,不管是硬件还是框架,全力以赴。另一个是专注于帮助它,这样就不会有特定的框架或技术获得对生态系统的控制。让所有这些不同的群体作为一个整体一起工作有可能从根本上彻底改变这个领域。

硬件现在刚刚被弄清楚,但这将在未来几年内发生巨大变化。我承认,这个软件现在有点粗糙。但是机遇从来不会被整齐地包装在一个带蝴蝶结的包裹里。通常情况下,这看起来像是一项艰苦的工作。但是今天做一点点工作会让你为明天带来的一切做好准备。

为什么不

进步是许多许多人几个世纪共同努力的结果,而不是孤立于任何一个地方或时间。通过帮助机器学习变得更加容易,你正在帮助改进工具,这些工具将间接影响数百万其他人的生活。这有可能带来历史上前所未有的进步。

你是谁

你可以等待别人给你带来未来,或者帮助他们建设未来。现在是开始行动的最佳时机。未来就是现在!来加入我们吧!