神经网络演算——多层感知器与反传递演算法

内容纲要

前言

由于单层感知器并没有办法处理像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

链接:http://indeex.cc

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


发表评论

您的电子邮箱地址不会被公开。