专栏名称: 唤之
目录
相关文章推荐
码农翻身  ·  Bill Gates 和 Linus ... ·  9 小时前  
OSC开源社区  ·  2024年AI编程工具的进化 ·  昨天  
OSC开源社区  ·  如何公正评价百度开源的贡献? ·  2 天前  
程序员的那些事  ·  字节用 8266.8 ... ·  3 天前  
码农翻身  ·  DeepSeek彻底爆了! ·  昨天  
51好读  ›  专栏  ›  唤之

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

唤之  · 掘金  · 程序员  · 2018-05-25 03:21

正文

作者:MATTHIJS HOLLEMANS, 原文链接 ,原文日期:2017-02-07
译者: TonyHan ;校对: 冬瓜 liberalisman ;定稿: 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 层的神经网络。

Cat2Probability Cat2Probability

层级有不同的种类。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 定义了两种类型, BNNSActivationFunctionRectifiedLinear BNNSActivationFunctionLeakyRectifiedLinear ,但在 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 为权重使用不同的内存分配。😅

对于 MPSCNN 权重数组看起来像这样:

weights[ outputChannel ][ kernelY ][ kernelX ][ inputChannel ]

但是对于 BNNS 来说,顺序是不同的:

weights[ outputChannel ][ inputChannel ][ kernelY ][ kernelX ]

我认为 MPSCNN 将输入通道放在最后的原因是,这样可以很好地映射到存储数据的 MTLTexture s 中的 RGBA 像素。但是对于 BNNS 所使用的 CPU 矢量指令,将输入通道视为单独的内存块会更高效。

这种差异对于开发者来说不是一个大问题,但是当你导入训练好的模型时你需要 知道权重的内存分配

备注:你可能需要编写一个转换脚本来导出培训工具中的数据,例如 TensorFlow 或 Caffe,并将其转换为 BNNS 或 MPSCNN 预期的格式。这两个 API 都不能读取这些工具所保存的模型,它们只接受原始的浮点值的缓冲数据。

MPSCNN 总是复制权重和偏差值,并将它们作为 16 位浮点内部存储。由于你必须将它们作为单精度浮点数提供,因此这有效地将你的学习参数的精度减半。

BNNS 在这里比较开放一些:它可以让你选择你想要存储学习参数的格式,也可以让你选择不复制。

将权重加载到网络中仅仅在创建网络时的 App 启动时起到重要作用。但是,如果你有大量的权重,你仍然需要认真对待。我的 VGGNet implementation 不能在 iPhone 6 上工作,因为 App 试图一次性将所有权重加载到 MPSCNN 时导致内存不足。(可以先创建大的层级,然后是再创建较小的层级。)

输入数据

一旦你创建了所有的层级对象,你终于可以开始使用神经网络进行推断啦!

正如你所看到的,BNNS 或 MPSCNN 都没有真正的“神经网络”的概念,他们只能看到每个层级。你需要逐个将数据放入这些层级中的每一层。

作为神经网络的用户,你关心的数据是进入第一层(例如一张图片)的输入和从最后一层出来的输出(这张图片是猫的概率)。其他在各层之间传递的数据,只是临时的中间结果。

那么你需要输入什么格式的数据?

MPSCNN 要求将所有数据放置在一个特殊的 MPSImage 对象内,这个对象实际上是 2D 纹理的集合。如果你正在使用图片,这会非常有意义 - 但是如果你的数据不是图片,则需要将其转换为 Metal 纹理。这会消耗 CPU 的性能。(你可以使用 Accelerate 框架来解决这个问题。)

备注:iOS 设备使用统一的内存模型,这意味着 CPU 和 GPU 访问相同的 RAM 芯片。与桌面计算机或服务器上的情况不同,你不需要将数据复制到 GPU。所以至少你的 iOS App 不会有这些性能消耗。

另一方面,BNNS 只需要一个指向浮点值缓冲区的指针。不需要将数据加载到特定对象中。所以这似乎比使用纹理更快…是么?

这样有一个重要的限制:在 BNNS 中,不同“通道”中的输入不能交错。

如果你的输入是图片,那么它有三个通道:一个用于红色像素,一个用于绿色像素,另一个用于蓝色像素。问题是像 PNG 或 JPEG 这样的图像文件会作为交错的 RGBA 值被加载到内存中。BNNS 并不会接受这种情况。

目前没有办法告诉 BNNS 使用红色像素值作为通道 0,绿色像素值作为通道 1,蓝色值作为通道 2,并跳过 alpha 通道。相反,你将不得不重新排列像素数据,以便输入缓冲区的首先包含所有 R 值,然后是所有 G 值,然后是所有 B 值。

