程序员视角学神经网络(二)

第一章 实值电路

  在我看来,思考神经网络的最佳方式是将其当做实值电路,在实值电路里,实数数值(实值电路对应于传统的01电路,后续以布林电路称呼)会沿着边“流动”,并且在门里相互作用而产生输出。但是,我们此处用的门不是AND, OR, NOT等,而是二元门,如+*max等。不同于普通布林电路,我们这里的电路上,还会有梯度信息流动在相同的边上,但是和实数数值的流动方向是相反。下面我们从最简单的开始。

基本用例:电路中有一个门

 如下图:

 

    该电路有两个实数输入,分别为xy,使用乘法门计算x*yJavascript版本的源码如下所示:

var forwardMultiplyGate = function(x, y) {

  return x * y;

};

forwardMultiplyGate(-2, 3); // returns -6. Exciting.

在数学形式上,我们可以认为这个门实现了一个实数函数:


和这个例子一样,我们所有的门会有一个或两个输入,并产生一个输出。

目标

我们在研究中感兴趣的问题细化如下:

1. 为一个给定电路提供特定输入值,如x = -2, y = 3

2. 电路计算出输出值,如前所述电路,结果为-6

3. 基于上,核心问题可表达为:应该如何微调输入,才能使输出变大?

在当前这个例子中,我们应该将xy的值向什么方向调整才能使输出的结果比-6大呢? 例如,调整 x = -1.99, y = 2.99, 输出结果为-5.95,比-6.0大。(这里不要困惑:-5.95-6.0大,输出值提升了0.05。)

策略#1 随机局部搜索

我们当前的电路,有两个输入,一个输出,想通过微调输入来让输出值变大?很难吗?我们可以很方便地让电路计算任意给定的xy,这不是很正常吗?为什么我们不能随机地微调xy,并跟踪微调效果最好的组合呢?上代码:

// circuit with single gate for now

var forwardMultiplyGate = function(x, y) { return x * y; };

var x = -2, y = 3; // some input values

 

// try changing x,y randomly small amounts and keep track of what works best

var tweak_amount = 0.01;

var best_out = -Infinity;

var best_x = x, best_y = y;

for(var k = 0; k < 100; k++) {

  var x_try = x + tweak_amount * (Math.random() * 2 - 1); // tweak x a bit

  var y_try = y + tweak_amount * (Math.random() * 2 - 1); // tweak y a bit

  var out = forwardMultiplyGate(x_try, y_try);

  if(out > best_out) {

    // best improvement yet! Keep track of the x and y

    best_out = out; 

    best_x = x_try, best_y = y_try;

  }

}

运行上述代码,得到best_x = -1.9928, bets_y = 2.9901, 最优输出 best_out = -5.9588. 搞定了? No:对于这种很小的问题来说,如果你能够忍受计算时间,算是个还可以的策略,但是,如果考虑到百万级输入的大规模电路,这个策略显然是不行的。

策略#2 数值梯度

我们来看一种更好的方式。在我们的电路中,门负责计算输出,我们的目的是希望微调输入xy,使得输出变大。

想象一下,电路的输出和输入是联动的,我们手动调节输出,对输出施加正向的拉力,这个正向的拉力随之通过门电路传导到输入,从而对输入产生作用力,使输入的值被动发生变化,这种变化恰好满足输出值的变化。通过反向传递过来的作用力,可以告诉我们xy应该如何变化,才能够使输出值变大。

在我们的例子中,这些力应该像什么样子呢?直观来看,我们能够想到作用在x上的力应该是正向的,因为让x稍微变大,就能够提升输出值,举例说明:将x-2调整为-1,输出变为-3,远比-6大了,另一方面,我们期望对y施加负向的力,使它变得更小,例如y3调整为2,会使输出更大:2 x -2 = -4,也是比-6大的。后续会发现这里描述的力,事实上,是输出对输入的导数。这个名词您可能已经听说过。

 

在这里,当我们对输出施加正向拉力,使其变大时,会有反向作用到每个输入上的力,这个力,我们就可以认为是输出对某个输入的导数。

 

