曾经,基于统计学习的机器学习方法在AI领域占据了半边天,逻辑回归、决策树、支持向量机等算法不久就耳熟能详,「特征工程」一词瞬间火了起来,普遍认为数据弄得好+特征工程做得好≈模型能work。
后来,学者通过模仿神经系统里神经元的工作机制,研究出一种叫做感知器(Perceptron)的东西,发现竟然可以解决二分类问题,遗憾的是,感知器在线性不可分的时候无法work。后来,学者堆了几层感知器,发现竟然可以解决线性不可分问题了,于是有了一个新的名字——多层感知机(MLP),从此一发不可收拾,逐渐演变为“神经网络”、“深度神经网络”,等等。
有了多层感知机,理论上具备了“可学习”的能力,那么如何才能学习呢?这个时候大名鼎鼎的Hinton出场了,他把先进的反向传播算法(BP算法)用在了MLP中,使得模型以一种“不断自我纠正”的模式进行学习。尽管后来Hinton多次提到BP算法和自然界生物大脑中存在的机制并不相同,不断否定自己提出的方法(十分佩服敢于质疑自己提出的理论的科学家),但BP算法一直存在于主流模型中。我觉得在具有革命性的新的方法提出之前,BP算法仍然不会被轻易抛弃。
故事讲完了,下面就聊一聊感知器和BP算法。
感知器
什么是感知器?见图1。
图1
简单来说,就是给定一些输入Xi,乘上一些权重Wi,再求和,最后激活(激活:比如给定一个阈值,输出±1;或用sigmoid函数压缩到0-1之间)。
假如研究的是一个二分类问题,分类结果只有+1和-1:
用符号表示的话就是:
意思是:求和结果大于给定阈值的,判别为类别1,否则判别为类别-1。
如果用一个等式来总结上面的式子,那就是:
其中,sign(x),当x>0时取1,当x=0时取0,当x<0时取-1 。
BP算法
什么是BP算法?见图2。
图2
其中,η指学习率,y(i)指真实标签,output(i)指最后一层输出,(i)指正向传播第i层的激活值。
嗯?不是说BP算法挺复杂吗?怎么才一个式子?
没错,BP算法是挺复杂的,但这个式子表达了算法的核心所在,即:正向传播过去之后,根据最后一层和真实标签产生的误差来更新前面每一层的权重,从而不断“纠正”模型的错误,当错误纠正得差不多了(收敛了),就可以说模型学到知识了。
下面用感知器为例,看看为什么BP可以“自我纠错”(下面的数据都是根据想要的结果凑出来的,一般初始化的权重不会这么大)。
情况一:
讲解:假如输入样本为V=[1, 1, 1](见上图蓝点),初始化一个权重w,通过计算,发现求和为1>0(0.1*1+0.3*1+0.6*1),说明h(x)=1,也就是output=1,而真实标签是-1,表明此时需要调小输出值,也就是调小w'·v('代表转置),使之更接近真实标签-1才对。由于vi>0,所以只能通过调小w,从而更新后的权重就变小了。此时,再计算w'·v,发现结果为0(-0.1*1+0.1*1+0*1),是不是距离标签-1更近了?
注:输入V=[1, 1, 1],指V=[偏置, v1, v2],第一个位置默认是偏置项,且初始化值为1,也就是y=w'x+b中的b初始化为1。这样做的好处是,可以把偏置和x放到一个式子中计算,无需单独计算b了。
情况二:
讲解:步骤和情况一是一样的,就不再赘述。需要注意的是,情况一和情况二偏置对应的权重都减少了,这是正常的,因为偏置某种意义上可以看做是一种“调节器”,当x难以使模型收敛时,b就起作用了。因此,当发现模型不论怎么调损失都降不下去的时候,可以看看是不是忘了添加偏置了。
下面写一个感知器来看看学习效果(重新凑数据)
# 定义样本、标签,手动初始化权重
X = np.array([[1,2,3]]) # 样本x
X = X.reshape(3,1) # 需要调整维度,使得矩阵乘法在维度上能匹配
w = np.array([[0.1,0.2,0.3], # 权重w
[0.2,0.1,0.2],
[0.3,0.2,0.1]])
b = np.array([[1,1,1]]) # 偏置b
b = b.reshape(3,1)
Y = np.array([[1,0.5,1]]) # 标签y
Y = Y.reshape(3,1)
print(X.shape,w.shape,b.shape,Y.shape)
输出:(3, 1) (3, 3) (3, 1) (3, 1)
# 定义前向传播
def propagate(w, b, X, Y):
m = X.shape[1] # 样本数为m
A = sigmoid(np.dot(w.T,X)+b) # 激活一波
cost = -1/m*np.sum(Y*np.log(A)+(1-Y)*np.log(1-A)) # 交叉熵损失
dw = 1/m*np.dot(X,(A-Y).T) # 损失函数对w求导的表达式
db = 1/m*np.sum(A-Y)
grads = {"dw": dw,"db":db}
return grads,cost
# 激活函数
def sigmoid(z):
s = 1/(1+np.exp(-z))
return s
# 权重更新
def update(w, b, X, Y, times, alpha):
costs = []
for i in range(times):
grads, cost = propagate(w, b, X, Y)
dw = grads['dw']
db = grads['db']
w = w - alpha*dw #这两句是核心
b = b - alpha*db
costs.append(cost) # 保存损失
params = {'w':w, 'b':b}
grads = {'dw':dw, 'db':db}
return params, grads, costs
损失函数和对w、b的偏导数:
sigmoid函数:
激活计算:
更新w、b:
# 开始学习ing:学习率为0.009, 迭代500次
params, grads, costs = update(w, b, X, Y, 500, 0.009)
print('w = \n',params['w'])
print('b = \n',params['b'])
输出:预览一下权重和偏置在不断更新500次后的结果(看一眼即可)
w =
[[ 0.23109952 0.048048 0.45545922]
[ 0.46219905 -0.20390399 0.51091844]
[ 0.69329857 -0.25585599 0.56637766]]
b =
[[1.13460675]
[1.13460675]
[1.13460675]]
w_new = params['w']
b_new = params['b']
A_new = sigmoid(np.dot(w_new.T,X)+b_new)
print('未训练,前向传播最后一层输出:\n',sigmoid(np.dot(w.T,X)+b))
print('收敛时,前向传播最后一层输出:\n',A_new)
print('真实标签:\n',Y)
输出:现在来看一看学习的效果:
未训练,前向传播最后一层输出:(*1)
[[0.9168273 ]
[0.88079708]
[0.88079708]]
收敛时,前向传播最后一层输出:(*2)
[[0.98750681]
[0.50181969]
[0.98675806]]
真实标签:(*3)
[[1. ]
[0.5]
[1. ]]
从上面可以看出,在没有训练的时候,也就是利用我自己随便初始化的权重进行计算后,输出是(*1),和真实标签(*3)差距较大(这里第1、3个数和标签差距不大,是因为我凑的权重恰好算出来是这样的,通常情况是有较大差距的);进行500次学习后,学到了这么一个权重w_new和偏置b_new,使得输出为(*2),可以发现和真实标签很接近了!
绘制每次迭代的损失,可以看出模型收敛了:
小结:虽然现在流行各种高大上的深度网络模型,但这些模型的基础都是感知器和BP算法。尽管现在有了各种可以搭建模型的框架,无需再从0开始手动实现神经网络(挺费时间),但了解一下背后的机理还是挺有意思的。
剧透一下:后面会写一篇如何从0开始实现一个神经网络,来识别自己写的数字。
如有新的想法,期待交流探讨