我们若采取这种预处理,就会占用宝贵的计算时间。其次,也许这些限制允许 BNNS 在其层级如何执行其的计算方面做一定的优化,从而使整个事情是个净增益。但这谁也不知道。

在任何情况下,如果您使用 BNNS 处理图像(CNNs主要的用途)那么你可能需要对输入数据进行一些调整以获得正确的格式。

还有 数据类型 的问题。

BNNS 和 MPSCNN 都允许你将输入数据指定为浮点值(16 位和 32 位)或整数(8、16 或 32 位)。你想将浮点数据作为网络的输入,你可能无法选择输入数据的格式。

通常,当你加载 PNG 或 JPEG 图像,或者从手机相机中取出静止图像时,会得到一个 8Bit 纹理,该纹理使用无符号的 8 位整数作为像素的 RGBA 值。使用 MPSCNN 这是没有问题的:纹理会自动转换为浮点值。

用 BNNS 你可以指定 Int8 作为图像的数据类型,但是我实践后发现是行不通的。其实也许是因为我没有投入大量的时间来研究它。由于我要重新修改输入图像的通道,于是顺便就轻松地将像素数据转换为浮点数。

备注:即使 BNNS 允许你指定整数作为数据和权重的数据类型,它在内部也会将其转换为浮点数据,进行计算,然后将结果转换为整数。为了获得最好的速度,你可能想要跳过这个转换步骤,并且总是直接处理浮点数据,即使它们占用了 2 到 4 倍的内存。

临时数据

在 BNNS 和 MPSCNN 中,每个层级都需要处理。你将数据放入一个层级,并从一个层级中获取数据。

深层网络将会有很多层级。我们只关心最后一层的输出,而不关心所有其他层的输出。但是我们仍然需要将这些中间结果存储在某个地方,即使它们只用了一小会儿。

MPSCNN 对此有一个特殊的对象, MPSTemporaryImage 。它就像一个 MPSImage ,但只能使用一次。写入一次数据,读取一次数据。之后,它的内存将被回收。(如果您熟悉 Metal,它们是使用 Metal 的资源堆来实现的。)

你应该尽可能地使用 MPSTemporaryImage ,因为这样可以避免大量的内存分配和释放。

使用 BNNS 的话就得靠自己。你需要自己管理临时数据缓冲区。幸运的是,它非常简单:您可以分配一个或两个大数组,然后在这些层级之间重复使用它们。

多线程

你可能想要在后台线程中构建网络层级。加载学习参数的所有数据可能需要几秒钟的时间。

在后台线程上执行推断也是一个好主意。

使用足够深的神经网络,推断可能需要 0.1 到 0.5 秒之间的时间,这样的延迟对于用户是很明显的。

使用 MPSCNN 创建一个命令队列和一个命令缓冲区,然后通知所有层级编码到命令缓冲区中,最后将工作提交给 GPU。GPU 完成后,会通过回调通知你。

每项工作的编码都可以在后台线程中进行,你不需要做任何事情来进行同步。

备注:在实时情况下(例如,将摄像机的实时视频帧提供给神经网络时),你希望 GPU 保持繁忙状态,并且避免 CPU 和 GPU 相互等待的情况。当 GPU 仍在处理前一帧时,CPU 应该已经编码了下一个视频帧。你需要使用 MPSImage 对象数组,并通过信号量保证对它们的同步访问——但说实话,如果现在的移动设备能够实时地进行深度学习,我会感到非常惊讶。

BNNS 在 CPU 上工作,所以你可以在后台线程中开始工作,然后阻塞,直到 BNNS 完成。

最好让 BNNS 弄清楚如何在可用的 CPU 内核上分割工作,但是有一个配置选项告诉 BNNS 有多少线程可以用来执行计算。(MPSCNN 不需要这个,它将使用尽可能多的 GPU 线程。)

备注:你不应该在多个线程之间共享 MPSCNN 对象或 BNNS 对象。它们可以在单个后台线程中使用,但不能同时在使用多个线程中使用。

速度问题

决定是否使用 BNNS 或 MPSCNN 是基于一个权衡: CPU 数据更快还是 GPU 更快?

并非所有数据都适合 GPU 处理。图像或视频是非常合适的,但像时间序列数据可能不适合。

将数据加载到 GPU 中是需要花费的,因为你需要将其封装到 MTLTexture 对象中。一旦 GPU 完成,读取结果就需要再次从纹理对象中获取。

使用基于 CPU 的 BNNS,便不会有这些开销,但是你也无法利用 GPU 的大规模并行性来进行计算。

在实践中,开发人员可能会 尝试两种方法,看看哪一个更快 。但是,如上所示,由于 BNNS 和 MPSCNN 具有不同的 API,因此需要编写两次代码。

