苹果的深度学习框架:BNNS 和 MPSCNN 的对比

Posted SwiftGG翻译组

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了苹果的深度学习框架:BNNS 和 MPSCNN 的对比相关的知识,希望对你有一定的参考价值。

译者:TonyHan;校对:冬瓜,liberalism;定稿:CMB

ios 10 开始,苹果在 iOS 平台上引入了两个深度学习的框架:BNNS 和 MPSCNN。

  • BNNS:全称为香蕉(bananas,译者注:此处开玩笑),Basic Neural Network Subroutines,是 Accelerate  框架的一部分。这个框架能够充分利用 CPU 的快速矢量指令,并提供一系列的数学函数。

  • MPSCNN 是 Metal Performance Shaders 的一部分。Metal Performance Shaders 是一个经过优化过的计算内核框架,并且可以运行在 GPU (而不是 CPU)上。

所以,作为 iOS 开发者,有了两个用于做深度学习的框架,它们有很多类似的地方。

应该选择哪个呢?

在这篇文章中,我们将针对 BNNS 和 MPSCNN 进行对比来显示出这两者的差异。而且我们会对这两个 API 进行速度测试,来看下谁更快一些。

为什么要优先使用 BNNS 或 MPSCNN ?

我们首先讨论这两个框架的作用。

目前 BNNS 和 MPSCNN 在卷积神经网络领域中用于变分推断。

与类似的 TensorFlow(使用此方案可以通过构建一个计算图,从头开始建立你的神经网络)相比,BNNS 和 MPSCNN 提供更高级的 API,不需要担心你的数学。

这也有一个缺点:BNNS 和 MPSCNN 的功能远远少于其他框架,如 TensorFlow。它们更容易上手,但同时限制了所能做的深度学习的种类。

苹果的深度学习框架只是为了一个目的:通过网络层级尽可能快地传递数据。

一切都与层级有关

你可以将神经网络想象为数据流经的管道。管道中的不同阶段便是网络层级。这些层级以不同的方式转换你的数据。同时深度学习,我们可以使用多达 10 层甚至 100 层的神经网络。


层级有不同的种类。BNNS 和 MPSCNN 提供的有:卷积层(convolutional layer)、池化层(pooling layer)、全连接层(Fully Connected Layer)和规范化层(normalization layer)。

在 BNNS 和 MPSCNN 中,层级是主要的建构单元。你可以创建层级对象,将数据放入层级中,然后再从层级中读出结果。顺便说一句,BNNS 称它们为“过滤器”,而不是层级:数据以一种形式进入过滤器并以另一种形式从过滤器出来。

为了说明层级作为建构单元的思想,下面描述了数据如何通过一个简单的神经网络在 BNNS 中流动:

// 为中间结果和最终结果分配内存。
var tempBuffer1: [Float] = . . .
var tempBuffer2: [Float] = . . .
var results: [Float] = . . .
// 对输入的数据(比如说一张图片)应用第一层级。
BNNSFilterApply(convLayer, inputData, &tempBuffer1)
// 对第一层级的输出应用第二层级。
BNNSFilterApply(poolLayer, tempBuffer1, &tempBuffer2)
// 应用第三和最后的层级。结果通常是概率分布。
BNNSFilterApply(fcLayer, tempBuffer2, &results)

要使用 BNNS 和 MPSCNN 构建神经网络,只需要设置层级并向它们发送数据。框架负责处理层级发生的事情,但你需要做的是连接层级。

不幸的是,这可能有点无聊。例如,通过加载一个提前训练好的 caffemodel 文件来获取一个完整配置的“神经网络”是行不通的。你必须手写代码,仔细地创建层级并进行配置来复制出网络的设计。这样就很容易犯错。

BNNS 和 MPSCNN 不做训练

在你使用神经网络之前,你必须先训练它。训练需要大量的数据和耐心——至少几个小时,甚至几天或几周,取决于你可以投入多少计算能力。你肯定不想在手机上进行训练(这可能会使手机着火)。

当得到一个训练网络,便可以用来进行预测。这被称为“推断”。训练本应需要使用重型计算机,但在现代的手机上进行推断是完全可能的。

这正是 BNNS 和 MPSCNN 设计的目的。

仅限卷积网络

但是这两个 API 都有限制。目前,BNNS 和 MPSCNN 仅支持一种深度学习:卷积神经网络(CNN)。CNN 的主要应用场景是机器视觉任务。例如,你可以使用 CNN 来描述给定照片中的对象。

虽然 CNN 很牛逼,但在 BNNS 或 MPSCNN 中无法支持其他深度学习架构(例如递归神经网络)。

