前言
在目前而言,神经网络恐怕是机器学习尤其是深度学习最为热门的一个方向,故笔者将其放在较后的位置予以论述。神经网络(Neural Network)顾名思义就是模拟人类(为什么不是其它动物?)生物特征上的大脑神经元的结构,故可以认为这实际上是一种人工智能或机器学习的结构主义,其主要代表人物是Hinton教授等人,但在人工神经网络大热的今天,为什么这派人物似乎没有得到计算机界最高奖项——图灵奖,这个恐怕就不太好解释,笔者思考其原因可能和神经网络本身一样不太具有解释性有极大关系(即在数学上没法完全说得清楚有关,其主要过程可能还是依赖于经验,即对各种参数组合的调整并观察以获得较好结果;难道这派人物的数学都不太好?如果拿Hinton和Vapnik比,确实如此,或还是缺少更为高级的数学理论以支撑,而不仅仅是分析学或概率学),故其与如复杂性理论、密码学(基于数论基础或其它诸如椭圆曲线等)等可能就存在天然的理论鸿沟。 从分类上看,人工神经网络所包含的类型很多,除了我们最熟悉的反向传播神经网络(BPNN,本文还是以此为主要内容)、卷积神经网络(CNN)、递归神经网络(RNN)、受限玻尔兹曼机(RBM)等等,还有不胜枚举的上述神经网络的各种变形,但究其根本任务还是进行分类和回归,当然还有用于编码、压缩以及特征抽取的。 从结构上看,绝大多数人工神经网络的结构都是相似的,它们都是层次式的,每层都包含若干个所谓的神经元,各层的神经元之间有相互联系的边,只不过或多或少而已;神经元包含了值和偏置,而连接的边则有所谓的权重,除了上述这些,每个神经元还有激励函数和其有关,这个激励函数一般是非线性的,如果是线性的就存在一些问题,当然在目前深度学习神经网络盛行,其激励函数也可能是线性的,如ReLU及其各种变种,大约常用的有二十多种之多,而非线性的激励函数主要是Sigmoid、双曲正切等,而在网络的输出部分可能还会用不同的函数类型(这里考虑的是归一化问题),主要是Softmax。 另外,特别对于反向传播神经网络等分类网络而言,其最终的评价函数也可以采用不同的形式,比如最为常见的MSE方法或者交叉熵方法等。 最后对于神经网络而言,其主要有几个显著的特征,第一为非线性,第二为非局限性,第三是非常定性以及第四为非凸性;以下就以反向传播神经网络为主要的论述对象。
反向传播神经网络
基本结构和目标
 与大部分人工神经网络类似,反向传播神经网络也是被组织成多个层次,以下使用L表示网络共有多少层,而使用l表示关于层的循环变量,而每一层均有若干个神经元,同层之间的神经元之间没有任何联系(这个与RNN不同,在RNN中同层之间的神经元有连接),第一层和最后一层分别被称作输入层和输出层,而中间的各个层均被称作隐层,每层之间的神经元是全连接关系(在卷积神经网络中并非如此),即如果l-1层有N1个神经元,而l层有N2个神经元,则连接的边有N1* N2条,故总体的连接的边数为:
在前文也提到过,对于每个神经元(反向传播神经网络中)而言,其中包含有神经元的值和偏置,它们一般为浮点数,而连接的边一般具有权重,它们一般也是浮点数,在实际应用中会根据情况将它们的取值范围约束在0和1之间或者-1和1之间。
除输入层的神经元之外,每层神经元的取值是通过如下公式获得的:
在上式中,等式左边为第l+1层的某个神经元,而等式右边的Activator为激活函数,一般可以使用Sigmoid或其它类似函数,如常见的三角函数,它的作用是将线性输出变成非线性的并约束了输出,而函数内部则是两层神经元连接的权重乘以相应的上层神经元的值,这里需要注意的是将偏置转变成了wi0(没有使用常见的b),而x0为1,所以j的下标是从0开始的,其内涵是没有任何差别的。
如果将公式2写成向量形式则如下:
故可以看出公式3更为简练一点,而且可以从向量空间(其实可能更为准去地说是赋范空间,这里对于角度反而没什么要求)的角度理解神经网络的结构,其实每一层都是一个来自与实数积空间的一个向量,只不过每层的维度不太一样罢了。
 那么对于一组输入样本,反向传播神经网络的目标一般是为了进行分类,故令样本的个数为N,每个样本都有分类标签,我们将其记为(xk,yk),在这里输入的样本一般是向量,而输出的一般也是向量,而分类标签基本上标量,则我们可以将其扩张成One-hot的向量,比如对于MNIST库手写数字的识别,可以将标签从0,1,2,...,9改成诸如(1,0,0,0,0,0,0,0,0,0)的向量,上边这个向量代表数字0。
再对标签进行向量化后,我们就可以非常方便的指定目标函数。对于分类任务,我们希望最后的结果就是总体输入样本和最终分类的距离越小越好,故可以想见只要整体距离近乎为0,这里可以取赋范空间的相关范数进行计算,期望如下:
其中x_k^L是最后一层、也就是输出层的输出向量,显然它应和样本标签在同一个空间中(即至少维度是相同的),而Ek也就是分类的误差。当然,我们肯定希望是没有误差最好,但这只能是美好的愿望(至少在反向传播神经网络中是如此)。
在实际应用中,这个范数当然可以取空间的1范数或无穷范数,但是由于它们是不可导的,故常用2范数,那么公式4就可以变成如下形式(加入1/2也只是为了消去常数):
以下就是对公式5的求导并利用梯度下降法逼近各个参数,这些参数也就是各个连接边的权重以及神经元的偏置。
反馈
 其实所谓反馈的目的就是为了调整各个参数,使之达到最优化,其具体手段不外对这些参数求导并利用梯度下降法进行;为了说明清楚,我们再把公式5按照欧氏空间(难道是赋范空间变成了距离空间?)的距离方式再重写一下,使之更易理解:
在公式6中,N(L)为输出层的神经元数量,其实就是输出层的维度,那么如果对上式求取偏导,则不是非常明显,如果综合公式2和公式6就能一目了然了,为了不使公式过长,现针对某个样本、某个输出神经元给出样例,则公式如下(仅针对一个样本,故分母中没有N):
通过上述公式的变形,就可以清晰地看出需要对哪些参数求取偏导,则我们先对连接边的权重进行,可得:
注意在公式8中,为了方便起见,对于权重是写成向量形式,而且不包含偏置,即其实是没有j=0这项的;另外,对于偏置的偏导,则如以下公式:
可以看出公式8和公式9差别不是太大,仅仅相差一个上层神经元的值;如果选择激活函数为Sigmoid,并用σ记之,由于这个函数有比较好的性质,因为容易计算,其导数为:σ(x) (1-σ(x)),故可将公式8和公式9改写成如下形式:
通过以上相关的推导,就能得到各个参数的偏导,则利用梯度下降法分别可以得到相关参数的更新公式,如下:
在以上公式中,η被称作学习率,一般是一个小于1的正实数,但在网络训练初期,这个学习率不宜取得的太小,这样可以加快模型的收敛速度,但在经过若干次迭代后,为了减少抖动,可以将其慢慢变小,也就是说学习率可以不必是一个固定数值,可以取其对迭代次数的减函数。
当然,需要说明的一点是相关参数的更新肯定不是训练完一个样本就立即更新,而一般是对所有样本训练完后再更新一次,或者为了加快收敛速度,也可以训练完一个样本子集就更新,这时在编写程序中应注意保留这些参数变化的结果(一般把它们加起来就可以了),然后在变更参数时,将变化和除以被训练样本的数量即可。
读者看过上述的推导后,可能会产生一个疑问,因上面的过程中实际上只涉及到了输出层到最后一个隐层之间的关系,而没有从输出层逐层触发到输入层和第一个隐层之间的关系,其实这个推导也比较简单,就是如果隐层越多则公式越长,这里做个比喻,这个递推公式就像是一棵树的结构,而不是线性的;以下再给出最后一个隐层到倒数第二个隐层(如果网络中确实存在,即神经网络至少是四层的)的变化公式:
需要注意的是,上述公式中的参数是最后一个隐层和倒数第二个隐层之间连接的边的权重,这个与公式10和11不同(因为显然那是最后一个隐层和输出层各神经元连接的边的权重),如果还有其它隐层,则公式的形式完全一样,这里其实可以明确看出公式10和14是乘积求和的关系,因为下一层神经元的取值恰恰来自于上一层所有神经元的取值。故可以想见所谓梯度消失是怎么来的(针对多个隐层而言),因为激活函数的导函取数一般是小于1的(如果使用Sigmoid),故在连乘多次后,值将趋近于零,这将导致梯度(即变化情况)无法传导到网络中前面的相关隐层,所以在多次迭代后仍无法收敛,而如果采用其它的激活函数则可能造成梯度爆炸(即每次计算的导函数值一般都大于1),不过解决梯度爆炸要比解决梯度消失略好,因为可以给出一个梯度变化的上限值。
所以通过上述分析,可以看出为什么基于反馈的神经网络没有太深层次的,最一般的应用一般也就是三层,即只包含一个隐层,而超过五层的网络几乎无法训练或使用;那么这时提高网络分类准确性的办法则只有增加隐层神经元的数量这一几乎是唯一的途径,故其在上个世纪一度走入了死胡同。
算法基本流程
对于反向传播神经网络的实现而言,一般包含如下几个主要的步骤(其实可能大多数人工神经网络算法也都包含这几步),其一是将训练数据进行一些处理,比如读入、整理或归一化等,在一些常用的机器学习框架中就直接包含了这些内容,如Tensorflow、R、Caffe等;其二为正向运算,即从网络的第一层开始,逐层进行运算,当然这是在对各参数进行初始化基础上进行的,初始化参数可以利用均匀分布在-1到1的区间上或者0到1的区间上生成;其三自然就是从输出层逐层向前进行反馈。 在训练完一次样本集后,对各个参数并进行重新训练,如此反复进行,一直到满足的精度为止或者达到迭代的上限。而训练万网络后,可利用测试数据对网络的分类能力进行验证,用错误分类的样本个数除以测试样本总数即可得到分类的准确性,具体如下:
总体流程
算法:训练反向传播神经网络 输入:规格化后的N个样本 输出:反向传播神经网络(主要是各层连接边的权值及其偏置)
初始化各层的相关参数,即对于层变量l=2,3...,L而言,用随机数初始化w_ij^l向量(这里j从0开始,为偏置);令T为迭代次数上限,t为迭代次数;令E为误差下限,e为误差;
while(t < T or |e| > E) {
for(x_k∈X) {
正向传递计算;
计算误差并累计至中间变量;
反向传播;
}
e =累计误差除以样本总数;
按公式调整相关权值参数及偏置;
t = t+1;
}
正向传递
 相较于反向传播而言,样本点的正向传递算法相更加直观些,其主要就是利用公式3对层间相关神经元相互关系进行计算并将每次的计算结果保存,以下算法假定神经元的激活函数就是Sigmoid,则相关计算方法如下(注意第一层为0,每层的神经元下标也是从0开始):
算法:反向传播神经网络正向计算 输入:一个样本点及L层带权神经网络 输出:神经网络输出向量
for(l = 1;l < L; l++) {
for(i = 0; i < N(l); i++) {
neuron[l][i].value = 0;
for(j=0; j < N(l-1); j++){
neuron[l][i].value += w[i][j]*neuron[l-1][j].value;
}
neuron[l][i].value = σ (neuron[l][i].value+ neuron[l][i].bias);
}
}
反向传播
 对于反向传播而言,顾名思义就是从网络的最后一层开始,逐层向前推进,计算各个参数需要调整的数量,并暂时累计之,待训练完一个迭代后统一调整并在下一个迭×××始前清除累积量。
