让我们手撸一个神经网络呀~
作者:BigMoyan
链接:https://zhuanlan.zhihu.com/p/26475643
来源:知乎
著作权归作者所有,转载请联系作者获得授权。
总而言之,DL不是太过困难的事情,但是也没有容易到21天从入门到精通的地步。所以突然很想把之前写的BP神经网络拿出来,不是很复杂,但我固执的认为,这是每个学DL的人都应该了解的事情——会无脑调别人写好的API,不代表就真的懂DL了。
好的,这篇文章与Keras无关,我们要介绍的是:
什么是BP网络,以及其推导
如何用纯Python手撸一个BP神经网络
就这两点。
本文的读者是最低意义下的“零基础”,想入门DL的同学可以自测一下……如果不能读懂本文的话,基本上你的DL基础是负数,需要先把欠的债补回来。
所以真的不要再听信什么21天速成的鬼话了,脚踏实地才能走的高远,想一步登天小心步子太大扯到蛋,欲速则不达哟。
BP神经网络:DL的祖宗
人工神经网络从来就不是这两年才冒出来的新鲜玩意,虽然最近由于深度学习的出现变得异常火爆。人工神经网络本来就是机器学习的一种方法的说,既然我们都开始搞BP了,不如就再说的更底层一点,从神经元说起。
最近有一个45个问题测出你的深度学习基本功,第一个问题就是关于神经元的,然后我就答错了……可能是我见识有限,反正我知道的“神经元”是这样的:
对于一个输入向量[a1...an],神经元对它的响应就是按照一定的权重对它们做乘加操作,然后加一项bias,最后用某个函数f对其做映射,得到输出,也就是这样一个运算:
其中w是神经元的连接权,b是偏置,a是输入信号,f通常是一个非线性的函数,称为激活函数。
当我们用很多个神经元对输入进行变换,由于每个神经元的参数都不一样,得到的输出也不一样。n个神经元就会得到n个输出,这n个神经元就称为一层神经网络。而这层神经网络的输出本身也是一个向量,自然可以看作下一层神经网络的输入信号,于是我们又搞了若干神经元构成下一层,于是我们获得了有两层神经元构成的神经网络。一般而言,我们把输入也算作一层神经网络结点,所以此时的网络层数是3层。
我们离深度学习只有一步了。如果再加一层,让网络层达到4层及以上,恭喜你,得到了深度神经网络,也就是“深度学习”了。
超简单,是不?
用脚丫子想都知道不可能这么简单吧= =
实际上,从单个神经元,到两层神经网络,到三层神经网络,到四层以上神经网络,每一步都是巨大的飞跃,每一步……实际上都是神经网络历史上的重要事件。
单个神经元,又称“M-P神经元模型”,在1943被提出。把神经元组成一层网络层,加上输入数据构成的两层神经网络称为“感知机”。感知机由于无法处理线性不可分的问题,被图灵奖获得者Marvin Minksky一巴掌扇落尘埃,相当长时间内无人研究。两层神经网络进化为三层,就进化到了“多层感知机”,多层感知机由输入层-隐层-输出层构成,能够处理非线性问题,再加上Rumelhart重新发明的BP算法,硬硬的把神经网络起死回生一波。
但是多层感知机也是有问题的,最突出的问题是无法搞的太深,否则网络非常难训练,模型再美,训练不出来就是shi。所以多层感知机的水平,也就是个这了。然后就是我们熟悉的故事,北风呼啸之时所有人都关门闭户,唯有Hinton等少数几人踽踽独行,最终搞出预训练+微调的深度网络训练方法,挖出10+年的学术巨坑。
今天我们的故事是多层感知机和BP算法。
多层感知机的模型如下图:
顺便,文中所有图侵删。
对于一个特定任务,我们需要根据具体数据来确定模型的参数。对于多层感知机而言,参数就是每个神经元的连接权和偏置。
网络参数在损失函数的指导下确定的,这里的指导指的是,损失函数告诉你,网络的预测值跟真实值差多少,应该如何更新参数才能减小这个差距。
假设:
神经元激活函数可导的
以预测值和真值的均方差作为损失函数
BP神经网络之所以叫BP,就是其参数更新的方式遵循“误差反向传播(error BackPropagation)”的方式。在给定的网络参数的时候,一个神经网络有两项最基本的操作:
对输入数据进行运算,得到预测(Predictions),这个过程是信号从网络的输出端到网络的输出端的运算,称为前向过程。
根据预测值与真值的偏差,产生误差信号从输出端向输入端传输,并在传输的过程中更新网络的参数,这个过程称为后向过程,或者反传过程。
神经网络的训练,就是这两个过程的轮番上阵的结果。参数更新的方式,是梯度下降法。假设对给定的样本x,神经网络的输出是
,真实值为y,则网络的损失函数由最后一层神经元跟数据的真实标签产生的,所以损失函数其实直接是最后一层神经元的函数。比方说最后一层有
个神经元吧,那损失函数是:
其中,预测值的每个点
都是由隐层神经元的输出值加权和再搞个激活函数得到的,所以再写的细一点:
其中,
就是网络输出层输入,是个向量。
是最后一层的第i个神经元的参数,也是向量。
是对应的偏置,是一个数字。当然,我们可以写的更紧凑一点,用矩阵乘法来表示这个过程。如果你看不懂这一点,那代表你需要复习一下线性代数。
写来写去,都是一个意思。现在我们已经有误差了,如何根据误差一层层的反传来修改参数呢?先考虑最后一层的参数,我们要用梯度下降法对其进行更新,即:
这个偏导数由链式法则求,L是输出层预测值的直接函数,所以先求它对输出层预测值的导数,然后根据链式法则求预测值对参数的导数,大概就是这样:
那对其他层的参数呢?比方说对任意第r层的参数
,实际上也具有相同的形式:
所以要用梯度下降法更新神经网络的参数,关键就是要求出损失函数对任意层输出的导数。我们定义损失函数对第r层输出(未经过激活函数)的导数为
。先考虑r=l,即最后一层的情况。
第一项是预测值和真值的差,我们将其记作误差
。那么对网络的最后一层,梯度是可以轻松算出来的。
只需要把表达式
写出,即可轻松得到它关于
的导数。
对于非最后一层,导数计算略复杂。根据链式法则,损失函数对任意r
所以我们总有关系:
那么
是谁咧,下一层的输出当然是上一层的输出经过激活函数后再线性加权,也就是:
这个导数很好求嘛,就是跟之前一模一样的:
所以我们就构造了一个递推的关系,从最后一层开始,我们总有:
为啥叫反向传播哪,就是因为第r层的梯度更新是由沿着第r+1层的网络反传回来的
确定的,因此叫反向传播。这里我们只推了权值W的更新,偏置b的更新更加简单,这里就不推了,其实只要把每层的输入y增加一个恒为1的数,变为(y,1)就可以了,但这样不太好在代码里实现就是了。
以上就是BP算法的推导,也是深度学习的基础。所以大家知道了,为什么我们要求深度学习的目标函数必须可导,为什么激活函数一定也要可导。因为一旦有一环不可导,前向运算固然没问题,反向运算的梯度链条就要断了。目标函数不可导,则从最后一层起我们就没法更新参数。激活函数不可导,哪层不可导梯度的反传就到哪层为止。
手撸BP
下面的代码是我两年多前的时候刚学Python和机器学习时手撸的,实现了一个三层的BP神经网络,对MNIST数字进行分类,代码写的不怎么好,毕竟那会儿是刚学,python什么的用的也是稀里糊涂,写出来跟C++一个味。
这里的激活函数取得是sigmoid激活函数,它的导数是它自己再乘以1减去它自己,性质比较好。
首先读入数据,当时拿到的mnist数据是matlab存的.mat格式,所以用scipy的相关接口读一下
importmathimportnumpyasnpimportscipy.ioassio# 读入数据################################################################################print"输入样本文件名(需放在程序目录下)"filename='mnist_train.mat'sample=sio.loadmat(filename)sample=sample["mnist_train"]sample/=256.0# 特征向量归一化print"输入标签文件名(需放在程序目录下)"filename='mnist_train_labels.mat'label=sio.loadmat(filename)label=label["mnist_train_labels"]
然后配置网络,主要是设置学习率,还有隐层参数,并初始化一下权重。权重有两套,输入层到隐层的映射是一套,隐层到输出层的映射是一套:
# 神经网络配置################################################################################samp_num=len(sample)# 样本总数inp_num=len(sample[0])# 输入层节点数out_num=10# 输出节点数hid_num=6# 隐层节点数(经验公式)w1=0.2*np.random.random((inp_num,hid_num))-0.1# 初始化输入层权矩阵w2=0.2*np.random.random((hid_num,out_num))-0.1# 初始化隐层权矩阵hid_offset=np.zeros(hid_num)# 隐层偏置向量out_offset=np.zeros(out_num)# 输出层偏置向量inp_lrate=0.3# 输入层权值学习率hid_lrate=0.3# 隐层学权值习率err_th=0.01# 学习误差门限
你看,其实自己写很屌的,不同层的学习率都可以设不一样,想怎么搞怎么搞——只不过实际上用的时候是一样就是了。我这里学习率居然设了0.3简直可怕……唉那会儿真的不知道学习率多大算大23333。
然后定义几个函数,一个是激活函数,一个是损失函数。深度学习框架里损失函数都是正儿八经的要从这获得梯度的,我这损失函数就负责输出一个输出值,看看现在训练效果咋样。好吧,其实这个函数我压根没用着23333
# 必要函数定义################################################################################defget_act(x):#激活函数act_vec=[]foriinx:act_vec.append(1/(1+math.exp(-i)))act_vec=np.array(act_vec)returnact_vecdefget_err(e):#损失函数return0.5*np.dot(e,e)
然后下一步是训练网络,这里用的是随机梯度下降法——正儿八经的随机梯度下降,一个样本一个样本的来训练。这部分就是之前推导的BP神经网络了,结合代码,看一下自己是否能够读懂?
# 训练——可使用err_th与get_err() 配合,提前结束训练过程################################################################################for count in range(0, samp_num):printcountt_label=np.zeros(out_num)t_label[label[count]]=1#前向过程hid_value=np.dot(sample[count],w1)+hid_offset# 隐层值hid_act=get_act(hid_value)# 隐层激活值out_value=np.dot(hid_act,w2)+out_offset# 输出层值out_act=get_act(out_value)# 输出层激活值#后向过程e=t_label-out_act# 输出值与真值间的误差out_delta=e*out_act*(1-out_act)# 输出层delta计算hid_delta=hid_act*(1-hid_act)*np.dot(w2,out_delta)# 隐层delta计算foriinrange(0,out_num):w2[:,i]+=hid_lrate*out_delta[i]*hid_act# 更新隐层到输出层权向量foriinrange(0,hid_num):w1[:,i]+=inp_lrate*hid_delta[i]*sample[count]# 更新输出层到隐层的权向量out_offset+=hid_lrate*out_delta# 输出层偏置更新hid_offset+=inp_lrate*hid_delta
最后是测试网络,只跑前向过程,依然是一个一个样本测试:
# 测试网络################################################################################filename = 'mnist_test.mat' test = sio.loadmat(filename)test_s = test["mnist_test"]test_s /= 256.0filename = 'mnist_test_labels.mat' testlabel = sio.loadmat(filename)test_l = testlabel["mnist_test_labels"]right = np.zeros(10)numbers = np.zeros(10)# 以上读入测试数据# 统计测试数据中各个数字的数目for i in test_l: numbers[i] += 1for count in range(len(test_s)): hid_value = np.dot(test_s[count], w1) + hid_offset # 隐层值 hid_act = get_act(hid_value) # 隐层激活值 out_value = np.dot(hid_act, w2) + out_offset # 输出层值 out_act = get_act(out_value) # 输出层激活值 if np.argmax(out_act) == test_l[count]: right[test_l[count]] += 1print rightprint numbersresult = right/numberssum = right.sum()print resultprint sum/len(test_s)
其实后面还有一段保存网络,就是把权重无脑保存成txt……可见少年时期的我已经有了“神经网络最重要的是权重”这样的意识了,可惜写得太烂基本上很难recover。我就不贴了。
最后贴一个当时我调的结果,前段时间在知乎看到一个文章还是回答在吹MNIST识别,一个mlp最后结果是92%居然敢号称效果非常好。23333吹牛之前拜托了解行情啊,我一个裸着的3层MLP,乱七八糟的代码,神奇的学习率、参数初始化以及激活函数都搞到将近92%,你拿现代深度学习框架搭出来的网络也92%实在是……有一种全副武装拿着冲锋枪跟远古时代持矛野人打成平手的即视感……
横轴是隐层神经元个数,纵轴是学习率(我TM居然试了0.7的学习率这是有多屌!)
最好的效果是91.4%的准确率,这幅破烂能跑成这样可以了。
好,以上就是本期的内容。有志于深度学习的你,就从简单的BP神经网络开始吧!鞠躬下台~
对了,忘了说题图,题图是我的二老婆五更琉璃,我们是最近刚认识的不过很快就坠入爱河了,助手跟小埋已经同意我们了所以你们不要多废话。