那么,我们如何精确地评估这个力(导数)呢?下面介绍一个非常简单的处理流程。我们和前述流程做相反的事情:不是对电路的输出施加正向拉力,而是对输入分别一个个地进行微调增加,看看输出值发生了什么改变,对于每次输入的微调,输出的改变量就是输出对输入的导数。例如,对x的导数,计算方式如下:


其中,h很小——它是x微调的值,如果您不熟悉微积分的话,要特别注意的是上述等式左侧的横线,不代表除,是一个完整的表达:x的导数,等式右边的横线是除。

对于初始输入x,y,电路会给出初始的输出值,然后,我们对其中的一个输入做微调,调整幅度为h,从而得到新的输出,这两个输出值相减告诉我们输出的变化值,而用输出的变化值除以h,只是归一化了这个由我们对输入微调所引起的输出变化。这段话精确地表达了我前面面所描述的,直接翻译为代码,如下:

var x = -2, y = 3;

var out = forwardMultiplyGate(x, y); // -6

var h = 0.0001;

// 计算x的导数

var xph = x + h; // -1.9999

var out2 = forwardMultiplyGate(xph, y); // -5.9997

var x_derivative = (out2 - out) / h; // 3.0

// 计算y的导数

var yph = y + h; // 3.0001

var out3 = forwardMultiplyGate(x, yph); // -6.0002

var y_derivative = (out3 - out) / h; // -2.0

我们先看看例子中的x,我们将x调整为x+h,电路的输出相应地发生改变,输出一个更大的值(-5.9997 > -6),除以h是为了归一化电路对我们微调输入引起的输出变化。严格来说,你会希望h的值趋于无穷小(数学中对梯度的定义是h无限趋近于0),但是在实际操作过程中,h = 0.00001在多数情况下都能够得到很好的近似值。现在,我们看到x的导数为+3,我在数字特地加了一个正号,因为它表明对x的微调是正向的,则电路的输出会变的更大,实际的微调变化值为3,这个值可以解释为正向微调的力的绝对大小。

 

输出对某个输入的导数,我们可以通过微调输入并观察由此引起的输出的变化,来计算得到。

顺便说下,我们常常说对于某个输入的导数,或者是对于所有输入的梯度,梯度其实就是所有输入的导数组合成的一个向量(也就是一个列表)。很重要的是,如果我们让对输入微调的拉力是沿着梯度的方向进行(也就是,我们仅对每个输入加上导数),我们可以看到输出值会变大,代码如下:

var step_size = 0.01;

var out = forwardMultiplyGate(x, y); // before: -6

x = x + step_size * x_derivative; // x becomes -1.97

y = y + step_size * y_derivative; // y becomes 2.98

var out_new = forwardMultiplyGate(x, y); // -5.87! exciting.

如前所料,我们通过梯度改变输入,电路的输出变得更大,这个比随机修改xy要简单多了吧?事实上,如果你学过微积分,你是能够证明梯度实际上是函数增加(变化)最陡的方向,所以,我们没有必要使用策略1,像猴子一样跳来跳去地找随机数。评估梯度,需要仅仅三次电路的前向传递计算来评估【此处原文不甚理解为何说三次】,而不是策略1中的上百次,就可以得到期望的局部最优拉力(假设我们的目标是最大化电路输出)。

步长越大并不意味着效果越好。这一点需要澄清一下,在这个非常简化的例子里,需要注意,使用比0.01更大的步长step_size,得到的效果都是正向的,例如,step_size = 1.0,则输出为-1,而且,实际上步长趋于无限大时,输出也是无限大的。这里需要明确的很重要的事情是,一旦我们的电路变得非常复杂(例如,整个神经网络),整个函数从输入到输出将会非常复杂。梯度则保证了如果你使用非常小的步长,沿着梯度的方向必然能够获得更大的输出,但是,如果步长偏大,则获得更大输出的概率会打一定折扣(不再是必然,而是可能)。我们的例子里,使用更大的步长没有问题,是因为这里的函数比较平滑导致的。

