BNNS实现神经网络

sdq
Explore, Think, Create
13 min readSep 28, 2016

iOS10和macOS10.12支持了BNNS框架,你可以在自己的app里加入神经网络。BNNS在CPU上运行并且对速度做了优化。同时,BNNS也有一个基于Metal框架的GPU版本(Metal框架的一部分)这篇文章主要写了如何通过BNNS来组建一个基本的神经网络。

我们来组建一个可以计算XOR方法的神经网络,这张图表是XOR的基本计算。

简单来说其实就是当两个输入一个为0一个为1的时候,输出结果为1,其余则输出0. 在大多数编程语言中,可以通过以下代码实现XOR:

a = 1
b = 0
c = a ^ b // 结果: 1

我们最后完成的神经网络大致是如下的样子。一个基本的神经网络包含三层:输入层、隐含层和输出层。每一层都包含了神经元。输入层里是你的两个输入 in1in2 。这两个输入都连接着隐含层的两个神经元 h1h2 。隐含层会进行一些计算处理,然后将结果传给输出层 out。最后输出层计算出结果是0或者1。

可以看到输入层的神经元其实不做任何事情,仅仅是负责获取输入值然后连接隐含层。所有的计算都是在隐含层和输出层进行。

这里的例子我们采用全连接层(每层之间所有神经元相互连接),而BNNS其实支持卷积神经网络层和池化层,可以通过它们实现更酷的深度学习。但我们在这个例子里尽量保持简单。

神经元之间的连接都具有一个权值,正是这些权值构成了神经网络的大脑:图中这些特定的权值形成了XOR方法。如果你使用不一样的数字,可能就不能实现XOR的效果。此外,那些额外加入的数字称为偏置值。

隐含层的神经元进行了如下的计算:

h1 = sigmoid(in1 * w1 + in2 * w2 + b1)
h2 = sigmoid(in1 * w3 + in2 * w4 + b2)

其中w1,w2,w3,w4是权值,b1,b2是偏置值。我们把参数全部代入上式,可以得到:

h1 = sigmoid(in1 * 54 + in2 * 17 - 8)
h2 = sigmoid(in1 * 14 + in2 * 14 - 20)

sigmoid()是一个数学方法,具体代码是这个样子的:

func sigmoid(x) {
return 1 / (1 + exp(-x))
}

这个方法其实是神经网络中使用的激活函数,其实类似的激活函数有很多种,BNNS支持了大部分通用的。不过激活函数本身是什么其实并不重要,它的目的只是将线性的in1 * w1 + in2 * w2 + b1转为非线性的结果。这也是神经网络的关键所在,没有它我们也无法实现XOR方法。sigmoid()的图形类似S的形状,这也是名字的由来(sigma是S的希腊文)

你可以发现,通过这个方法可以把任意的x输入,转换到0到1中间的值。因为我们需要处理的是二进制问题,所以这个也是我们所期待的结果。我们需要输出比较漂亮的0或1,所以希望尽量能使in1 * w1 + in2 * w2 + b1是一个绝对较大值(大于+5或者小于–5)。如果x太靠近0,那输出的结果会在0到1中间。

我们来试一下现在的这个神经网络。当输入值 in1in2 全为0的时候,隐含层 h1h2 也基本是0。

h1 = sigmoid(0 * 54 + 0 * 17 - 8)  = sigmoid(-8)  = 0.000335
h2 = sigmoid(0 * 14 + 0 * 14 - 20) = sigmoid(-20) = 0.000000

当输入值 in1 = 0in2 = 1 时,隐含层的 h1 趋近于1而 h2 趋近于0.这里要注意,这两个值永远不会完全等于0或1,只是无限逼近它们。

h1 = sigmoid(0 * 54 + 1 * 17 - 8)  = sigmoid(9)  = 0.999876
h2 = sigmoid(0 * 14 + 1 * 14 - 20) = sigmoid(-6) = 0.002472

这里可以计算出 h1h2 与输入值的对应关系为:

h1h2 与输出层连接,输出层神经元的计算公式如下:

out = sigmoid(h1 * 92 + h2 * -98 - 48)

理论部分到这里就结束了,现在让我们来看看代码。

首先,我们需要引入必要的库,然后定义两个BNNSFilter对象

#include <Accelerate/Accelerate.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

BNNSFilter hidden_layer;
BNNSFilter output_layer;

这里注意,BNNS把神经网络中的层(layer)称为filter。你可以把它们作为每个神经网络层之间的过滤器:

其实这个和之前展示的图基本相同只是有细微变化。不同点在于所有的计算不再是由神经元进行了,而是全部由过滤器完成。这些过滤器由create_network()生成。下面是这个函数的第一部分,其中BNNSActivation对象用来表示激活函数,同时我们也定义了权值和偏置值。

