1. 目录

  • 引言
    • 简单问题定义
    • 更复杂的问题
    • 机器学习的思路
  • 神经元
  • 数据集
  • 损失函数
  • 梯度下降算法
    • 算法过程
    • 为什么梯度下降算法有用?
  • 代码实现

2. 引言

随着人工智能技术的发展,在互联网上常听到的卷积神经网络(CNN)、循环神经网络(RNN)、图神经网络(GCN),以及目前最新的GPT,无论是哪一种,都是以神经网络(NN)为基础改进的变种。因此,非常有必要对神经网络这一基础概念进行介绍。

2.1 简单问题定义

本文设计了一个非常简单的问题,然后尝试通过神经网络解决这个问题,希望这个过程中把神经网络的核心概念讲清楚。

问题具体定义如为:给出一个学生成绩信息表,字段分别为学习时间和成绩。根据表格的数据规律,设计一个程序,输入学习时间后,给出预测成绩

下面这张表是对某班级的学习时间和学习成绩统计,思考一下,如果输入为多少?时,又为多少?

ID (小时) (成绩)
001 1 2
002 2 4
003 3 6
004 4 8
005 5 10
006 6 12
007 7 14
008 8 16

事实上,由于这个数据表较为简单,基本无需思考就能得出规律,并归纳成公式:

2.2 更复杂的问题

但是,如果数据表没有这么简单,还能这么快找到规律吗?

如下图是一个房价数据表[2],有81个字段,好几千条数据。假如现在让你写一个程序,输入为前80个字段的值,输出为房价的预测值,你还能看出数据表中数据的规律,并得出房价和其他80个字段的函数关系吗?恐怕会有些不现实。


以下图为例,如果数据结构更复杂一些,数据表的字段是图像,和图像对应的类型(鱼、飞机、自行车...)。你能找到图片像素与其类型间的规律,并归纳成一个数学公式,以便未来作预测吗?恐怕更加不现实,因为实在是太复杂了。


2.3 机器学习的思路

复杂问题的内在规律也更加复杂,因此我们放弃了传统手工去总结数据规律的方式,转而利用计算机强大的计算能力,去海量数据中挖掘规律。也可以理解为让计算机去归纳出一个函数,尽可能逼近实际应用问题的函数。挖掘数据规律的算法有很多种,耳熟能详的有随机森林算法、SVR、K最近邻算法、Adaboost、神经网络等。

事实上,目前常说的深度学习就是基于神经网络的技术,广泛应用在各个领域,可以说现在日常看到或听说的人工智能都是指基于神经网络的技术。其他几个算法也有一定的出场率,但相对来说会少一些,对这方面的研究热度也会低一些,一般称他们为传统的机器学习算法。

3. 神经元

神经元是神经网络的基本单元,不同神经元结合可以组成一个复杂的神经网络。现代神经网络,往往结构更加复杂,且神经元数量级巨大。但万变不离其宗,只要对一个基本的神经元进行研究,就可以理解神经网络的最核心的本质。

下面是一个基本神经元的常规定义图,结构非常简单。


其中,上图神经元结构用公式表达为:

  • 代表输入神经元的变量。对于房价预测问题,它可以是房子的面积、颜色、层数等。对于图片分类问题,它可以代表一张图片第个像素值。
  • 代表权重值,这个值一开始是随机或设定某一常数,通过学习和训练,让机器自动寻找最合适的值。一般也称它为可训练参数(Trainable parameters)。
  • 也是可训练参数,但是它在一个神经元中只有一个。
  • 代表把和其对应的权重相乘并累加。
  • 为激活函数,常见的有等。可以先简单理解为为线性函数,函数为直线形态,用了激活函数后可以让它转弯,拥有更好的表达力。

回到我们一开始提到的成绩预测问题。我们尝试用一个基本神经元去解决这个问题。为了更加简化,我们先把激活函数和偏置省略,且该问题只有学习时间一个输入,因此求和也可以省略,最后这个简化的神经元结构如下图所示。


其表达式为:

之前提到过,由于数据较为简单,我们猜到了应该为2。但这显然不是机器学习的思维,我们要想办法让计算机自动去找的值。

4. 数据集

先来回忆一下人类的学习模式:不断地做练习题,总结规律,然后参加考试。神经元的学习方式也类似,通过不断地做练习题,总结规律并调整自己的,最后参加考试,看神经元是否真的从练习题中学到了东西。

在机器学习领域,每一道题目叫「样本」,所有练习题被称为「训练集」,而考试的所有题目被称为「测试集」,所有题目放一起被称为「数据集」。

在每一个样本中,作为输入到神经元的字段叫「特征」,期望神经元预测的字段叫「标签」。下面举例:

  • 在成绩预测问题中,学习时间是特征,成绩是标签。
  • 在房价预测问题中,房子面积、位置、层数是特征,房价是标签。