爬山类比。我们电路的输出值就像是一座山的高度,同时,我们被蒙上双眼摸索着要向上爬。我们能够感觉到脚下的陡峭程度(即梯度),因此,当我们拖着脚一点点往前走,我们必然能够感觉到陡峭程度,也能保证在不断向上爬,但是,如果步幅加大,我们可能会刚好踩进一个坑里。

非常好,我希望我已经让你明白数值梯度在评估过程中,是非常有用的,并且效率很高。但是,貌似我们还可以做的更好。

策略#3 解析梯度

在上一部分,我们通过探测电路的输出值来求取每个输入的梯度(导数),这种方法我们称之为数值梯度。数值梯度的方法存在一个问题,当我们微调每一个输入值时,都需要计算一次输出值,这样的话计算量会随着输入的增加呈线性增长,而在实际应用中,经常会有成千上万个输入,电路也不仅仅是简单的乘法门,而是有着相当复杂度的表达式,这意味着将会有更高的计算量,所以,我们需要更好的方法来解决。

幸运的是,有一种更简单,更快捷的方法来计算梯度:我们可以使用微积分的方法来推导出一个表达式,和计算输出值一样简单,这种方法称为解析梯度,在这里,不再需要微调任何数值了。你可能看过其他人在教授神经网络时,推导大量的梯度公式,平心而论,这些公式说是唬人且让人难以理解的(如果你数学不是太好的话)。但是,其实这种方式大可不必,我写过大量的神经网络代码,里面的数学推导代码不会多于两行,而且95%的情况下,是不需要写任何推导的。这是因为,我们仅需要推导非常小且简单的表达式的梯度。我会向你展示如何运用链式法则来把这些非常简单的推导组合起来来计算全部变量的梯度。

解析梯度不需要微调输入,仅通过数学方式(微积分)就能够推导出来。

如果你记得乘法规则,幂规则,除法规则等等,那就很容易推导出一个很小的表达式如x*y的变量xy的导数了。好吧,假设你不记得这些微积分规则,我们从头开始。举个栗子,这里给出x的导数表达式:


(我没有把h写成h趋于0,原谅我把,数学系的同志们~)。好了,现在把函数体代入上述表达式(f(x,y) = xy),你准备好阅读全文最难的数学内容了吗^_^?代入后:


结果很有趣,关于x的导数刚好等于y,你注意到上一节的巧合了吗?我们微调xx+h,得到x_derivative = 3.0,正好等于y的值,通过解析梯度的公式说明它并非巧合。同样的,y的导数为x。如此,就不再需要微调去计算导数了。我们使用了强大的数学方法,现在可以将导数的计算结果放入代码了:

var x = -2, y = 3;
var out = forwardMultiplyGate(x, y); // before: -6
var x_gradient = y; // by our complex mathematical derivation above
var y_gradient = x;
var step_size = 0.01;
x += step_size * x_gradient; // -1.97
y += step_size * y_gradient; // 2.98
var out_new = forwardMultiplyGate(x, y); // -5.87. Higher output! Nice.

够简单吧!

回顾一下我们学到的:

  • l 输入:给定一个电路,一些输入,能够计算一个输出
  • l 输出:我们感兴趣的是寻找对每个输入的微调值,这种微调能够使得输出变得更大
  • 策略1:随机寻找
  • 策略2:通过计算梯度的方式,但是每次都要在微调输入后,侦测输出的变化情况
  • 策略3:最终方案,我们通过解析梯度的数学方法得到梯度值,和数值梯度的原理一样,但是效率更高,也不需要微调输入

顺便说一下,在实际应用中,所有的神经网络库都会计算解析梯度,但是会通过和数值梯度进行比较来对计算结果进行校对,这是因为数值梯度容易计算(但是计算复杂度高),而解析梯度则会经常隐藏有bug,即便如此,瑕不掩瑜,它的效率很高。接下来,我们将会看到,计算梯度的耗时和前向运算量是差不多的。

(基本用例部分结束)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值