bool create_network(void) {
BNNSFilterParameters filter_params;
bzero(&filter_params, sizeof(filter_params));

BNNSActivation activation;
bzero(&activation, sizeof(activation));
activation.function = BNNSActivationFunctionSigmoid;

float input_to_hidden_weights[] = { 54.0f, 14.0f, 17.0f, 14.0f };
float input_to_hidden_bias[] = { -8.0f, -20.0f };
float hidden_to_output_weights[] = { 92.0f, -98.0f };
float hidden_to_output_bias[] = { -48.0f };

接下来需要定义的是过滤器,基本都是使用了创建过滤器的模板。其中尤其重要的是in_size和out_size,分别代表输入与输出的数量。

    BNNSFullyConnectedLayerParameters input_to_hidden_params;
bzero(&input_to_hidden_params, sizeof(input_to_hidden_params));
input_to_hidden_params.in_size = 2;
input_to_hidden_params.out_size = 2;
input_to_hidden_params.activation = activation;
input_to_hidden_params.weights.data = input_to_hidden_weights;
input_to_hidden_params.weights.data_type = BNNSDataTypeFloat32;
input_to_hidden_params.bias.data = input_to_hidden_bias;
input_to_hidden_params.bias.data_type = BNNSDataTypeFloat32;

BNNSFullyConnectedLayerParameters hidden_to_output_params;
bzero(&hidden_to_output_params, sizeof(hidden_to_output_params));
hidden_to_output_params.in_size = 2;
hidden_to_output_params.out_size = 1;
hidden_to_output_params.activation = activation;
hidden_to_output_params.weights.data = hidden_to_output_weights;
hidden_to_output_params.weights.data_type = BNNSDataTypeFloat32;
hidden_to_output_params.bias.data = hidden_to_output_bias;
hidden_to_output_params.bias.data_type = BNNSDataTypeFloat32;

然后我们开始分别创建第一和第二个过滤器。

    BNNSVectorDescriptor input_desc;
bzero(&input_desc, sizeof(input_desc));
input_desc.size = 2;
input_desc.data_type = BNNSDataTypeFloat32;

BNNSVectorDescriptor hidden_desc;
bzero(&hidden_desc, sizeof(hidden_desc));
hidden_desc.size = 2;
hidden_desc.data_type = BNNSDataTypeFloat32;

hidden_layer = BNNSFilterCreateFullyConnectedLayer(&input_desc,
&hidden_desc, &input_to_hidden_params, &filter_params);
if (hidden_layer == NULL) {
fprintf(stderr, "BNNSFilterCreateFullyConnectedLayer failed\n");
return false;
}

BNNSVectorDescriptor output_desc;
bzero(&output_desc, sizeof(output_desc));
output_desc.size = 1;
output_desc.data_type = BNNSDataTypeFloat32;

output_layer = BNNSFilterCreateFullyConnectedLayer(&hidden_desc,
&output_desc, &hidden_to_output_params, &filter_params);
if (output_layer == NULL) {
fprintf(stderr, "BNNSFilterCreateFullyConnectedLayer failed\n");
return false;
}

return true;
}

对于一个如此简单的神经网络,create_network()函数依然比较庞大,主要原因在于你需要详细描述神经网络中所有的具体参数。

一旦完成了神经网络的创建,现在要做的就是进行推断了。所谓推断,就是对任何输入值进行预测。预测方法如下,相对比较简单。我们把输入值放入输入层中,然后分别用BNNSFilterApply()求出隐含层与输出层。

float predict(float in1, float in2) {
float input[] = { in1, in2 };
float hidden[] = { 0.0f, 0.0f };
float output[] = { 0.0f };

int status = BNNSFilterApply(hidden_layer, input, hidden);
if (status != 0) {
fprintf(stderr, "BNNSFilterApply failed on hidden_layer\n");
}

status = BNNSFilterApply(output_layer, hidden, output);
if (status != 0) {
fprintf(stderr, "BNNSFilterApply failed on output_layer\n");
}

printf("Predict %f, %f = %f\n", a, b, output[0]);
return output[0];
}

最后,来测试一下整个神经网络。

int main(int argc, const char * argv[]) {
if (create_network()) {
printf("Making predictions for XOR gate:\n");

predict(0, 0);
predict(0, 1);
predict(1, 0);
predict(1, 1);

destroy_network();
}
return 0;
}

void destroy_network(void) {
BNNSFilterDestroy(hidden_layer);
BNNSFilterDestroy(output_layer);
}

输出结果如下所示,完美实现了XOR的方法。

Making predictions for XOR gate:
Predict 0.000000, 0.000000 = 0.000000
Predict 0.000000, 1.000000 = 1.000000
Predict 1.000000, 0.000000 = 1.000000
Predict 1.000000, 1.000000 = 0.000000
Program ended with exit code: 0

到这里整个示例就结束了。你可能会感到好奇,其中那些神奇的权值是怎么获得的。其实这些值是通过训练得到的。训练并不在本文的讨论范围内,因为目前BNNS并不支持训练,需要通过独立的训练系统去获得权值。训练神经网络的基本概念如下:

  1. 使用一些较小的随机值来初始化权值,把偏置值全部置为0。
  2. 通过前向传递计算输入值产生的输出值。通常一开始的输出值会是错误的。
  3. 计算出差错的程度,或者说是误差值。
  4. 这个时候把误差值进行反向传递,调整权值,使得下一次计算相对准确。BNNS不支持反向传递,这也是为什么我们无法训练的原因。
  5. 返回第二步,重复几千次。每一次迭代,神经网络都会更准确一些。

最终,我们通过神经网络实现了XOR函数。这里可以用一个图来展示这个神经网络的决策边界。

你可以在这里获取到源代码。

本文翻译自http://matthijshollemans.com/2016/08/24/neural-network-hello-world

--

--