Press "Enter" to skip to content

神经网络 XOR JS 实现

简要

 

前端神经网络初学者,捣鼓 XOR 逻辑运算,之前自己很多时候,都在看文章或在做很浮于表面的 Demo,没有很细节地实践过这幺底层的逻辑和相关思考,勿喷~

 

背景

 

来自 @夕山 大佬的灵感,看到 《神经网络的演进史》 以及对逻辑运算的训练过程, 神经网络的程序入门(Js 版) ,代码如下:

 

var log = console.log;
var Perceptron = function() { // 感知器
  this.step=function(x, w) { // 步阶函数:计算目前权重 w 的情况下,网路的输出值为 0 或 1
    var result = w[0]*x[0]+w[1]*x[1]+w[2]*x[2]; // y=w0*x0+x1*w1+x2*w2=-theta+x1*w1+x2*w2
    if (result >= 0) // 如果结果大于零
      return 1;      //   就输出 1
    else             // 否则
      return 0;      //   就输出 0
  }
  this.training=function(truthTable) { // 训练函数 training(truthTable), 其中 truthTable 是目标真值表
    var rate = 0.01; // 学习调整速率,也就是 alpha
    var w = [ 1, 0, 0 ]; 
    for (var loop=0; loop<1000; loop++) { // 最多训练一千轮
      var eSum = 0.0;
      for (var i=0; i<truthTable.length; i++) { // 每轮对于真值表中的每个输入输出配对,都训练一次。
        var x = [ -1, truthTable[i][0], truthTable[i][1] ]; // 输入: x
        var yd = truthTable[i][2];       // 期望的输出 yd
        var y = this.step(x, w);  // 目前的输出 y
        var e = yd - y;                  // 差距 e = 期望的输出 yd - 目前的输出 y
        eSum += e*e;                     // 计算差距总和
        var dw = [ 0, 0, 0 ];            // 权重调整的幅度 dw
        dw[0] = rate * x[0] * e; w[0] += dw[0]; // w[0] 的调整幅度为 dw[0]
        dw[1] = rate * x[1] * e; w[1] += dw[1]; // w[1] 的调整幅度为 dw[1]
        dw[2] = rate * x[2] * e; w[2] += dw[2]; // w[2] 的调整幅度为 dw[2]
        if (loop % 100 == 0)
          log("%d:x=(%s,%s,%s) w=(%s,%s,%s) y=%s yd=%s e=%s", loop, 
               x[0].toFixed(3), x[1].toFixed(3), x[2].toFixed(3), 
               w[0].toFixed(3), w[1].toFixed(3), w[2].toFixed(3), 
               y.toFixed(3), yd.toFixed(3), e.toFixed(3));
      }
      if (Math.abs(eSum) < 0.0001) return w; // 当训练结果误差够小时,就完成训练了。
    }
    return null; // 否则,就传会 null 代表训练失败。
  }
}
function learn(tableName, truthTable) { // 学习主程式:输入为目标真值表 truthTable 与其名称 tableName。
  log("================== 学习 %s 函数 ====================", tableName);
  var p = new Perceptron();       // 建立感知器物件
  var w = p.training(truthTable); // 训练感知器
  if (w != null)                  // 显示训练结果
    log("学习成功 !");
  else
    log("学习失败 !");
  log("w=%j", w);
}
var andTable = [ [ 0, 0, 0 ], [ 0, 1, 0 ], [ 1, 0, 0 ], [ 1, 1, 1 ] ]; // AND 函数的真值表
var orTable  = [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ] ]; // OR  函数的真值表
var xorTable = [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 0 ] ]; // XOR 函数的真值表
learn("and", andTable); // 学习 AND 函数
learn("or",  orTable);  // 学习 OR  函数
learn("xor", xorTable); // 学习 XOR 函数

 

背后求出的 weight 其实就是对上述真值表的线性划分(盗图自 @夕山 大佬 :joy:):

 

 