然而,已经提供的建构单元(卷积层,池化层和全连接层)高效并且为构建更复杂的神经网络提供了良好的基础,即便你必须手工编写一些代码来填补 API 中的空白。

  • 备注:Metal Performance Shaders 框架还附带有用于在 GPU 上进行快速矩阵乘法的计算内核。同时,Accelerate 框架包含用于在 CPU 上执行相同操作的 BLAS 库。所以,即使 BNNS 或 MPSCNN 不包含你所需的深层学习架构的所有层级类型,你也可以借助这些矩阵例程来推出自己的层级类型。而且,如果有必要的话,你可以用 Metal Shading Language 编写你自己的 GPU 代码。

不同之处

假如它们的功能一致,那为何 Apple 要给我们两个 API?

很简单:BNNS 运行在 CPU 上,MPSCNN 运行在 GPU 上。有时使用 CPU 速度更快,有时使用 GPU 更快。

  • “等一下…难道 GPU 不是高度并行的计算怪物么?难道我们不应该一直在 GPU 上运行我们的深层神经网络吗?!”

并没有。对于培训,你一定希望通过 GPU 来进行大规模并行计算(即使只是一个许多 GPU 的集群)但推论时,使用枯燥的旧的 2 或 4 核 CPU 可能会更快。

下面我将详细讨论的速度差异,但首先让我们来看看这两个 API 是有哪些不同。

  • 备注:Metal Performance Shaders 框架仅适用于 iOS 和 tvOS,不适用于 Mac。BNNS 也适用于 macOS 10.12 及更高版本。如果你想要保证 iOS 和 MacOS 之间的深度学习代码的可移植性,BNNS 是你唯一的选择(或使用第三方框架)。

它是 Swifty 的么?

BNNS 实际上是一个基于 C 的 API。如果你使用 Objective-C 是可以的,但 Swift 使用它有点麻烦。相反,MPSCNN 更兼容 Swift。

不过,你必须接受这些 API 比所谓的 UIKit 更低级的事实。Swift 并没有将所有的东西都抽象成简单的类型。你经常需要使用 Swift 的 UnsafeRawPointer 指针来处理原始字节。

Swift 也没有一个原生的 16 位浮点类型,但是 BNNS 和 MPSCNN 在使用这样的半精度浮点数时才是最高效的。你将不得不使用 Accelerate 框架在常规类型和半精度浮点数之间进行转换。

从理论上讲,当使用 MPSCNN 时,你不必自己编写任何 GPU 代码,但实际上我发现某些预处理步骤——如从每个图像像素中减去平均 RGB 值,使用 Metal Shading Language(基于 C++ 实现) 中的定制的计算内核是最容易实现的。

所以,即使你在 Swift 中使用这两个框架,也要准备好用这两个 API 来进行一些底层级别的 Hacking 行为。

激活函数

随着数据在神经网络中从一层流向下一层,数据在每层都会以某种方式被转换。层级应用了激活函数,来作为此转换的一部分。没有这些激活函数,神经网络将无法学习非常有趣的事情。

