作者 | @Aloys (腾讯员工,后台工程师)
本文授权转自腾讯的知乎专栏
▌
一. 前言:
作为AI入门小白,参考了一些文章,想记点笔记加深印象,发出来是给有需求的童鞋学习共勉,大神轻拍!
【毒鸡汤】:算法这东西,读完之后的状态多半是 --> “我是谁,我在哪?” 没事的,吭哧吭哧学总能学会,毕竟还有千千万万个算法等着你。
本文货很干,堪比沙哈拉大沙漠,自己挑的文章,含着泪也要读完!
▌
二. 科普:
▌
三. 通往沙漠的入口: 神经元是什么,有什么用:
开始前,需要搞清楚一个很重要的问题:人工神经网络里的神经元是什么,有什么用。只有弄清楚这个问题,你才知道你在哪里,在做什么,要往哪里去。
首先,回顾一下神经元的结构,看下图, 我们先忽略激活函数不管:
没错,开始晒公式了!我们的数据都是离散的,为了看得更清楚点,所以换个表达方式,把离散的数据写成向量。该不会忘了向量是啥吧?回头致电问候一下当年的体育老师!
现在回答问题刚才的问题:
先睹为快,看效果图,自己可以去玩:传送门
http://t.cn/RBCoWof
对上面的图简单说明一下:
又要划重点了:
我们需要对神经元的输出做判定,那么就需要有判定规则,通过判定规则后我们才能拿到我们想要的结果,这个规则就是:
-
假设,0代表红点,1代表蓝点(这些数据都是事先标定好的,在监督学习下,神经元会知道点是什么颜色并以这个已知结果作为标杆进行学习)
-
当神经元输出小于等于 0 时,最终结果输出为 0,这是个红点
-
当神经元输出大于 1 时,最终结果输出为 1,这是个蓝点
上面提到的规则让我闻到了激活函数的味道!(这里只是线性场景,虽然不合适,但是简单起见,使用了单位阶跃函数来描述激活函数的功能)当 x<=0 时,y = 0; 当 x > 0 时,y = 1
这是阶跃函数的长相:
此时神经元的长相:
▌
四. 茫茫大漠第一步: 激活函数是什么,有什么用
从上面的例子,其实已经说明了激活函数的作用;但是,我们通常面临的问题,不是简单的线性问题,不能用单位阶跃函数作为激活函数,原因是:
阶跃函数在x=0时不连续,即不可导,在非0处导数为0。用人话说就是它具备输出限定在[0-1],但是它不具备丝滑的特性,这个特性很重要。并且在非0处导数为0,也就是硬饱和,压根儿就没梯度可言,梯度也很重要,梯度意味着在神经元传播间是有反应的,而不是“死”了的。
接下来说明下,激活函数所具备的特性有什么,只挑重要的几点特性讲:
-
非饱和性
:饱和就是指,当输入比较大的时候,输出几乎没变化了,那么会导致梯度消失!什么是梯度消失:就是你天天给女生送花,一开始妹纸还惊喜,到后来直接麻木没反应了。梯度消失带来的负面影响就是会限制了神经网络表达能力,词穷的感觉你有过么。sigmod,tanh函数都是软饱和的,阶跃函数是硬饱和。软是指输入趋于无穷大的时候输出无限接近上线,硬是指像阶跃函数那样,输入非0输出就已经始终都是上限值。数学表示我就懒得写了,传送门在此(https://www.cnblogs.com/rgvb178/p/6055213.html),里面有写到。如果激活函数是饱和的,带来的缺陷就是系统迭代更新变慢,系统收敛就慢,当然这是可以有办法弥补的,一种方法是使用交叉熵函数作为损失函数,这里不多说。ReLU是非饱和的,亲测效果挺不错,所以这货最近挺火的。
这里只说我们用到的激活函数:
求一下它的导数把,因为后面讲bp算法会直接套用它:
先祭出大杀器,高中数学之复合函数求导法则:
它的导数图像:
▌
五. 沙漠中心的风暴:BP(Back Propagation)算法
1. 神经网络的结构
经过上面的介绍,单个神经元不足以让人心动,唯有组成网络。神经网络是一种分层结构,一般由输入曾,隐藏层,输出层组成。所以神经网络至少有3层,隐藏层多于1,总层数大于3的就是我们所说的深度学习了。
下面的图左侧是原始数据,中间很多绿点,外围是很多红点,如果你是神经网络,你会怎么做呢?
一种做法:把左图的平面看成一块布,把它缝合成一个闭合的包包(相当于数据变换到了一个3维坐标空间),然后把有绿色点的部分撸到顶部(伸缩和扭曲),然后外围的红色点自然在另一端了,要是姿势还不够帅,就挪挪位置(平移)。这时候干脆利落的砍一刀,绿点红点就彻底区分开了。
重要的东西再说一遍:神经网络换着坐标空间玩数据,根据需要,可降维,可升维,可大,可小,可圆可扁,就是这么“无敌”
这个也可以自己去玩玩,直观的感受一下:传送门
https://cs.stanford.edu/people/karpathy/convnetjs//demo/classify2d.html
2.正反向传播过程
看图,这是一个典型的三层神经网络结构,第一层是输入层,第二层是隐藏层,第三层是输出层。PS:不同的应用场景,神经网络的结构要有针对性的设计,这里仅仅是为了推导算法和计算方便才采用这个简单的结构
我们以战士打靶,目标是训练战士能命中靶心成为神枪手作为场景:
那么我们手里有这样一些数据:一堆枪摆放的位置(x,y),以及射击结果,命中靶心和不命中靶心。
我们的目标是:训练出一个神经网络模型,输入一个点的坐标(射击姿势),它就告诉你这个点是什么结果(是否命中)。
我们的方法是:训练一个能根据误差不断自我调整的模
型,训练模型的步骤是:
-
正向传播:把点的坐标数据输入神经网络,然后开始一层一层的传播下去,直到输出层输出结果。
-
反向传播(BP):就好比战士去靶场打靶,枪的摆放位置(输入),和靶心(期望的输出)是已知。战士(神经网络)一开始的时候是这样做的,随便开一枪(w,b参数初始化称随机值),观察结果(这时候相当于进行了一次正向传播)。然后发现,偏离靶心左边,应该往右点儿打。所以战士开始根据偏离靶心的距离(误差,也称损失)调整了射击方向往右一点(这时,完成了一次反向传播)
-
当完成了一次正反向传播,也就完成了一次神经网络的训练迭代,反复调整射击角度(反复迭代),误差越来越小,战士打得越来越准,神枪手模型也就诞生了。
3.BP算法推导和计算
2.隐层-->输出层:
正向传播结束,我们看看输出层的输出结果:[0.7987314002, 0.8374488853],但是我们希望它能输出[0.01, 0.99],所以明显的差太远了,这个时候我们就需要利用反向传播,更新权值w,然后重新计算输出.
1.计算输出误差:
PS: 这里我要说的是,用这个作为误差的计算,因为它简单,实际上用的时候效果不咋滴。如果激活函数是饱和的,带来的缺陷就是系统迭代更新变慢,系统收敛就慢,当然这是可以有办法弥补的,一种方法是使用交叉熵函数作为损失函数。
交叉熵做为代价函数能达到上面说的优化系统收敛下欧工,是因为它在计算误差对输入的梯度时,抵消掉了激活函数的导数项,从而避免了因为激活函数的“饱和性”给系统带来的负面影响。如果项了解更详细的证明可以点 --> 传送门(https://blog.csdn.net/lanchunhui/article/details/50086025)
对输出的偏导数:
2.隐层-->输出层的权值及偏置b的更新:
我们知道,权重w的大小能直接影响输出,w不合适那么会使得输出误差。要想直到某一个w值对误差影响的程度,可以用误差对该w的变化率来表达。如果w的一点点变动,就会导致误差增大很多,说明这个w对误差影响的程度就更大,也就是说,误差对该w的变化率越高。而误差对w的变化率
就是误差对w的偏导。
所以,看下图,总误差的大小首先受输出层神经元O1的输出影响,继续反推,O1的输出受它自己的输入的影响,而它自己的输入会受到w5的影响。这就是连锁反应,从结果找根因。
那么,根据链式法则则有:
现在挨个计算:
有个学习率的东西,学习率取个0.5。关于学习率,不能过高也不能过低。因为训练神经网络系统的过程,就是通过不断的迭代,找到让系统输出误差最小的参数的过程。每一次迭代都经过反向传播进行梯度下降,然而误差空间不是一个滑梯,一降到底,常规情况下就像坑洼的山地。学习率太小,那就很容易陷入局部最优,就是你认为的最低点并不是整个空间的最低点。如果学习率太高,那系统可能难以收敛,会在一个地方上串下跳,无法对准目标(目标是指误差空间的最低点),可以看图:
xy轴是权值w平面,z轴是输出总误差。整个误差曲面可以看到两个明显的低点,显然右边最低,属于全局最优。而左边的是次低,从局部范围看,属于局部最优。而图中,在给定初始点的情况下,标出的两条抵达低点的路线,已经是很理想情况的梯度下降路径。
3.输入层-->隐层的权值及偏置b更新:
那么,现在开始算总误差对w1的偏导:
4.结论:
我们通过亲力亲为的计算,走过了正向传播,也体会了反向传播,完成了一次训练(迭代)。随着迭代加深,输出层的误差会越来越小,专业点说就是系统趋于收敛。来一张系统误差随迭代次数变化的图来表明我刚才说描述:
▌
六. 沙漠的绿洲:代码实现
1. 代码代码!
其实已经有很多机器学习的框架可以很简单的实现神经网络。但是我们的目标是:在看懂算法之后,我们是否能照着算法的整个过程,去实现一遍,可以加深对算法原理的理解,以及对算法实现思路的的理解。顺便说打个call,numpy这个库,你值得拥有!
-
代码实现如下。代码里已经做了尽量啰嗦的注释,关键实现的地方对标了公式的编号,如果看的不明白的地方多回来啃一下算法推导。对应代码也传到了github上。
-
代码能自己定义神经网络的结构,支持深度网络。代码实现了对红蓝颜色的点做分类的模型训练,通过3层网络结构,改变隐藏层的神经元个数,通过图形显示隐藏层神经元数量对问题的解释能力。
-
代码中还实现了不同激活函数。隐藏层可以根据需要换着激活函数玩,输出层一般就用sigmoid,当然想换也随你喜欢~
import h5py
import sklearn.datasets
import sklearn.linear_model
import matplotlib
import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(1)
font = fm.FontProperties(fname='/System/Library/Fonts/STHeiti Light.ttc')
matplotlib.rcParams['figure.figsize'] = (10.0, 8.0)
def sigmoid(input_sum):
"""
函数:
激活函数Sigmoid
输入:
input_sum: 输入,即神经元的加权和
返回:
output: 激活后的输出
input_sum: 把输入缓存起来返回
"""
output = 1.0/(1+np.exp(-input_sum))
return output, input_sum
def sigmoid_back_propagation(derror_wrt_output, input_sum):
"""
函数:
误差关于神经元输入的偏导: dE/dIn = dE/dOut * dOut/dIn 参照式(5.6)
其中: dOut/dIn 就是激活函数的导数 dy=y(1 - y),见式(5.9)
dE/dOut 误差对神经元输出的偏导,见式(5.8)
输入:
derror_wrt_output:误差关于神经元输出的偏导: dE/dyⱼ = 1/2(d(expect_to_output - output)**2/doutput) = -(expect_to_output - output)
input_sum: 输入加权和
返回:
derror_wrt_dinputs: 误差关于输入的偏导,见式(5.13)
"""
output = 1.0/(1 + np.exp(- input_sum))
doutput_wrt_dinput = output * (1 - output)
derror_wrt_dinput = derror_wrt_output * doutput_wrt_dinput
return derror_wrt_dinput
def relu(input_sum):
"""
函数:
激活函数ReLU
输入:
input_sum: 输入,即神经元的加权和
返回:
outputs: 激活后的输出
input_sum: 把输入缓存起来返回
"""
output = np.maximum(0, input_sum)
return output, input_sum
def relu_back_propagation(derror_wrt_output, input_sum):
"""
函数:
误差关于神经元输入的偏导: dE/dIn = dE/dOut * dOut/dIn
其中: dOut/dIn 就是激活函数的导数
dE/dOut 误差对神经元输出的偏导
输入:
derror_wrt_output:误差关于神经元输出的偏导
input_sum: 输入加权和
返回:
derror_wrt_dinputs: 误差关于输入的偏导
"""
derror_wrt_dinputs = np.array(derror_wrt_output, copy=True)
derror_wrt_dinputs[input_sum <= 0] = 0
return derror_wrt_dinputs
def tanh(input_sum):
"""
函数:
激活函数 tanh
输入:
input_sum: 输入,即神经元的加权和
返回:
output: 激活后的输出
input_sum: 把输入缓存起来返回
"""
output = np.tanh(input_sum)
return output, input_sum
def tanh_back_propagation(derror_wrt_output, input_sum):
"""
函数:
误差关于神经元输入的偏导: dE/dIn = dE/dOut * dOut/dIn
其中: dOut/dIn 就是激活函数的导数 tanh'(x) = 1 - x²
dE/dOut 误差对神经元输出的偏导
输入:
derror_wrt_output:误差关于神经元输出的偏导: dE/dyⱼ = 1/2(d(expect_to_output - output)**2/doutput) = -(expect_to_output - output)
input_sum: 输入加权和
返回:
derror_wrt_dinputs: 误差关于输入的偏导
"""
output = np.tanh(input_sum)
doutput_wrt_dinput = 1 - np.power(output, 2)
derror_wrt_dinput = derror_wrt_output * doutput_wrt_dinput
return derror_wrt_dinput
def activated(activation_choose, input):
"""把正向激活包装一下"""
if activation_choose == "sigmoid":
return sigmoid(input)
elif activation_choose == "relu":
return relu(input)
elif activation_choose == "tanh":
return tanh(input)
return sigmoid(input)
def activated_back_propagation(activation_choose, derror_wrt_output, output):
"""包装反向激活传播"""
if activation_choose == "sigmoid"