以上的单层感知器,能成功学习 AND 和 OR 函数,但是 XOR 就很无能为力,看到这里,觉得很意犹未尽。

 

因为之前也捣鼓过 DeepLearning 的手写识别 相关的 Demo,所以想决定加个隐藏层来实现 xor 操作,从理论上讲,加一层隐藏 Layer 之后,将原有 XOR 矩阵做一次转换映射之后,便能做进一步的线性划分了。类似以下:

 

 

我们通过加一层 2 维空间隐藏层,将 XOR 映射为上图中的第二个真值表,于是我们就能愉快地分割了 :joy:

 

XOR 训练 JS 代码

 

var log = console.log;
var random = () => {
  return +(Math.random() * 2 - 1).toFixed(4);
};
class Perceptron {
  constructor({ truthTable, tableName, w1, w2 }) {
    this.tableName = tableName;
    this.rate = 0.001;
    this.loopTimes = 1000000;
    this.w1 = w1 || [[ random(), random(), random()], [random(), random(), random()]]
    this.w2 = w2 || [ random(), random(), random() ];
    this.truthTable = truthTable;
  }
  // 用于校验结果
  verify(showlog) {
    const { truthTable, tableName, w1, w2 } = this;
    for (var i=0; i<truthTable.length; i++) {
      var x1 = [ truthTable[i][0], truthTable[i][1], 1 ];
      var x2 = [ this.step(x1, w1[0]), this.step(x1, w1[1]), 1];
      var y = this.step(x2, w2);
      if (showlog) {
        console.log(truthTable[i][0] + ' ' + tableName + ' ' + truthTable[i][1] + ' = ', y, '(', x1, ',', x2, ')' );
      }
      if (y !== truthTable[i][2]) return false;
    }
    return true;
  }
  step(x, w) {
    // 正向求和
    var result = w[0]*x[0]+w[1]*x[1]+w[2]*x[2];
    // 激活函数
    if (result >= 0) return 1;
    else return 0;
  }
  training() { // 训练函数 training(truthTable), 其中 truthTable 是目标真值表
    const { rate, w1, w2, truthTable, loopTimes } = this;
    for (var loop=0; loop < loopTimes; loop++) { // 训练次数
      var e = 0;
      // 每轮对于真值表中的每个输入输出配对,都训练一次。
      for (var i=0; i < truthTable.length; i++) {
        // 一层输入值 + basis
        var x1 = [ truthTable[i][0], truthTable[i][1], 1 ];
        // 期望值
        var yd = truthTable[i][2];
        // 隐藏层
        var x2 = [ this.step(x1, w1[0]), this.step(x1, w1[1]), 1];
        // 输出结果层
        var y = this.step(x2, w2);
        // 预期差值
        e = yd - y;
        // 反向传播第一层
        // 这里的链式求导,还是模糊的状态,为什幺是这种形式?
        w2[0] += rate * x2[0] * e;
        w2[1] += rate * x2[1] * e;
        w2[2] += rate * x2[2] * e;

        // 反向传播第二层
        w1[0][0] += rate * x1[0] * e * w2[0];
        w1[0][1] += rate * x1[0] * e * w2[1];
        w1[0][2] += rate * x1[0] * e * w2[2];
        w1[1][0] += rate * x1[1] * e * w2[0];
        w1[1][1] += rate * x1[1] * e * w2[1];
        w1[1][2] += rate * x1[1] * e * w2[2];
      }
      // 校验通过,则停止训练
      if (this.verify()) {
        return {
          loop, w1, w2
        };
      }
    }
    // 训练失败
    return null;
  }
}
function learn(tableName, truthTable, w1, w2) {
  // 建立感知器物件
  var p = new Perceptron({
    truthTable,
    tableName,
    w1,
    w2,
  });
  var result = p.training(); // 开始训练
  if (result != null) {
    log(" 学习 %s 函数成功:", tableName);
    log("loop=", result.loop, ' w1: ', JSON.stringify(result.w1), ' w2: ', JSON.stringify(result.w2));
    p.verify(true);
  } else {
    log(" 学习 %s 函数失败", tableName);
  }
}
var andTable = [ [ 0, 0, 0 ], [ 0, 1, 0 ], [ 1, 0, 0 ], [ 1, 1, 1 ] ]; // AND 函数的真值表
var orTable  = [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 1 ] ]; // OR  函数的真值表
var xorTable = [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 0 ] ]; // XOR 函数的真值表

