神经网络也许是计算机计算的将来,一个了解它的好方法是用一个它可以解决的难题来说明。假设给出 500 个字符的代码段,您知道它们是C,C++,JAVA或Python。现在构造一个程序,来识别编写这段代码的语言。一种解决方案是构造一个能够学习识别这些语言的神经网络。
这篇分享就讨论了神经网络的基本功能以及构造神经网络的方法,这样就可以在编码时应用它们了。根据一个简化的统计,人脑由百亿条神经组成 ,每条神经平均连结到其它几千条神经。通过这种连结方式,神经可以收发不同数量的能量。神经的一个非常重要的功能是它们对能量的接受并不是立即作出响应,而是将它们累加起来,当这个累加的总和达到某个临界阈值时,它们将它们自己的那部分能量发送给其它的神经。大脑通过调节这些连结的数目和强度进行学习。尽管这是个生物行为的简化描述。但同样可以充分有力地被看作是神经网络的模型。
阈值逻辑单元(Threshold Logic Unit, TLU)
理解神经网络的第一步是从对抽象生物神经开始,并把重点放在阈值逻辑单元(TLU)这一特征上。一个TLU 是一个对象,它可以输入一组加权系数的量,对它们进行求和,如果这个和达到或者超过了某个阈值,输出一个量。让我们用符号标注这些功能。
首先,有输入值以及它们的权系数:X1, X2, ..., Xn 和W1, W2, ..., Wn。接着是求和计算出的Xi*Wi ,产生了激发层a,换一种方法表示:
a = (X1 * W1)+(X2 * W2)+...+(Xi * Wi)+...+ (Xn * Wn)
阈值称为 theta。最后,输出结果y。当a >=theta 时y=1,反之y=0。请注意输出可以是连续的,因为它也可以由一个squash 函数s(或sigma)判定,该函数的自变量是a,函数值在0 和1 之间,y=s(a)。
图 1. 阈值逻辑单元,带有sigma 函数(顶部)和cutoff 函数(底部)
TLU 会分类,假设一个TLU 有两个输入值,它们的权系数等于1,theta 值等于1.5。当这个TLU 输入<0,0>、<0,1>、<1,0> 和<1,1> 时,它的输出分别为0、0、0、1。TLU 将这些输入分为两组:0 组和1 组。就像懂得逻辑连接(布尔运算AND)的人脑可以类似地将逻辑连接的句子分类那样,TLU 也懂得一点逻辑连接之类的东西。
TLU 能够用几何学上的解释来阐明这种现象。它的四种可能输入对应于笛卡尔图的四个点。从等式X1*W1 + X2*W2 = theta,换句话说,也即TLU 转换其分类行为的点开始,它的点都分布在曲线X2 = -X1 + 1.5 上。这个方程的曲线将4 个可能的输入分成了两个对应于TLU 分类的区域。这是TLU 原理中更为普通的实例。在TLU 有任意数目的N 个输入的情况下,一组可能的输入对应于N 维空间中的一个点集。如果这些点可以被超平面———换句话说,对应于上面示例中的线的N 维的几何外形切割,那么就有一组权系数和一个阈值来定义其分类刚好与这个切割相匹配的TLU。
TLU 的学习原理
既然 TLU 懂得分类,它们就知道素材。神经网络也可假定为可以学习。它们的学习机制是模仿大脑调节神经连结的原理。TLU 通过改变它的权系数和阈值来学习。实际上,从数学的观点看,权系数阈值的特征有点武断。让我们回想一下当SUM(Xi * Wi) >= theta 时TLU 在临界点时输出的是1 而不是0,这相当于说临界点是出现在SUM(Xi * Wi)+ (-1 * theta) >= 0 的时候。所以,我们可以把-1 看成一个常量输入,它的权系数theta 在学习(或者用技术术语,称为培训)的过程中进行调整。这样,当SUM(Xi * Wi)+ (-1 * theta) >= 0 时,y=1,反之y=0。
在训练过程中,神经网络输入:一系列需要分类的术语示例 ;它们的正确分类或者目标 。这样的输入可以看成一个向量:<X1, X2, ..., Xn, theta, t>,这里t 是一个目标或者正确分类。神经网络用这些来调整权系数,其目的使培训中的目标与其分类相匹配。更确切地说,这是有指导的培训,与之相反的是无指导的培训。前者是基于带目标的示例,而后者却只是建立在统计分析的基础上(请参阅本文随后的参考资料)。权系数的调整有一个学习规则,一个理想化的学习算法如下所示:
清单 1. 理想化的学习算法
fully_trained = FALSE
DO UNTIL (fully_trained):
fully_trained = TRUE
FOR EACH training_vector = <X1, X2, ..., Xn, theta, target>::
# Weights compared to theta
a = (X1 * W1)+(X2 * W2)+...+(Xn * Wn) - theta
y = sigma(a)
IF y != target:
fully_trained = FALSE
FOR EACH Wi:
MODIFY_WEIGHT(Wi) # According to the training rule
IF (fully_trained):
BREAK
您或许想知道,“哪些培训规则?”有很多,不过有一条似乎合理的规则是基于这样一种思想,即权系数和阈值的调整应该由分式(t - y) 确定。这个规则通过引入alpha (0 < alpha < 1) 完成。我们把alpha 称为学习率。Wi 中的更改值等于(alpha * (t - y)* Xi)。当alpha 趋向于0 时,神经网络的权系数的调整变得保守一点;当 alpha 趋向于1 时,权系数的调整变得激进。一个使用这个规则的神经网络称为感知器,并且这个规则被称为 感知器学习规则。
Rosenblatt 于1962 年(请参阅参考资料)下的结论是,如果N 维空间的点集被超平面切割,那么感知器的培训算法的应用将会最终导致权系数的分配,从而定义了一个TLU,它的超平面会进行需要的分割。当然,为了记起Keynes,最终我们都切断了与外界的联系,专心思考。但是在计算时间之外,我们仍濒临危险,因为我们需要自己的神经网络对可能输入的空间进行不止一次的切割。
文章开始的难题举例说明了这个,假设给您 N 个字符的代码段,您知道是C、C++、Java 或者Python。难的是构造一个程序来标识编写这段代码的语言。用TLU 来实现需要对可能的输入空间进行不止一次的分割。它需要把空间分成四个区域。每种语言一个区域。把神经网络培训成能实现两个切割就可完成这种工作。第一个切割将C/C++ 和Java/Python 分开来,另一个将C/Java 和C++/Python 分开。一个能够完成这些切割的网络同样可以识别源代码样本中的语言。但是这需要网络有不同结构,在描述这个不同之处之前,先来简单地看一下实践方面的考虑。
图 2. 初步的(不完整的)感知器学习模型
考虑到排除取得 N 个字符代码所需的计算时间,统计从ASCII 码的32到127的范围内可视ASCII 码字符出现的频率,并在这个统计以及关于代码语言的目标信息的基础上培训神经网络。
我们的方法是将字符统计限制到C、C++、Java 和Python 代码字符库中最常用的20 个非字母数字字符。由于关注浮点运算的执行,我们打算用一种规格化因素将这20 字符统计分开来,并以此培训我们的网络。显然,一个结构上的不同是我们的网络有20 个输入节点,但这是很正常的,因为我们的描述已经暗示了这种可能性。一个更有意思的区别是出现了一对中间节点,N1 和N2,以及输出节点数量从两个变成了四个(O1 到O4)。
我们将训练 N1,当它一看到C 或C++,设置y1=1,看到Java或Python,它将设置y1=0。同理训练N2,当它看到C 或Java,设置y2=1,看到C++ 或Python,设置y2=0。此外,N1 和N2 将输出1 或0 给Oi。现在如果N1 看到C 或C++,而且N2 看到C 或者Java,那么难题中的代码是C。而如果N1 看到C 或C++,N2 看到C++ 或Python,那么代码就是C++。这个模式很显而易见。所以假设Oi 已被培训并根据下面表格的情况输出1 或0。
映射到输出(作为布尔函数)的中间节点。
如果这样可行的话,我们的网络就可以从代码示例中识别出语言了。这个想法很好。但是在实践上却有些难以置信。不过这种解决方案预示了 C/C++ 和Java/Python 输入被一个超平面切割了,同样C/Java 和C++/Python 输入被另一个切割。这是一个网络培训的解决方案,迂回地解决了这个输入空间的设想。
关于 delta 规则
另一种训练的规则叫做 delta 规则。感知器培训规则是基于这样一种思路——权系数的调整是由目标和输出的差分方程表达式决定。而delta 规则是基于梯度降落这样一种思路。这个复杂的数学概念可以举个简单的例子来表示。从给定的几点来看,向南的那条路径比向东那条更陡些。向东就像从悬崖上掉下来,但是向南就是沿着一个略微倾斜的斜坡下来,向西像登一座陡峭的山,而北边则到了平地,只要慢慢的闲逛就可以了。所以您要寻找的是到达平地的所有路径中将陡峭的总和减少到最小的路径。在权系数的调整中,神经网络将会找到一种将误差减少到最小的权系数的分配方式。
将我们的网络限制为没有隐藏节点,但是可能会有不止一个的输出节点,设 p 是一组培训中的一个元素,t(p,n) 是相应的输出节点n 的目标。但是,设y(p,n) 由以上提到的squash 函数s 决定,这里a(p,n) 是与p 相关的n 的激活函数,或者用(p,n) = s( a(p,n) ) 表示为与p 相关的节点n 的squash 过的激活函数。为网络设定权系数(每个Wi),也为每个p 和n 建立t(p,n) 与y(p,n) 的差分,这就意味着为每个p 设定了网络全部的误差。因此对于每组权系数来说有一个平均误差。但是delta 规则取决于求平均值方法的精确度以及误差。我们先不讨论细节问题,只是说一些与某些p 和n 相关的误差:?* square( t(p,n) - y(p,n) )(请参阅参考资料)。现在,对于每个Wi,平均误差定义如下:
清单 2. 查找平均误差
sum = 0
FOR p = 1 TO M: # M is number of training vectors
FOR n = 1 TO N: # N is number of output nodes
sum = sum + (1/2 * (t(p,n)-y(p,n))^2)
average = 1/M * sum
delta 规则就是依据这个误差的定义来定义的。因为误差是依据那些培训向量来说明的,delta 规则是一种获取一个特殊的权系数集以及一个特殊的向量的算法。而改变权系数将会使神经网络的误差最小化。
我们不需要讨论支持这个算法的微积分学,只要认为任何Wi 发生的变化都是如下所示就够了:alpha * s'(a(p,n)) * (t(p,n) - y(p,n)) * X(p,i,n).
X(p,i,n) 是输入到节点n 的p 中的第i 个元素,alpha 是已知的学习率。最后s'( a(p,n) ) 是与p 相关的第n 个节点激活的squashing 函数的变化(派生)率,这就是delta 规则,并且Widrow 和Stearns向我们展示了当alpha 非常小的时候,权系数向量接近某个将误差最小化的向量。用于权系数调节的基于delta 规则的算法就是如此。
梯度降落(直到误差小到适当的程度为止)
step 1: for each training vector, p, find a(p)
step 2: for each i, change Wi by:
alpha * s'(a(p,n)) * (t(p,n)-y(p,n)) * X(p,i,n)
这里有一些与感知器算法相区别的重要不同点。显然,在权系数调整的公式下有着完全不同的分析。delta 规则算法总是在权系数上调整,而且这是建立在相对输出的激活方式上。最后,这不一定适用于存在隐藏节点的网络。
反向传播
反向传播这一算法把支持 delta 规则的分析扩展到了带有隐藏节点的神经网络。为了理解这个问题,设想Bob 给Alice 讲了一个故事,然后Alice 又讲给了Ted,Ted 检查了这个事实真相,发现这个故事是错误的。现在Ted 需要找出哪些错误是Bob 造成的而哪些又归咎于Alice。当输出节点从隐藏节点获得输入,网络发现出现了误差,权系数的调整需要一个算法来找出整个误差是由多少不同的节点造成的,网络需要问,“是谁让我误入歧途?到怎样的程度?如何弥补?”这时,网络该怎么做呢?
图 3:“代码识别”反向传播的神经网络
反向传播算法同样来源于梯度降落原理,在权系数调整分析中的唯一不同是涉及到 t(p,n) 与y(p,n) 的差分。通常来说Wi 的改变在于:
alpha * s'(a(p,n)) * d(n) * X(p,i,n)
其中 d(n) 是隐藏节点n 的函数,让我们来看(1)n 对任何给出的输出节点有多大影响;(2)输出节点本身对网络整体的误差有多少影响。一方面,n 影响一个输出节点越多,n 造成网络整体的误差也越多。另一方面,如果输出节点影响网络整体的误差越少,n 对输出节点的影响也相应减少。这里d(j) 是对网络的整体误差的基值,W(n,j) 是n 对j 造成的影响,d(j) * W(n,j) 是这两种影响的总和。但是n 几乎总是影响多个输出节点,也许会影响每一个输出结点,这样,d(n) 可以表示为:
SUM(d(j)*W(n,j))
这里 j 是一个从n 获得输入的输出节点,联系起来,我们就得到了一个训练规则,第1 部分:在隐藏节点n 和输出节点j 之间权系数改变,如下所示:
alpha * s'(a(p,n))*(t(p,n) - y(p,n)) * X(p,n,j)
第 2 部分:在输入节点i 和输出节点n 之间权系数改变,如下所示:
alpha * s'(a(p,n)) * sum(d(j) * W(n,j)) * X(p,i,n)
这里每个从 n 接收输入的输出节点j 都不同。关于反向传播算法的基本情况大致如此。将 Wi 初始化为小的随机值。使误差小到适当的程度要遵循的步骤。
第 1 步:输入培训向量。
第 2 步:隐藏节点计算它们的输出
第 3 步:输出节点在第 2 步的基础上计算它们的输出。
第 4 步:计算第 3 步所得的结果和期望值之间的差。
第 5 步:把第 4 步的结果填入培训规则的第 1 部分。
第 6 步:对于每个隐藏节点 n,计算 d(n)。
第 7 步:把第 6 步的结果填入培训规则的第 2 部分。
通常把第 1 步到第3 步称为正向传播,把第4 步到第7 步称为反向传播。反向传播的名字由此而来。
识别成功
在掌握了反向传播算法后,可以来看我们的识别源代码样本语言的难题。为了解决这个问题,我们提供了 Neil Schemenauer 的Python 模型bpnn(请参阅参考资料)。用它的模型解决问题真是难以置信的简单,在我们的类NN2 里定制了一个类NN,不过我们的改变只是调整了表达方式和整个过程的输出,并没有涉及到算法。基本的代码如下所示:
清单 3:用bpnn.py 建立一个神经网络
# Create the network (number of input, hidden, and training nodes)
net = NN2(INPUTS, HIDDEN, OUTPUTS)
# create the training and testing data
trainpat = []
testpat = []
for n in xrange(TRAINSIZE+TESTSIZE):
#... add vectors to each set
# train it with some patterns
net.train(trainpat, iterations=ITERATIONS, N=LEARNRATE, M=MOMENTUM)
# test it
net.test(testpat)
# report trained weights
net.weights()
当然我们需要输入数据,实用程序 code2data.py 提供了这个功能。它的界面很直观:只要将一堆扩展名各不相同的文件放到一个子目录./code 中,然后运行这个实用程序,并列举那些扩展名作为命令选项。例如:
python code2data.py py c java
您得到的是一堆 STDOUT 上的向量,可以把这些向量输入到另一个进程或者重定向到一个文件,它的输出如下所示:
清单 4:Code2Data 的输出
0.15 0.01 0.01 0.04 0.07 0.00 0.00 0.03 0.01 0.00 0.00 0.00 0.05 0.00 > 1 0 0
0.14 0.00 0.00 0.05 0.13 0.00 0.00 0.00 0.02 0.00 0.00 0.00 0.13 0.00 > 1 0 0
[...]
让我们回忆一下输入值都是不同特殊字符出现的规格化数目,目标值(在大于号以后)是 YES/NO,它代表包含这些字符的源代码文件的类型,不过对于什么是什么来说,并没有非常明显的东西。数字可以是输入或期望的 任意值,这才是最重要的。
下一步是运行实际的code_recognizer.py 程序。这需要(在STDIN 中)像上面一样的向量集。这个程序有一个包,它能够根据实际文件推断出需要多少输入节点(计算在内的和期望的),选择隐藏节点的数目是一个诀窍。对于源代码的识别,6 到8 个隐藏节点似乎工作得很好。如果打算试验网络从而发现对于这些不同的选项它是如何做的,您可以覆盖命令行中的所有参数,但每一次运行还是会耗费一些时间。值得注意的是,code_recognizer.py 将它的(大的)测试结果文件发送到STDOUT,而将一些友好的消息放在STDERR 里。这样在大部分时间里,为了安全保管,您将会把STDOUT 定向到一个文件,并监视针对进程和结果概要的STDERR。
清单 5:运行 code_recognizer.py
> code2data.py py c java | code_recognizer.py > test_results.txt
Total bytes of py-source: 457729
Total bytes of c-source: 245197
Total bytes of java-source: 709858
Input set: ) ( _ . = ; " , ' * / { } : - 0 + 1 [ ]
HIDDEN = 8
LEARNRATE = 0.5
ITERATIONS = 1000
TRAINSIZE = 500
OUTPUTS = 3
MOMENTUM = 0.1
ERROR_CUTOFF = 0.01
TESTSIZE = 500
INPUTS = 20
error -> 95.519... 23.696... 19.727... 14.012... 11.058... 9.652...
8.858... 8.236... 7.637... 7.065... 6.398... 5.413... 4.508...
3.860... 3.523... 3.258... 3.026... 2.818... 2.631... 2.463...
2.313... 2.180... 2.065... 1.965... 1.877... 1.798... 1.725...
[...]
0.113... 0.110... 0.108... 0.106... 0.104... 0.102... 0.100...
0.098... 0.096... 0.094... 0.093... 0.091... 0.089... 0.088...
0.086... 0.085... 0.084...
Success rate against test data: 92.60%
不断减少误差是个很好的兆头,这至少在一段长时间里所获得的一种进步,且最后的结果必然是深入人心的。就我们的观点而言,网络完成了一项值得尊敬的工作,来识别代码 — 我们将会乐意倾听,对于您的数字向量它是如何做的。
总结
本文从某种程度上阐述了神经网络的基础,使您能够开始在您自己的编码过程中应用它们。我们鼓励您运用在这里学到的东西,并尝试编写您自己的对于这个难题的解决方案。(请参阅于我们的解决方案的参考资料)。
参考资料
我们的代码识别程序是基于 Neil Schemenauer 的反向传播模块。关于有指导培训和无指导培训的差异,以及神经网络的一般介绍。
请参阅由 D. Michie、D.J. Spiegelhalter 以及C.C. Taylor 编辑的Machine Learning,Neural and Statistical Classification,具体内容请参阅第6 章。
关于 Rosenblatt 的感知器结果,请参阅他的Principles of Neurodynamics, New York: Spartan Books。
关于一些 delta 规则的详细信息,请参阅Kevin Gurney 的著作An Introduction to Neural Networks 1997, London: Routledge。
也可以参阅Neural Nets 早期的在线版本。
关于 delta 规则的证明,请参阅B. Widrow 和S.D. Stearns 的Adaptive Signal Processing, 1985, New Jersey: Prentice-Hall。
关于包含图形界面的感知器的执行,请参阅 Omri Weisman 和Ziv Pollack 撰写的The Perceptron 。
如果想加入我们“计算机视觉战队”,请扫二维码加入学习群,我们一起学习进步,探索领域中更深奥更有趣的知识!