以成绩预测问题为例子,数据表是一个有8个样本的数据集。我们可以把它以3:1比例划分为训练集和测试集。我们取前6条作为训练集,它们分别为:

ID (小时) (成绩)
001 1 2
002 2 4
003 3 6
004 4 8
005 5 10
006 6 12
ID (小时) (成绩)
007 7 14
008 8 16

5. 损失函数

有了数据集后,神经元也就有了学习资料。接下去,还有一个问题就是如何评判神经元的表现好坏。我们前面提到过,是由机器自己去调整的。假如机器的调整算法是让取随机数,那么调整3次的过程可能如下:

神经元表达式
6
1
100

我们以ID为003的样本为例,将分别代入到上述3个神经元中,其预测输出与样本真实值分别为:

神经元表达式 (误差)
18 6 12
3 6 3
300 6 294

其中(误差)我们先简单定义为样本预测值与真实值之间差的绝对值,表达式如下:

通过上表可以看出,第三个神经元误差最大,第二个神经元误差最小,因此可以认为第二个神经元是效果最好的,我们也希望计算机能够调整出误差更小的神经元。

但是,上述都是仅针对ID为003的这条单一的样本,我们更期望得到神经元在整个训练集上的综合评价,故将公式变为:

其中代表样本总数, 代表第条样本的误差值。该公式为所有样本误差的均值,称为平均绝对误差(Mean absolute error,MAE),我们一般称这样的函数为损失函数(Loss function)

损失函数有很多种,更为常用的损失函数是均方差损失(Mean Squared Error,MSE),本文也选它作为评价神经元表现好坏的损失函数,其定义为:

6. 梯度下降算法

在上一节中,我们详细介绍了损失函数。这一节的目标是寻找一种神经元的调整算法,使其在训练集上的损失值尽可能小。

目前我们的神经元里可调的参数只有,如果一拍脑袋,我们可以想到如下几种方法:

  • 每次让随机,取损失值最小的一个神经元。
  • 每次让自增一个常数,取损失值最小的一个神经。
  • ...其他不靠谱的方案

上述方法,其实都是穷举法。在如此简单场景下或许可行,问题稍微复杂一些就直接失效,无轮是算法效率还是在可行性上。

6.1 算法过程

下面介绍目前所有主流神经网络的参数调整算法:梯度下降算法。我个人认为,这是深度学习中最核心最奇妙的算法。它的步骤为:

  • 定义损失函数,它是一个关于的函数。
  • 计算损失函数的导数
    • 为学习率,是一个正数,用于控制参数调整的步长。
    • 注:在成绩预测的例子中,在这一步更新。

重复上述过程轮,一般可使损失函数的值越来越小,直到收敛为一个较小的常数。接着,我们保留作为神经元的最终参数,以用于实际预测。

这里还是稍有抽象,为了更加具体,我们用梯度下降算法继续解决成绩预测问题,加深对该算法的理解。

在上一节中我们已定好损失函数,因此直接跳到第二步,计算损失函数的导数,具体过程为:

其中,代表第个样本特征(学习时间),代表第个样本的标签(成绩),在损失函数中它们是常数。求得导数后,我们便得到了参数的调整公式:

6.2 为什么梯度下降算法有用?

为了方便理解,假设损失函数为平方函数:

那么它的导数为:

它的函数图像如下:


  • 时,递增;取任意点,显然,推出
    • 结合上图可以看出,时, 使损失函数往更小的方向走。
  • 时,递减;取任意点,显然,推出
    • 结合上图可以看出,时, 使损失函数往更小的方向走。

上述例子可能不够严谨,不过可以帮助理解为什么梯度下降算法能让神经元往损失函数值更小的方向演变。

7. 代码实现

对于成绩预测这个问题,本文并未用任何深度学习框架,用Swift实现了一个简单的神经元,及其训练的代码,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import Foundation

let x_data = [1.0,2.0,3.0,4.0,5.0,6.0] // 特征
let y_labels = [2.0,4.6,6.0,8.0,10.0,12.0] // 标签

// 初始化w
var w = 10.0

// 神经元的表达式
func forward(x: Double) -> Double {
return x * w
}

// Loss函数
func loss(x_data: [Double], y_labels: [Double]) -> Double {
var totalLoss = 0.0
for (x,y) in zip(x_data,y_labels) {
let y_predict = forward(x: x)
totalLoss = totalLoss + pow((y_predict - y),2)
}
return totalLoss / Double(x_data.count)
}

// Loss函数的导函数
func gradient(x_data: [Double], y_labels: [Double]) -> Double {
var grad = 0.0
for (x,y) in zip(x_data, y_labels) {
grad = 2.0 * x * (x * w - y)
}
return grad / Double(y_labels.count)
}