// 因为失败概览较高,所以,我们尝试多次学习
for (var i = 0; i< 10; i++) {
  console.log(`======== 第 ${i} 次学习 ========`)
  // 通过预训练,输入初始状态,加快训练
  // learn("and", andTable, [[0.5, 0, 0], [0, 0, 0]], [0.5, 0, -0.5]);
  learn("and",  andTable);
  learn("or",  orTable);
  learn("xor", xorTable);
  console.log('\n')
}

 

结果输出:

 

======== 第 5 次学习 ========
 学习 and 函数成功:
loop= 341730  w1:  [[0.39963790000397476,33.63402299979549,-0.9887426000058843],[-0.20303059999641215,0.41553599999999374,-0.41543859999487487]]  w2:  [0.0009000000000003372,-0.0010000000000000763,-0.0005999999999999929]
0 and 0 =  0 ( [ 0, 0, 1 ] , [ 0, 0, 1 ] )
0 and 1 =  0 ( [ 0, 1, 1 ] , [ 1, 1, 1 ] )
1 and 0 =  0 ( [ 1, 0, 1 ] , [ 0, 0, 1 ] )
1 and 1 =  1 ( [ 1, 1, 1 ] , [ 1, 0, 1 ] )
 学习 or 函数成功:
loop= 244  w1:  [[0.5519268,-0.06474380000000002,-0.06840439999999995],[-0.49513199999999985,-1.0470319999999997,0.558002]]  w2:  [0.39760000000000023,-0.5260999999999998,0.13020000000000032]
0 or 0 =  0 ( [ 0, 0, 1 ] , [ 0, 1, 1 ] )
0 or 1 =  1 ( [ 0, 1, 1 ] , [ 0, 0, 1 ] )
1 or 0 =  1 ( [ 1, 0, 1 ] , [ 1, 1, 1 ] )
1 or 1 =  1 ( [ 1, 1, 1 ] , [ 1, 0, 1 ] )
 学习 xor 函数成功:
loop= 696460  w1:  [[-0.7513457000000062,0.35677259999999433,0.39457490000601025],[-36.21090990031919,35.48711019926247,-0.06928969999966195]]  w2:  [-0.000299999999999751,0.001400000000000198,0.00010000000000053716]
0 xor 0 =  0 ( [ 0, 0, 1 ] , [ 1, 0, 1 ] )
0 xor 1 =  1 ( [ 0, 1, 1 ] , [ 1, 1, 1 ] )
1 xor 0 =  1 ( [ 1, 0, 1 ] , [ 0, 0, 1 ] )
1 xor 1 =  0 ( [ 1, 1, 1 ] , [ 1, 0, 1 ] )

 

结论

 

不确定反向传播的链式求导对了没(抄@夕山 大佬的形式直接推的第二层),不过,学习是能成功,不过有一定概率。我们也可以清晰地看到 xor 的第二层映射层,将一层的输入打散后,我们便能轻松的分割~

 

希望借此最简单的神经网络来更加深刻地理解背后的原理和推导过程。

 

参考

《神经网络的演进史》
神经网络的程序入门(Js 版)

Deep Learning 之 Hello World Demo 自己写的在线识别手写数字

“反向传播算法”过程及公式推导
JS 加法器模拟 XOR 如此重要的原因
JS 加法器模拟 XOR 如此重要的原因

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注