神经网络演算——多层感知器与反传递演算法
前言
由于单层感知器并没有办法处理像XOR 这样的函数。
为了提升「感知器」的能力,可以在输入与输出节点之间,再加入一些隐藏层,并透过这些隐藏层对整个学习空间进行更多次的分割,以便能处理XOR 这类难以用单一线性函数分割的问题。
但是加入了隐藏层之后,感知器的学习与训练就更复杂了,这时必须有足够的「数学理论」才能为「多层感知器」提供一个方向,而「反传递演算法」 (Back-Propagation ) 正是这样一个可以提供「多层感知器」学习方向的好东西,其数学基础则是建构在多变数微分「梯度」概念之上的一种「梯度下降法」。
事实上、反传递演算法(Back-Propagation) 的概念在1974 年就由Paul J. Werbos 所提出来了,但没有受到重视,后来在1986 年又被Rumelhart 重新发明了出来,而且受到了广泛的重视。
在本文中,我们将说明「多层感知器」与「反传递演算法」的概念,并用JavaScript 进行实现。
模型与数学原理
「多层感知器」模型包含「输入层、隐藏层与输出层」,这种多层感知器与「单层感知器」的一个明显不同点,在于拥有一个隐藏层,因此其能力增强了很多。

既然反传递演算法是一种梯度下降法,那么我们只要能计算出梯度的方向,就能让「多层感知器」的权重朝着能量下降最快的方向前进。
那么,怎么计算梯度呢?先让我们来看一张多变数的能量曲线图:

上图中,底下的平面上所画的向量,就是上面那个曲面在该点梯度的投影,指示了该平面最陡的下降方向。
在直觉概念上,曲面上某一点的梯度,其实是曲面在该点切平面的法向量,梯度的计算公式如下:
$\Delta f = \frac{\delta f}{\delta x_1}\vec{e_1} + … + \frac{\delta f}{\delta x_n}\vec{e_n}$
如果我们可以计算某函数之梯度的话,只要朝着梯度的方向走去,就是最快下降的道路了。
采用这种沿着梯度方向往下走的方法,就称为「梯度下降法」(Gradient Descent),这种方法可以说是一种「贪婪演算法」(Greedy Algorithm),因为它每次都朝着最斜的方向走去,企图得到最大的下降幅度。
为了要计算梯度,我们不能采用「单层感知器里的那种不可微分的sign() 梯度函数」 (如下图a所示),因为这样就不能用微积分的方式计算出梯度了,而必须改用可以微分的连续函数sigmoid() (如下图b 所示),这样才能够透过微分计算出梯度。