// 训练神经元
func train() {
let epoch_num = 50 // 训练次数
let alpha = 0.01 // 学习率
for epoch in 0..<epoch_num {
let loss_v = loss(x_data: x_data, y_labels: y_labels)
let grad_v = gradient(x_data: x_data, y_labels: y_labels)
w = w - alpha * grad_v //调整w参数,往Loss小的方向走
print("Epoch:\(epoch),w=\(w),loss=\(loss_v)")
}
}

train()

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Epoch:0,w=9.04,loss=967.5266666666666
Epoch:1,w=8.1952,loss=748.9282666666664
Epoch:2,w=7.451776,loss=579.6862161066666
Epoch:3,w=6.79756288,loss=448.66085650500264
Epoch:4,w=6.2218553344,loss=347.226220259234
Epoch:5,w=5.715232694272,loss=268.70287191269966
Epoch:6,w=5.26940477095936,loss=207.91870883986962
Epoch:7,w=4.877076198444237,loss=160.86885269658896
Epoch:8,w=4.531827054630928,loss=124.45227587071315
Epoch:9,w=4.228007808075216,loss=96.26785073565797
Epoch:10,w=3.9606468711061904,loss=74.45641523490589
Epoch:11,w=3.7253692465734476,loss=57.578472908098036
Epoch:12,w=3.5183249369846337,loss=44.51948769619559
Epoch:13,w=3.3361259445464775,loss=34.416547674958586
Epoch:14,w=3.1757908312009,loss=26.601576474149695
Epoch:15,w=3.0346959314567923,loss=20.557358861683884
Epoch:16,w=2.910532419681977,loss=15.883489297778075
Epoch:17,w=2.80126852932014,loss=12.2700045560546
Epoch:18,w=2.7051163058017234,loss=9.476966638801322
Epoch:19,w=2.6205023491055166,loss=7.318653382409264
Epoch:20,w=2.5460420672128548,loss=5.651317066580671
Epoch:21,w=2.480517019147312,loss=4.363705917133857
Epoch:22,w=2.4228549768496346,loss=3.3697250453093854
Epoch:23,w=2.3721123796276786,loss=2.602754036198805
Epoch:24,w=2.327458894072357,loss=2.0112473314102286
Epoch:25,w=2.2881638267836744,loss=1.555327906528606
Epoch:26,w=2.2535841675696333,loss=1.2041500671301375
Epoch:27,w=2.223154067461277,loss=0.9338577719422357
Epoch:28,w=2.196375579365924,loss=0.7260040633539245
Epoch:29,w=2.172810509842013,loss=0.5663275188517147
Epoch:30,w=2.1520732486609715,loss=0.44380512612635187
Epoch:31,w=2.133824458821655,loss=0.3499191737365202
Epoch:32,w=2.1177655237630564,loss=0.27808983411812216
Epoch:33,w=2.1036336609114894,loss=0.22323602240044685
Epoch:34,w=2.091197621602111,loss=0.18143556002315395
Epoch:35,w=2.080253907009858,loss=0.1496622118450294
Epoch:36,w=2.070623438168675,loss=0.12558222931631807
Epoch:37,w=2.062148625588434,loss=0.10739695335046053
Epoch:38,w=2.0546907905178218,loss=0.09372106664635203
Epoch:39,w=2.048127895655683,loss=0.08348843606607916
Epoch:40,w=2.042352548177001,loss=0.0758793058980987
Epoch:41,w=2.0372702423957607,loss=0.0702640121749914
Epoch:42,w=2.0327978133082696,loss=0.06615947939331678
Epoch:43,w=2.0288620757112774,loss=0.06319560580338775
Epoch:44,w=2.025398626625924,loss=0.0610892975000022
Epoch:45,w=2.022350791430813,loss=0.05962441790595736
Epoch:46,w=2.0196686964591155,loss=0.05863631123769426
Epoch:47,w=2.0173084528840217,loss=0.05799986199243287
Epoch:48,w=2.0152314385379393,loss=0.057620287388506976
Epoch:49,w=2.0134036659133865,loss=0.057426041503838704

可以看出,经过50次训练后,loss函数值一直在下降,最终趋近于0;神经元的参数趋近于2,其最终表达式为:

与我们设想的一致。这说明,我们对神经元结构的设计,以及后续训练过程中涉及的损失函数、梯度下降算法,对成绩预测问题是有效的。

现实中的问题往往较为复杂,单一神经元难以满足,因此会选用更为复杂的结构,典型的是卷积神经网络、循环神经网络等。但要论其核心思想,以及训练过程,那几乎与本文这个简单的神经元无差,故本文可作为这些复杂网络的前置知识学习。

参考资料

[1] 《PyTorch深度学习实践》完结合集

[2] House Prices - Advanced Regression Techniques