激活函数有很多选择,BNNS 和 MPSCNN 都支持最常用的功能:

  • 修正线性单元(ReLU)和带泄漏修改线性单元(Leaky ReLU)

  • 逻辑函数(logistic sigmoid)

  • 双曲正切函数(tanh)和 扩展双曲正切函数(scaled tanh

  • 绝对值

  • 恒等函数(the identity function),它传递数据而不改变数据

  • 线性(只在 MPSCNN 上)

你会认为这与 API 一样简单,但是奇怪的是,与 MPSCNN 相比,BNNS 有一个不同的定义这些激活函数的方式。

例如,BNNS 定义了两种类型,BNNSActivationFunctionRectifiedLinearBNNSActivationFunctionLeakyRectifiedLinear,但在 MPSCNN 中,只有一种 MPSCNNNeuronReLU 类型,使用 alpha 参数来标记是否为带泄漏的修正线性单元(Leaky ReLU)。同样的还有双曲正切函数(tanh)和 扩展双曲正切函数(scaled tanh)。

可以肯定地说,MPSCNN 采用比 BNNS 更灵活和可定制的方法。整个 API 层面都是如此。

例如:MPSCNN 允许您通过继承 MPSCNNNeuron 并编写一些 GPU 代码来创建自己的激活函数。使用 BNNS 就无法实现,因为没有用于定制的激活函数的 API;只提供了枚举。如果你想要的激活函数不在列表中,那么使用 BNNS 就会掉进大坑。

  • 17年2月10号更新:以上内容有点误导,所以我应该澄清下。由于 BNNS 在 CPU 上运行,你可以简单地获取层级的输出并根据你的喜好进行修改。如果你需要一种特殊的激活函数,你可以在 Swift 中自己实现(最好使用 Accelerate 框架)并在进入下一层之前将其应用于上一层的输出。所以 BNNS 在这方面的能力不亚于 Metal。

  • 17年6月29日更新:关于 MPSCNNNeuron 子类的澄清:如果你这样做,实际上并不能使用 MPSCNNConvolution 的子类。这是因为 MPS 在 GPU 内核中执行激活函数时使用了一个技巧,但这只适用于 Apple 自己的 MPSCNNNeuron 子类,不适用于你自己创建的任何子类。

事实上,在 MPSCNN 中,一切都是 MPSCNNKernel 的一个子类。这意味着你可以单独使用一个激活函数,如 MPSCNNNeuronLinear,就像它是一个单独的层级一样。在预处理步骤中,这对以常量进行缩放数据是很有用的。(顺便说一句,BNNS 没有类似于“线性”的激活函数。)

  • 备注:在我看来,感觉就像 BNNS 和 MPSCNN 是由 Apple 内部不同的团体创建的。它们有非常相似的功能,但它们的 API 之间有一些奇怪的差异。我不在 Apple 公司工作,所以我不知道这些差异存在的原因。也许是出于技术或性能的原因。但是你应该知道 BNNS 和 MPSCNN 不是“热插拔”的。如果你想要知道在 CPU 或 GPU 上进行推理时哪种方法最快,你将不得不实现两次深度学习网络。

层级类型

我之前提到深层神经网络是由不同类型的层级组成的:

  • 卷积(Convolutional)

  • 池化(Pooling),最大值和平均值

  • 完全连接(Fully-connected)

BNNS 和 MPSCNN 都实现了这三种层级类型,但是每种 API 的实现方式都有细微差别。

例如,BNNS 可以在池化层中使用激活函数,但是 MPSCNN 不行。但是,在 MPSCNN 中,你可以将激活函数添加到池化层后面作为单独的一层,所以最终这两个 API 能实现相同的功能,但是它们实现的路径不同。

在 MPSCNN 中,完全连接层被视为卷积的一个特例,而在 BNNS 中,它被实现为矩阵向量乘法。实践中并不会有差别,但是这表明这两个框架采取了不同的方法来解决同样的问题。

我觉得对于开发者来说,MPSCNN 使用起来更方便。

当对图像使用卷积时,除非添加“填充”像素,否则输出图像会缩小一些。使用 MPSCNN,就不必担心这一点:你只需告诉它,希望输入和输出图像有多大。使用 BNNS 你就必须自己计算填充量。像这样的细节让 MPSCNN 成为更易用的 API。

除了基础层级,MPSCNN 还提供以下层级:

  • 归一化(特征归一化、跨通道归一化(弱化)、局部对比度归一化)

  • Softmax,也称为归一化指数函数

  • 对数 Softmax,即使用 Softmax 函数并配合 log 似然代价函数

  • 激活函数层

这些额外的层级类型无法在 BNNS 中找到。

对于规范化层来说,这可能不是什么大问题,因为我觉得它们并不常见,但 softmax 是大多数卷积网络在某些时候需要做的事情(通常在最后)。

softmax 函数将神经网络的输出转化为概率分布:“我 95% 肯定这张照片是一只猫,但只有 5% 确定它是一只 Pokémon 。”

在 BNNS 中没有提供 softmax 是有点奇怪的。在 Accelerate 框架中使用 vDSP 函数来写代码实现并不难,但是也不是很方便。

学习参数

训练神经网络时,训练过程会调整一组数字来表示网络正在学习什么。这些数字被称为学习参数。

学习参数由所谓的权重和偏差值组成,这些值只是一些浮点数。当你向神经网络发送数据时,各层级实际上将你的数据乘以这些权重,添加偏差值,然后再应用激活函数。

创建层级时,需要为每个层级指定权重和偏差值。这两个 API 只需要一个原始指针指向浮点值的缓冲区。需要由你来确保这些数字以正确的方式组织。如果这里操作错误,神经网络将会输出垃圾数据。

你可能猜到了:BNNS 和 MPSCNN 为权重使用不同的内存分配。

以上是关于苹果的深度学习框架:BNNS 和 MPSCNN 的对比的主要内容,如果未能解决你的问题,请参考以下文章

深度学习系列50:苹果m1芯片加速pytorch

深度学习系列50:苹果m1芯片加速pytorch

深度学习系列50:苹果m1芯片加速pytorch

iOS 11 : CORE ML—浅析

如何免费云端运行Python深度学习框架?

移动端深度学习框架汇总