当我们改成可微分的sigmoid() 函数之后,就可以运用微积分公式,事先求出其微分函数dsigmoid() 。我这里了双曲正切函数tanh(x) 作为sigmoid() 函数,其定义如下所示:
$sinhx = \frac{e^x – e^{-x}}{2}$
$coshx = \frac{e^x + e^{-x}}{2}$
$tanhx = \frac{sinhx}{coshx}$
由于tanh(x)的微分是$1 – x^2$,因此在下列这段程序中,我们定义这些函数的计算方式:
var tanh=function(x) {
return (Math.exp(x) - Math.exp(-x)) / (Math.exp(x) + Math.exp(-x));
}
function sigmoid(x) {
return tanh(x); // 表现较好
}
function dsigmoid(x) {
return 1.0 - x*x;
}
上述程序中dsigmoid(y)中的1.0 – x*x则是y=tanh(x)的微分式,对每个y=tanh(x)都取微分式的时候,其实就是梯度的方向。
(node:我看到国内有些例子会采用作为sigmoid()函数,这与tanh(x)函数的形状非常类似,也是一种可行的方法)。
var log = console.log;
// 建立大小为 n 的阵列并填入初始值 fill
var makeArray=function(n, fill) {
var a = [];
for (var i=0; i<n; i++)
a.push(fill);
return a;
}
// 建立大小为 I*J 的矩阵并填入初始值 fill
var makeMatrix=function(I, J, fill) {
var m = [];
for (var i=0; i<I; i++)
m.push(makeArray(J, fill));
return m;
}
// numbersToStr():以精度为 precision 个小数來输出阵列 array
var numbersToStr=function(array, precision) {
var rzStr = "";
for (var i=0; i<array.length; i++) {
if (array[i]>=0)
rzStr+=" "+array[i].toFixed(precision)+" ";
else
rzStr+=array[i].toFixed(precision)+" ";
}
return rzStr;
}
// rand():获取 a 到 b 之间的一个随机数
var rand=function(a, b) {
return (b-a)*Math.random() + a;
}
// sigmoid(x)=tanh(x)
function sigmoid(x) {
var tanh = (Math.exp(x) - Math.exp(-x)) / (Math.exp(x) + Math.exp(-x));
return tanh; // 双曲正切函数
}
// dsigmoid(x)=1-x^2;
function dsigmoid(x) {
return 1.0 - x*x;
}
function NeuralNet() {
// init():设定网络结构与权重的随机初始值的函数。
this.init=function(ni, nh, no) {
// number of input, hidden, and output nodes
this.ni = ni + 1; // +1 for bias node
this.nh = nh;
this.no = no;
// 建立各层的节点阵列
this.ai = makeArray(this.ni, 1.0);
this.ah = makeArray(this.nh, 1.0);
this.ao = makeArray(this.no, 1.0);
// 建立权重矩阵
this.wi = makeMatrix(this.ni, this.nh, 0.0);
this.wo = makeMatrix(this.nh, this.no, 0.0);
// 随机设定权重初始值。
for (var i=0; i<this.ni; i++)
for (var j=0; j<this.nh; j++)
this.wi[i][j] = rand(-0.2, 0.2);
for (var j=0; j<this.nh; j++)
for (var k=0; k<this.no; k++)
this.wo[j][k] = rand(-2.0, 2.0);
// 上一次的改变量矩阵,用来当动量以便爬过肩型区域。
this.ci = makeMatrix(this.ni, this.nh, 0.0);
this.co = makeMatrix(this.nh, this.no, 0.0);
return this;
}
// 计算网络的输出的函数
this.update=function(inputs) {
// 设定输入值
for (var i=0; i<this.ni-1; i++)
this.ai[i] = inputs[i];
// 计算隐藏层输出值 ah[j]
for (var j=0; j<this.nh; j++) {
var sum = 0.0;
for (var i=0; i<this.ni; i++)
sum = sum + this.ai[i] * this.wi[i][j];
this.ah[j] = sigmoid(sum);
}
// 计算输出层输出值 ao[k]
for (var k=0; k<this.no; k++) {
var sum = 0.0;
for (var j=0; j<this.nh; j++)
sum = sum + this.ah[j] * this.wo[j][k];
this.ao[k] = sigmoid(sum);
}
return this.ao; // 传回输出层输出值 ao
}
// 反传递学习的函数 (重要)
this.backPropagate = function(targets, rate, moment) {
// calculate error terms for output : 计算输出层误差
var output_deltas = makeArray(this.no, 0.0);
for (var k=0; k<this.no; k++) {
var error = targets[k]-this.ao[k];
output_deltas[k] = dsigmoid(this.ao[k]) * error;
}
// 计算隐藏层误差
var hidden_deltas = makeArray(this.nh, 0.0);
for (var j=0; j<this.nh; j++) {
var error = 0.0;
for (var k=0; k<this.no; k++) {
// 注意、在此输出层误差 output_deltas 会反传递到隐藏层,因此才称为反传递算法。
error = error + output_deltas[k]*this.wo[j][k];
}
hidden_deltas[j] = dsigmoid(this.ah[j]) * error;
}
// 更新输出层权重
for (var j=0; j<this.nh; j++) {
for (var k=0; k<this.no; k++) {
var change = output_deltas[k]*this.ah[j];
this.wo[j][k] = this.wo[j][k] + rate*change + moment*this.co[j][k];
this.co[j][k] = change;
// print N*change, M*this.co[j][k]
}
}
// 更新输入层权重
for (var i=0; i<this.ni; i++) {
for (var j=0; j<this.nh; j++) {
var change = hidden_deltas[j]*this.ai[i];
this.wi[i][j] = this.wi[i][j] + rate*change + moment*this.ci[i][j];
this.ci[i][j] = change;
}
}
// 计算输出层误差总合
var error = 0.0;
for (var k=0; k<targets.length; k++)
error = error + 0.5*Math.pow(targets[k]-this.ao[k],2);
return error;
}
// 对真值中 (训练样本) 中的每个输入都打印出“网络输出”与“期望输出”,以便观察学习结果是否都正确。
this.test = function(patterns) {
for (var p in patterns) {
var inputs = patterns[p][0];
var outputs= patterns[p][1];
log("%j -> [%s] [%s]", inputs, numbersToStr(this.update(inputs), 0), numbersToStr(outputs, 0));
// this.dump();
}
}
// 主要学习函数,反复呼叫反传递算法
// 参数:rate: learning rate (学习速率), moment: momentum factor (动量常数)
this.train=function(patterns, iterations, rate, moment) {
for (var i=0; i<iterations; i++) {
var error = 0.0;
for (var p in patterns) {
var pat=patterns[p];
var inputs = pat[0];
var targets = pat[1];
var outputs = this.update(inputs);
error = error + this.backPropagate(targets, rate, moment);
}
if (i % 100 == 0)
log('%d:error %j', i, error);
}
}
}
module.exports = NeuralNet; // 到处 NeuralNet。
1. 学习XOR 函数
var NN = require("./prop");
pat = [
[[0,0], [0]],
[[0,1], [1]],
[[1,0], [1]],
[[1,1], [0]]
];
nn = new NN().init(2, 2, 1);
nn.train(pat, 1000, 0.5, 0.1);
nn.test(pat);
执行结果:
0:error 1.1411586806597014
100:error 0.15669092345306487
200:error 0.0044566959936791035
300:error 0.0018489705409186357
400:error 0.0011477205633429219
500:error 0.0008277968129286529
600:error 0.0006456614467953627
700:error 0.005231441443909679
800:error 0.0004595906757934737
900:error 0.0003945408066808508
[0,0] -> [ 0 ] [ 0 ]
[0,1] -> [ 1 ] [ 1 ]
[1,0] -> [ 1 ] [ 1 ]
[1,1] -> [-0 ] [ 0 ]
2. 学习七段显示函数
/* 七段显示排列
A
F B
G
E C
D
*/
var NN = require("./prop");
pat = [
// A B C D E F G
[[1,1,1,1,1,1,0], [0,0,0,0]], // 0
[[0,1,1,0,0,0,0], [0,0,0,1]], // 1
[[1,1,0,1,1,0,1], [0,0,1,0]], // 2
[[1,1,1,1,0,0,1], [0,0,1,1]], // 3
[[0,1,1,0,0,1,1], [0,1,0,0]], // 4
[[1,0,1,1,0,1,1], [0,1,0,1]], // 5
[[1,0,1,1,1,1,1], [0,1,1,0]], // 6
[[1,1,1,0,0,0,0], [0,1,1,1]], // 7
[[1,1,1,1,1,1,1], [1,0,0,0]], // 8
[[1,1,1,1,0,1,1], [1,0,0,1]] // 9
];
nn = new NN().init(7, 5, 4);
nn.train(pat, 10000, 0.2, 0.01);
nn.test(pat);
执行结果:
0:error 21.80370718175807
100:error 3.0996784544877736
200:error 2.9554663137424373
300:error 2.9322332121195545
400:error 0.9175505320368402
500:error 0.5911840202045504
600:error 0.6702566860375645
700:error 0.6175745429758741
800:error 0.6073471516556047
900:error 0.601200049561361
1000:error 0.5810463514787689
1100:error 0.5364677212922591
1200:error 0.532025286869445
1300:error 0.46666848524996085
1400:error 0.48129628693742754
1500:error 0.8155362088747744
1600:error 0.5829386518767099
1700:error 0.6944742612114545
1800:error 0.49717362214697597
1900:error 0.40957109669176334
2000:error 0.5388564563993076
2100:error 0.3703582901903478
2200:error 0.5178647638260341
2300:error 0.1764373289120007
2400:error 0.25347246319196093
2500:error 0.33310966813566406
2600:error 0.17106878914718923
2700:error 0.1365002209754472
2800:error 0.1594051132697459
2900:error 0.3070991793860354
3000:error 0.3766039636947747
3100:error 0.3555367190225767
3200:error 0.11555541960454409
3300:error 0.11367500949340971
3400:error 0.12234128181753154
3500:error 0.1675446667610037
3600:error 0.09044262748000728
3700:error 0.08628776394501735
3800:error 0.27906234926518514
3900:error 0.04818459875532369
4000:error 0.062418918530088664
4100:error 0.2804289611800696
4200:error 0.13725495522690973
4300:error 0.12719742994691247
4400:error 0.07177660395615833
4500:error 0.08548411758763816
4600:error 0.03974217740792855
4700:error 0.09595126476746213
4800:error 0.03853494372617759
4900:error 0.06360901767700806
5000:error 0.07246959735102428
5100:error 0.05362418748287888
5200:error 0.04669033343340621
5300:error 0.03270696475959521
5400:error 0.03940008954106113
5500:error 0.047208537352753516
5600:error 0.049368429554604215
5700:error 0.042625347453785954
5800:error 0.056241589618292134
5900:error 0.016798400589135128
6000:error 0.03404851177897533
6100:error 0.028972975396903942
6200:error 0.01572555942490573
6300:error 0.048110746037786964
6400:error 0.039118552165591194
6500:error 0.03954060666366999
6600:error 0.047240563507126423
6700:error 0.013729342899560402
6800:error 0.03734015049471263
6900:error 0.04385222818693631
7000:error 0.038098235270263764
7100:error 0.014325393180305138
7200:error 0.039093361005808284
7300:error 0.011914229228792664
7400:error 0.012490068609142688
7500:error 0.010110888778014877
7600:error 0.017266400583083073
7700:error 0.037972260655506615
7800:error 0.010317947862704183
7900:error 0.02181165885044425
8000:error 0.033354842242808616
8100:error 0.033244707069915634
8200:error 0.02269772865101642
8300:error 0.008219315372175379
8400:error 0.03342460798252796
8500:error 0.008080093519395289
8600:error 0.02466937317542233
8700:error 0.03307092886686206
8800:error 0.033433889409569414
8900:error 0.031423007039930506
9000:error 0.018154152094468162
9100:error 0.008635680953338276
9200:error 0.030890671102892397
9300:error 0.009020762345545542
9400:error 0.015823853695083934
9500:error 0.029353956299920176
9600:error 0.03028116871034789
9700:error 0.03009059907189612
9800:error 0.025996249652393937
9900:error 0.009595759182954272
[1,1,1,1,1,1,0] -> [ 0 0 -0 -0 ] [ 0 0 0 0 ]
[0,1,1,0,0,0,0] -> [ 0 -0 -0 1 ] [ 0 0 0 1 ]
[1,1,0,1,1,0,1] -> [-0 -0 1 0 ] [ 0 0 1 0 ]
[1,1,1,1,0,0,1] -> [-0 0 1 1 ] [ 0 0 1 1 ]
[0,1,1,0,0,1,1] -> [ 0 1 -0 0 ] [ 0 1 0 0 ]
[1,0,1,1,0,1,1] -> [-0 1 -0 1 ] [ 0 1 0 1 ]
[1,0,1,1,1,1,1] -> [-0 1 1 0 ] [ 0 1 1 0 ]
[1,1,1,0,0,0,0] -> [-0 1 1 1 ] [ 0 1 1 1 ]
[1,1,1,1,1,1,1] -> [ 1 -0 -0 0 ] [ 1 0 0 0 ]
[1,1,1,1,0,1,1] -> [ 1 0 -0 1 ] [ 1 0 0 1 ]
结语
可以看到这两个训练结果都是完全正确的,这代表反传递演算法可以让多层感知器学会XOR 与七段显示器的函数。
当然、多层感知器也可以学会更难的问题,像是手写的数字与英文字母辨认等等,手写中文字体辨认和语音辨认当然也是可行的,只不过需要更多的训练实例与节点,学习的效果才能更好。
code enjoy! 🙂
作者:indeex
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。