关于神经网络

⚠️ 本文尚未完全完成

在本文中,我将分享一下我近期学习到的神经网络相关
同时,使用 Swift 实现一个简单的神经网络,并予以测试

神经网络

什么是神经网络

一般意思上的神经网络指人工神经网络(Artificial Neural Network)

如何简单形象又有趣地讲解神经网络是什么?
How do you explain Machine Learning and Data Mining to non Computer Science people?
wikipedia

很直观的演示

Tensorflow
Swift-AI(需要有 Mac/Xcode 环境)

神经网络的基本样式

FFNN

上图是一个最简单的多层神经网络,也是接下来我将要在本文中实现的神经网络

我理解的神经网络

给定若干个数据(inputs),以及每个数据所代表的意义(answers),已知每个数据的值和每个数据代表的意义间有某种复杂的(难以使用传统数学方法得知)函数关系

现在有该类:

1
2
3
4
class FFNN {
func train(inputs:[Float], outputs:[Float])
func calculate(input:[Float]) -> [Float]
}

我现在使用足够多的 inputs 和对应的 answers 不断训练这个 FFNN,即同时告知输入和输出:

1
2
3
4
let ffnn = FFNN()
for item in SAMPLE_DATAS {
ffnn.train(item.input, item.answer)
}

逐渐的,这个 ffnn 就可以学习/模仿/估算(更多描述为拟合)到若干个数据,以及每个数据所代表的意义中隐含的函数关系
当我充分训练了这个 ffnn
那么假设现在我有一个数据(input),我们不知道它所代表的意义(answer)
但是之前训练过的 ffnn 差不多能猜出来:

1
2
3
4
5
let result = ffnn.calculate(input)
if result.diff(input.true_result) < 0.05 {
print("✅ 八九不离十!")
}

如果我们训练完了 ffnn 后,用一些已知答案的数据做测试,发现 ffnn 计算出来的答案和真实答案的误差已经很小了,那么我们就可以认为对 ffnn 的训练成功了,或者说,ffnn.train 这个黑盒子已经『猜透』了数据中某种复杂的(难以使用传统数学方法得知)函数关系

使用 Swift 实现一个 FFNN

实现

首先附上完整项目链接

这份代码极大程度上参考了Swift-AI

下面我会对代码中的一些关键部分做一下讲解

数据初始化

初始化输入层、隐藏层、输出层神经元的数量、层级之间的权重、计算所需的缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
class FFNN {
// 输入层神经元数量
let inputCount: Int
// 隐藏层神经元数量
let hiddenCount: Int
// 输出层神经元数量
let outputCount: Int
// 输入层 - 隐藏层权重
var inputToHiddenWeight: [Float]
// 隐藏层 - 输出层权重
var hiddenToOutputWeight: [Float]
...
}

激励函数

关于激励函数的选择,建议大家看看这里:请问人工神经网络中的activation function的作用具体是什么?为什么ReLu要好过于tanh和sigmoid function? - 知乎用户的回答 - 知乎
我在这个例子中使用的是 sigmoid 函数:

1
2
3
func sigmoid(x: Float) -> Float {
return 1 / (1 + exp(-x))
}

求解

⚠️ 凭什么拟合?

i 根据 inputToHiddenWeighthiddenToOutputWeight 以及 activation function 计算输出的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func update(i: [Float]) -> [Float] {
// 保证输入 = 输入层神经元
assert(i.count == inputCount)
// 为输入层神经元加入偏置
inputCache = [1.0] + i
// 矩阵相乘
mul(A: inputToHiddenWeight, B: inputCache, C: &hiddenToOutputCache, M: hiddenCount, N: 1, P: inputWithBiasCount)
// 激励
hiddenToOutputCache = hiddenToOutputCache.map(sigmoid)
// 矩阵相乘
mul(A: hiddenToOutputWeight, B: hiddenToOutputCache, C: &outputCache, M: outputCount, N: 1, P: hiddenWithBiasCount)
// 激励
outputCache = outputCache.map(sigmoid)
// 返回结果
return outputCache
}

反向传播

关于如何计算权值,非常推荐大家先看看这个问题:如何直观的解释back propagation算法? - Evan Hoo 的回答 - 知乎

下面是一些我认为对我理解 BP 算法帮助比较大的话:

  1. 机器学习可以看做是数理统计的一个应用,在数理统计中一个常见的任务就是拟合,也就是给定一些样本点,用合适的曲线揭示这些样本点随着自变量的变化关系
  2. 采用我们常用的梯度下降法就可以有效的求解最小化 cost 函数的问题
  3. BP算法正是用来求解这种多层复合函数的所有变量的偏导数的利器