因为我很好奇,所以我决定分别使用 BNNS 和 MPSCNN 建立一个非常基本的卷积神经网络来测量哪一个更快。

我的神经网络设计大概是这样(点击图片放大):

The convolutional neural network used for the speed test The convolutional neural network used for the speed test

这种网络设计可以用来分类图像。网络采用 256×256 的 RGB 图像(无 alpha 通道)作为输入,并产生一个具有 100 个 浮点值 的数组。输出会表示出 100 多种可能类别的对象的概率分布。

实际上,神经网络需要有更多的层级才能真正有用。它最后也本该有一个 softmax 层,但是因为 BNNS 没有使用 softmax 函数,所以我把它去掉了。

我实际上并没有训练这个神经网络来学习任何有用的东西,而是用合理的随机值进行初始化。这是一个没有用的神经网络。然而,它确实允许我们比较在 BNNS 和 MPSCNN 中建立相同的神经网络所需要的内容,以及每个网络运行有多快。

如果你想一起实践, 这是 GitHub 上的代码 。在 Xcode 中打开这个项目,并在至少有一个 A8 处理器的 iOS 10 兼容设备上运行它(它不能在模拟器上运行)。

The speed test app The speed test app

点击按钮后,App 冻结几秒钟,同时在每个神经网络上执行 100 个独立的推断。该 App 显示了创建网络需要多长时间(并不是很有趣),以及需要多长时间才能完成 100 次重复的推断。

该 App 还打印出每个网络计算的结果。由于网络并没有进行训练,因此这些数字什么意义都没有,仅仅是用于调试目的。我想确保两个网络实际上计算的事情相同,从而保证测试是公平的。

答案中的小差异是由于浮点四舍五入(由于 Metal 在内部使用的 16 位浮点数,我们只得到 3 位小数的精度),而且也可能是由于每个框架具体执行计算的差异而产生的。但结果足够接近。

App 的工作原理

这个 App 创建的神经网络具有 2 个卷积层、1 个 max-pooling 层,1 个 average-pooling 层和 1 个全连接层。然后,它会测量向网络发送 100 次相同图像需要多长时间。

与此有关的主要源文件是 BNNSTest.swift MetalTest.swift

你猜对了, BNNSTest 类使用 BNNS 功能创建神经网络。以下是创建第一个卷积层所需的一小段代码:

inputImgDesc = BNNSImageStackDescriptor(width: 256, height: 256, channels: 3, 
                   row_stride: 256, image_stride: 256*256, 
                   data_type: dataType, data_scale: 0, data_bias: 0)

conv1imgDesc = BNNSImageStackDescriptor(width: 256, height: 256, channels: 16, 
                   row_stride: 256, image_stride: 256*256, 
                   data_type: dataType, data_scale: 0, data_bias: 0)

let relu = BNNSActivation(function: BNNSActivationFunctionRectifiedLinear, 
                          alpha: 0, beta: 0)

let conv1weightsData = BNNSLayerData(data: conv1weights, data_type: dataType, 
                           data_scale: 0, data_bias: 0, data_table: nil)

let conv1biasData = BNNSLayerData(data: conv1bias, data_type: dataType, 
                        data_scale: 0, data_bias: 0, data_table: nil)

var conv1desc = BNNSConvolutionLayerParameters(x_stride: 1, y_stride: 1, 
                    x_padding: 2, y_padding: 2, k_width: 5, k_height: 5, 
                    in_channels: 3, out_channels: 16, 
                    weights: conv1weightsData, bias: conv1biasData, 
                    activation: relu)

conv1 = BNNSFilterCreateConvolutionLayer(&inputImgDesc, &conv1imgDesc, 
                                         &conv1desc, &filterParams)

使用 BNNS,你需要创建大量“描述符”助手来描述你将要使用的数据以及层级的属性和权重。其他层级也会重复此操作。现在你可以明白为什么我之前说这个会很无聊。

MetalTest 类使用 MPSCNN 做同样的事情:

conv1imgDesc = MPSImageDescriptor(channelFormat: channelFormat, width: 256, 
                                  height: 256, featureChannels: 16)

let relu = MPSCNNNeuronReLU(device: device, a: 0)

let conv1desc = MPSCNNConvolutionDescriptor(kernelWidth: 5, kernelHeight: 5, 
                    inputFeatureChannels: 3, outputFeatureChannels: 16, 
                    neuronFilter: relu)

conv1 = MPSCNNConvolution(device: device, convolutionDescriptor: conv1desc, 
            kernelWeights: conv1weights, biasTerms: conv1bias, flags: .none)

在这里你也可以创建各种描述符对象,但代码会短一些。







请到「今天看啥」查看全文