算法:反向传播神经网络逆向反馈计算 输入:一个样本点及L层带权神经网络 输出:待调整参数偏差累计和(对于一个样本点而言暂时不会调整各个参数)
// 累计误差,使用欧式距离
e += (y_k-x_k )^T (y_k-x_k);
for(l = L-1;l > 0; l--) {
for(i = 0; i < N(l); i++) {
for(j = 0; j < N(l-1); j++) {
// 如果是最后一层
if(l == L-1) {
// 累计权重变化
w[i][j].delta_weight_sum += (y_ki-σ(W_i^T X^((l-1) ))(σ(x) (1-σ(x)))|_(x=x_k^L ) X^((l-1) );
} else {
// 如果是其它层
// 累计权重变化
w[i][j].delta_weight_sum += (σ(x) (1-σ(x)))|_(x=x_k^l ) X^((l-1) );
}
} // end of for j
// 累计偏置变化
if(l == L - 1) {
neuron[l][i].delta_bias_sum += (y_ki-σ(W_i^T X^((l-1) ))(σ(x) (1-σ(x)))|_(x=x_k^L );
} else {
neuron[l][i].delta_bias_sum += (σ(x) (1-σ(x)))|_(x=x_k^l );
}
} // end of for i
}
 由于上述算法都是对于相关参数的变化量累计值而言的,故在最后调整参数时应该将这些累积量除以样本的总数;另外,在实际实现时应注意偏置是跟随神经元的(输入层没有偏置),而神经元之间的连接则可以采用单独的变量进行记录或者也可以将其放入神经元数据结构之中。
其它
 如果读者自己实现一个反向传播的神经网络会发现其实它的计算还是相当耗时的,尤其在样本很多(如MNIST就包含5万个训练样本和1万个测试样本,而新的图形库也相当巨大)而且迭代次数较高时(比如至少迭代2000次)整体训练过程就会比较长,这时可以考虑使用多线程的并发训练或这干脆使用分布式训练,另外使用CUDA也可以在很大程度上缩短训练的时间,但CUDA毕竟存在一定局限性(必须是NIVIDA的显卡),而且在虚机上不一定有效。
 当采用多线程并发或者分布式训练,一般可以将大的样本集进行一定粒度的切分,然后定期对参数进行统一调整并再次同步或下发这些参数。
 最终训练好的网络可以采用XML格式或者JSON格式存储,在加上序列化和反序列化接口就能较好地进行实用了。
 另外,在计算整体误差时,也可以不使用平方差公式,而采用交叉熵的形式进行,其公式如下:
![](http://i2.51cto.com/images/blog/201712/04/6e4172094f759fda6c61fbedcc6a72b8.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
 在上式中,p为神经网络输出层的维度,ln为自然对数,当然在实现时要避免其值小于等于零,否则是没有意义的,故选择何种激励函数、参数初值就变得尤为重要,如若不然会得到很多错误。
优化方法
 在按本文的相关方法或者类似的方法,实际实现基于反向传播的人工神经网络时,会发现可能实际效果并不是特别理想,其准确了可能尽在百分之九十左右徘徊,即没有你使用Tensorflow等框架在同样的数据集上获得的效果理想,这个也是比较正常的结果。
 因为我们暂时还没有考虑其它一些相关的优化方法。在对数据进行训练时可能会出现一些所谓的过拟合及欠拟合现象,而尤以前者最为常见。过拟合(Overfitting)的原因比较复杂,其主要表现是在训练的时候准确率较高,当网络应用在训练数据以外的数据时,表现较差;它的产生可能有如下原因,第一为训练数据不足或样本标签错误,第二为数据中可能存在较大的噪音,第三迭代次数过多,存在过分训练(Overtraining)的因素。下面简单地给出几种解决的方案或思路,以对抗过拟合。
正则化
 首先,回忆下我们在对一般线性回归模型的参数求解中遇到的相关问题及解决方法,即可以在一般线性回归模型中使用LASSO回归和岭回归,同样地我们可以在反向传播神经网络的误差评估函数中同样使用1-范数和2-范数正则化手段,即将公式6和公式16分别变形为如下形式(2-范数,即使用类似岭回归的方法):
在使用这个所谓正则化方法时,如果采用LASSO回归则由于其目标函数在零点处不可导,故需要进行一些特殊处理(如使用软阈收缩算子),而对于岭回归则没有这种问题,但LASSO方法的效果要略为好些;另外,在上面四个公式中,w是针对网络中的所有权值而言的,而不仅仅是某层和某层之间的连接边的权重;而且,由于评估函数发生了变化,则其导函数相关的计算方法也会相应地产生一点点差异,这里需要注意。λ被称作正则化参数,其取值一般是大于1的,经验表明可以取5。
随机丢弃
 除了上述正则化方法可以对神经网络的训练结果产生一些积极地变化外,我们还可以对各个神经元进行一些修改,这种修改的方法被称作Dropout。所谓Dropout就是在对神经网络进行训练时,可以以一定的概率使网络中的神经元处于无效状态,当然这个过程主要是在反向传播的阶段,即随机进行梯度的传播,每次好像都在训练一个不同的网络。而且这种方法的好处还在于可以缩短训练的时间(因为神经元只是原来的子集,数量上减少了)。
 在使用Dropout方法时,可以采用伯努利分布,其参数就选择0.5,即每次随机丢弃50%的神经元,经验表明(又是经验!)选择这个参数效果最好。Dropout技术本身比较简单,这里就不给出更多描述了,在各个开源框架中也有相应的实现,而且如果自己实现这个方法也并不复杂。
数据扩展
 另外,针对训练样本的不足而造成的过拟合,我们可以采用人工生成训练数据的方法来克服这个问题,即对已有训练数据实施缩放、旋转(线性方法)、轻微变形(非线性方法)等手段来扩充训练数据,这个对于图形的识别具有较好的效果。
结语
 通过本文简单地对反向传播神经网络的描述,读者可能可以感受到这类基于神经网络的机器学习算法确实没有非常强的数学基础(至少很多问题无法说清楚,可能更多是依赖直觉?),这点和支持向量机略有不同(支持向量机本身就具有严格的数学基础,推导过程也是丝丝入扣),在实际使用中经常也是需要针对不同的应用场景对各类参数的组合进行各种各样地调整,这个在卷积神经网络、循环神经网络(RNN,即Recurrent Neural Network,最主要的变体是长短时记忆网络,即LSTM)或递归神经网络(也是RNN,不过这里的R是Recursive的首字母,结构上有些不同)中也是极为常见的,故训练的时候还是需要珍惜机器时间。
 另外,由于反向传播神经网络在历史上存在着梯度消失或梯度爆炸问题,故神经网络在很长的一段时间内几乎销声匿迹了,但随着相关解决办法的提出以及硬件的发展,深度学习目前成为最为热门的技术,而在深度神经网络中除了使用Dropout等技术外,还使用了一些线性的激励函数,这使深度成为可能,甚至100多层的网络也很多见,这个是在以前无法想象的。近期,Hinton教授又宣称可以不用反馈来训练网络,而是采用一种被称作胶囊(Capsule)的技术来进行,这个可能也是非常令人期待的东西。
 最后推荐一个网站的内容:http://neuralnetworksanddeeplearning.com,这个网站上面的内容要更为详细和丰富,从某种程度上来说,可能详细得有点多余了,不过这个详细程度只针对反向传播神经网络而言,对于其它深度学习的神经网络并不详细,不过还是建议耐心阅读,不知是否有翻译过来的书?