十分抱歉,这里的 backpropagation 写的不够简洁明了,我就不多讲了,仅仅说明一下这个 function 调用后,对 FFNN. inputToHiddenWeightFFNN. hiddenToOutputWeight 的影响吧

  • update 后调用
  • 修改 weight
  • 通过调整各个神经元链接间的 weight ,是拟合出来和函数越来越接近目标函数
1
2
3
func backpropagation(answer: [Float]) {
...
}

测试

测试这一小节的数据/数据隐含的关系都是我 YY 的,仅仅是为了方便大家理解,如有雷同,纯属巧合
关于 FFNN 在这里的使用方式,我也是 YY 的,不具有指导作用,请大家仅作参考

需求

假设现在有一家公司要要针对自己的移动端用户做一个结识妹子的活动,该活动在移动端的入口为首页的某个可点击区域,该公司运营希望这个活动能更有针对性的推广,也就是筛选出来可能会对这个活动更感兴趣的用户,在这些用户使用 App 时,在移动应用首页向其推广活动

假设现在我们通过某些方式(如灰度发布)得到了一些用户数据:

数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Item {
/// 性别
/// 0 女性 1 男性
let male: Float
/// 婚恋状况
/// 0 非单身 1 未知 2 单身
let single: Float
/// 年龄
/// 15 ~ 55
let age: Float
/// 点击热度/感兴趣程度
/// 0 ~ 1000
let frequency: Float
}

创建数据

在这里我假设 frequencyage 服从正态分布的关系,同时和性别于年龄有关

这里使用正态分布可能并不是太恰当,因为用户对活动的喜好程度可能并不是一个概率分布问题

通俗一点的讲,我创建的这些数据中,对结识妹子活动最感兴趣的是年龄为 25 岁的男性单身用户(item.age = 25, item.single = 2, item.male = 1),25岁之前,对活动的兴趣逐渐升高,25岁之后,对活动的兴趣逐渐降低,这个关系大概服从某个类似正态分布的关系。性别为女的话,对结识妹子活动的兴趣为男性用户的一半,确定非单身的用户对活动兴趣为单身用户的 1/3,婚恋情况未知取中间值 2/3。

1
2
3
4
5
6
7
8
9
let CustomNormalDistribution = NormalDistribution(μ: 0, σ: 1)
class DataGenerator {
class func generateItems(count:Int) -> [Item] {
...
frequency = CustomNormalDistribution(age) * (male == 1 ? 1 : 0.5) * (single == 1 ? 0.66 : (single == 2 ? 1 : 0.33))
...
}
}

训练

而我们的 FFNN 就是要通过大量的“学习”,来拟合上面我假想的这个函数关系

1
2
3
4
5
6
7
8
9
10
11
12
13
let ffnn = FFNN(inputCount: 3, hiddenCount: 5, outputCount: 1)
let data = DataGenerator.generateItems(10000)
for (index, item) in data.enumerate() {
ffnn.update([item.male, item.single, item.age])
ffnn.backpropagate([item.frequency])
if index % 10 == 0 {
let testData = DataGenerator.getPerson()
let result = ffnn.update([testData.male, testData.single, testData.age])
let answer = testData.frequency
print(result[0] - answer)
}
}

应用

FFNN 训练好了之后,我们就可以将这个 ffnn 应用在当前的这个结识妹子的模型上了

这时我们可以认为 ffnn 已经能根据某个用户的信息(性别,年龄,婚恋状况),较准确的分析出该用户对这个活动的兴趣

这样,我们就可以根据兴趣的多少,来针对每位用户改变首页中该活动的展示方式,让这个活动能更有针对性的推广,让首页寸土寸金的空间为创造更多的流量上文提到的需求

再次附上完整项目链接

总结

Matrix

上面的代码中,hiddenWeightsoutputWeights 可能因为 Apple 的 Accelerate 库的 API 的原因,使用的是一维数组。但是,权重的最佳表现和计算方式应该是矩阵(Matrix)。Surge这个库是个矩阵的 Swift 实现,大家可以使用 git submodule add 来集成并使用,估计 backpropagation 的代码能变简洁很多😂

业界

实际上关于神经网络业界成熟的轮子已经有很多了,目前比较火的就是 Google 的 TensorFlow

关于如何使用基于 TensorFlow 的 ANN,大家可以看一下这里:TensorFlow Mechanics 101,或者中文版

接下来的一段时间里,我也将以 TensorFlow 为辅助工具,继续学习 CNN、RNN 等其他神经网络模型,有时间给大家分享 ~

相关链接