原文见:http://karpathy.github.io/neuralnets/
##神经网络骇客指南(译)
各位好,我是一名斯坦福的计算机科学的博士。作为我研究的一部分,我已经在深度学习上研究了好几年,我有几个“pet project”,其中一个是ConvNetJS - 一个用来训练神经网络的Javascript库。Javascript允许一个人轻松地将现在所发生的事情可视化,并且可以实现多样的参数选择设置,但是我仍然经常听到人们想要一些更加彻底的话题。这篇文章(我打算慢慢地写到几个章节那么长)是我一份谦逊地尝试。我把它放在网上而不是以一个PDF文件的形式呈现是因为,所有的图书都应该这样,并且最终希望它能包括一些动画和演示。
我对神经网络的个人经验是:当我抛开一切整篇、密集的反向传播方程的推导,而仅仅开始写代码时,一切都清晰多了。因此这个教程会包含非常少的数学(我不认为这是有必要的,而且有些时候会混淆一些简单的概念)。由于我的背景是计算机科学以及物理,我会以骇客的角度来看待问题。我会围绕着代码以及物理直觉而不是数学推导来展示。基本上我会以一种“我刚开始学习时希望被那样教导”的方式努力地呈现算法。
“...当我开始编写代码时一切都清晰多了。”
你可能会想急切地跳进去学习神经网络、反向传播、它们如何能应用于数据集上、等等。但是在我们到达那里之前,我想先让我们忘掉这一切。让我们后退一步,明白什么是真正的核心。让我们从元电路开始谈起。
###第一章:元电路
在我看来,思考神经网络的最佳方式是将其比作元电路。在这里,实际的值(而不是布尔值{0,1}
)沿着边沿“流动”并且在门出交汇。但是不同于门电路的与
、或
、非
等,我们的二进制门包含例如*
(乘)、+
(加)、max
或者一元门例如exp
等等。不同于基本的布尔电路,我们最终也会有gradients在同样的边沿流动,但是是向相反的方向。这已经有点超前了,我们还是先专注一下,从简单的开始。
基本情况:电路中的单门
让我们先考虑一个单一的、简单的、包含一个门的电路。示例如下:
这个电路接受两个实际值x
和y
并且在*
门中计算x * y
。 Javascript的版本会非常简单,看起来像这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43var forwardMultiplyGate = function(x, y) {
return x * y;
};
forwardMultiplyGate(-2, 3); // returns -6. Exciting.
```
以数学的形式我们可以认为这个门实现了函数:
f(x, y) = x * y
在这个例子中,我们所有的门都会接受一个或两个输入,并产生一个**单一**的输出值。
##### 目标
我们在学习时所感兴趣的问题看起来像下面这样:
1. 我们为一个已知电路提供一些具体的输入(例如 `x = -2`,`y = 3`)
2. 电路计算出一个输出值(例如 `-6`)
3. 那么问题的核心变为:我们如何轻微的改变输入以便增加输出?
在这个例子中,我们应该向什么方向改变`x,y`以便得到一个比`-6`更大的数字呢?注意到,例如`x = -1.99`以及`y = 2.99`时`x * y = -5.95` 这是一个比`-6.0`更大的数。别被它搞晕了:`-5.95`是比`-6.0`更大的。这个增量为`0.05`,尽管`-5.95`的大小(到0的距离)更小一些。
##### 策略#1:本地随机搜索
好,等一下,现在我们有一个电路,我们有一些输入并且我们希望轻微地改变它们以便增加输出?为什么这个很难?我们可以简单的“转发”电路来计算对于任何给定的`x`和`y`的输出,所以这不是很简单吗?我们为什么不随机调整`x`和`y`来跟踪效果最好的策略呢:
```js
// 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
,best_y = 2.9901
,以及best_out = -5.9588
。因为-5.9588
比-6.0
更大,所以我们就搞定了,是吗?并不是这样:如果你能负担得起时间的话,它对于仅包含几个门的微小的问题来说是一个完美的策略。但是对于接受上百万输入的大量的电路来说,并非如此。结果是,我们可以做的更好。
策略#2:数值梯度
这里就有一个更好的方法。再次记住,在我们前期我们被提供了一个电路(例如,我们的电路是一个单一的*
门)和一些特殊的输入(例如x = -2, y = 3
)。门会计算出结果(-6
),而现在我们希望对x
和y
进行微调以便得到更高的输出。
对于我们接下来要做的事情,一个很棒的直觉是这样:想象一下获取来自于门电路的输出值输出值,并且对其正向加压。正向电压会反过来通过们进行传输并且引起对输入x
和y
的推动。这个推动就告诉了我们应该如何改变x
和y
以便增加输出值。
在我们这个特定的例子中,这个推动力可能是什么样子的呢?考虑一下,我们可以凭直觉知道施加在x
上的力应该是正向的,因为使x
轻微地增大会增加电路的输出。例如,将x
从x = -2
增加到x = -1
会使我们得到-3
- 远大于-6
。在另一方面,我们会希望在y
上施加负向的力,而使它变得更小(因为更小的y
,例如从y = 3
降到y = 2
会使我们的结果更高:2 * -2 = -4
,同样比-6
更大)。但这毕竟是我们脑中的直觉。随着我们的深入,我们会了解到,我这里提到的牵引力实际上是基于输入值(x
和y
)的导数输出值。你可以已经听说过这些:
导数可以被认为是一种施加于各个输入值的力,用于使输出变得更高。
所以我们如何来精确地评价这个牵引力(导数)呢?实际上有一个非常简单的方法。我们反向地来操作:不同于增加电路的输出值,我们一个接一个地迭代每个输入值,轻微地增加它并检测输出值如何改变。输出的改变就是导数。尽管我们到现在为止还是凭借直觉。我们还是看一下数学定义。我们可以写出函数关于某个输入的导数,例如对x
的导数可以写成:
在这里,h是非常小的,这是你改变的总量。并且,如果你不太熟悉计算的话,必须要注意的是,在等式的左边,横线并不表示除法。这个符号∂f(x,y)/∂x是一个整体:函数 f(x,y) 对x的编导。等式右边的横线表示除法。我知道这非常让人迷惑,但这是一个标准符号。无论怎样,我希望这并没有太吓人,因为它的确很简单:电路被赋予一些f(x,y)的初始值,然后我们将其中的一个输入改变一个非常小的量h,并且获取新的输出f(x+h,y)。将二者相减我们能得到改变量,然后除以h我们就得到了对于任意改变量的标准量。从另一方面说,下面的代码这正反应了我上面阐述:
1 | var x = -2, y = 3; |
让我们对x
进行研究,我们将输入从x
变为x + h
,然后电路反馈