关于 Hacker的神经网络指南

gaogang博客

关于 Hacker的神经网络指南

黑客的神经网络指南

注意:现在这是一个非常古老的教程,我将其放弃,但我不相信应该被引用或使用。更好的材料包括CS231n课程讲座,幻灯片和笔记,或深度学习书

你好,我是斯坦福大学CS博士生。作为我研究的一部分,我在深度学习方面工作了几年,我的几个相关宠物项目之一是ConvNetJS--一个用于训练神经网络的Javascript库。Javascript允许人们很好地可视化正在发生的事情并使用各种超参数设置进行游戏,但我仍然经常听到那些要求对该主题进行更彻底处理的人。这篇文章(我计划慢慢扩展到几本书的章节)是我的谦虚尝试。它是在网上而不是PDF,因为所有的书都应该是,并且最终它有望包括动画/演示等。

我对神经网络的个人经验是,当我开始忽略反向传播方程的全页,密集推导并开始编写代码时,一切都变得更加清晰。因此,本教程将包含非常少的数学(我不认为它是必要的,它有时甚至可以模糊简单的概念)。由于我的背景是计算机科学和物理,我将从我所说的黑客角度来发展这个主题。我的论述将围绕代码和物理直觉而不是数学推导。基本上,我将努力以我希望在我开始时遇到的方式呈现算法。

“......当我开始编写代码时,一切都变得更加清晰。”

您可能渴望直接进入并了解神经网络,反向传播,如何将它们应用于实践中的数据集等。但在我们到达之前,我希望我们首先忘记所有这些。让我们退后一步,了解核心真正发生的事情。让我们先谈谈实值电路。

更新说明:我刚刚暂停了本指南的工作,并将大量精力重新用于在斯坦福大学教授CS231n(卷积神经网络)课程。这些笔记在cs231.github.io上,课程幻灯片可以在这里找到。这些材料与这里的材料高度相关,但更全面,有时更精致。

第1章:实值电路

在我看来,思考神经网络的最佳方式是实值电路,其中实值(而不是布尔值{0,1})沿着边缘“流动”并在门中相互作用。然而,代替门如ANDORNOT,等,我们有二门,例如*(乘), +(加),max或一元门如exp等不同于普通的布尔电路,但是,我们最终也将有梯度的流动电路的边缘相同,但方向相反。但我们已经领先于自己。让我们专注并开始简单。

基本案例:电路中的单门

让我们首先考虑一个带有一个门的简单电路。这是一个例子:

xy*

该电路采用两个实值输入xyx * y*门进行计算。这个Javascript版本看起来很简单:

<span style="color:#000000"><code><strong>var</strong> forwardMultiplyGate <strong>=</strong> <strong>function</strong>(x, y) {
  <strong>return</strong> x <strong>*</strong> y;
};
forwardMultiplyGate(<strong>-</strong><span style="color:#009999">2</span>, <span style="color:#009999">3</span>); <span style="color:#999988"><em>// returns -6. Exciting.</em></span>
</code></span>

在数学形式中,我们可以将此门视为实现实值函数:

与此示例一样,我们所有的门都将采用一个或两个输入并产生单个输出值。

目标

我们对学习感兴趣的问题如下:

  1. 我们提供一个给定的电路部分特定的输入值(例如x = -2y = 3
  2. 电路计算输出值(例如-6
  3. 那么核心问题就是:如何稍微调整输入以增加输出?

在这种情况下,我们应该改变哪个方向x,y以获得大于-6?的数字?请注意,例如,x = -1.99y = 2.99给出x * y = -5.95,高于-6.0。不要对此感到困惑:-5.95比(更高)更好-6.0。这是一个进步0.05,尽管幅度-5.95(一般为从零距离)恰好是较低的。

策略#1:随机本地搜索

好的。等等,我们有一个电路,我们有一些输入,我们只想略微调整它们以增加输出值?为什么这么难?我们可以轻松地“转发”电路来计算任何给定x和的输出y。所以这不是微不足道的吗?为什么我们不随意调整xy跟踪最有效的调整:

<span style="color:#000000"><code><span style="color:#999988"><em>// circuit with single gate for now</em></span>
<strong>var</strong> forwardMultiplyGate <strong>=</strong> <strong>function</strong>(x, y) { <strong>return</strong> x <strong>*</strong> y; };
<strong>var</strong> x <strong>=</strong> <strong>-</strong><span style="color:#009999">2</span>, y <strong>=</strong> <span style="color:#009999">3</span>; <span style="color:#999988"><em>// some input values</em></span>

<span style="color:#999988"><em>// try changing x,y randomly small amounts and keep track of what works best</em></span>
<strong>var</strong> tweak_amount <strong>=</strong> <span style="color:#009999">0.01</span>;
<strong>var</strong> best_out <strong>=</strong> <strong>-</strong><strong>Infinity</strong>;
<strong>var</strong> best_x <strong>=</strong> x, best_y <strong>=</strong> y;
<strong>for</strong>(<strong>var</strong> k <strong>=</strong> <span style="color:#009999">0</span>; k <strong><</strong> <span style="color:#009999">100</span>; k<strong>++</strong>) {
  <strong>var</strong> x_try <strong>=</strong> x <strong>+</strong> tweak_amount <strong>*</strong> (<span style="color:#0086b3">Math</span>.random() <strong>*</strong> <span style="color:#009999">2</span> <strong>-</strong> <span style="color:#009999">1</span>); <span style="color:#999988"><em>// tweak x a bit</em></span>
  <strong>var</strong> y_try <strong>=</strong> y <strong>+</strong> tweak_amount <strong>*</strong> (<span style="color:#0086b3">Math</span>.random() <strong>*</strong> <span style="color:#009999">2</span> <strong>-</strong> <span style="color:#009999">1</span>); <span style="color:#999988"><em>// tweak y a bit</em></span>
  <strong>var</strong> out <strong>=</strong> forwardMultiplyGate(x_try, y_try);
  <strong>if</strong>(out <strong>></strong> best_out) {
    <span style="color:#999988"><em>// best improvement yet! Keep track of the x and y</em></span>
    best_out <strong>=</strong> out; 
    best_x <strong>=</strong> x_try, best_y <strong>=</strong> y_try;
  }
}
</code></span>

当我跑,我得到best_x = -1.9928best_y = 2.9901best_out = -5.9588。再次,-5.9588高于-6.0。所以,我们完成了,对吧?不完全:如果你能负担得起计算时间的话,对于有少量门的微小问题,这是一个非常好的策略,但如果我们想最终考虑拥有数百万输入的大电路,那就不行了。事实证明,我们可以做得更好。

状态#2:数值梯度

这是一个更好的方法。再次记住,在我们的设置中,我们给出了一个电路(例如我们的电路带有一个*门)和一些特定的输入(例如x = -2, y = 3)。该门计算输出(-6),现在我们想调整xy使输出更高。

对于我们即将做的事情的一个很好的直觉如下:想象一下从电路中输出的输出值并将其拉向正方向。这种积极的张力将反过来通过大门并在输入x和引导上产生力y。告诉我们如何xy应该改变以增加产值的力量。

在我们的具体例子中,这些力量可能是什么样的?通过它思考,我们可以直觉说力量x也应该是积极的,因为x稍微大一些可以改善电路的输出。例如,xx = -2增加到x = -1将给我们输出-3- 远大于-6。另一方面,我们期望引起的负面力量y推动它变得更低(因为较低的y,例如y = 2,从原始的下降y = 3会使输出更高:2 x -2 = -4再次,大于-6)。无论如何,这是要记住的直觉。当我们经历这一点时,事实证明我所描述的力量实际上将成为衍生物输出值相对于其输入(xy)的值。你可能以前听过这个词。

当我们拉动输出变得更高时,导数可以被认为是每个输入上的一个力。

那么我们如何准确地评估这种力量(衍生物)呢?事实证明,有一个非常简单的程序。我们将向后工作:我们将逐个迭代每个输入,而不是拉动电路的输出,稍微增加它,看看输出值会发生什么。输出响应变化的量是导数。现在有足够的直觉。让我们看一下数学定义。我们可以根据输入记下函数的导数。例如,相对于的导数x可以计算为:

$$ \ frac {\ partial f(x,y)} {\ partial x} = \ frac {f(x + h,y) - f(x,y)} {h} $$

其中\(h \)很小 - 这是调整量。此外,如果您不熟悉微积分,请务必注意,在上面等式的左侧,水平线表示除法。整个符号\(\ frac {\ partial f(x,y)} {\ partial x} \)是一回事:函数\(f(x,y)\)相对于\(x的导数\)。右边的水平线师。我知道它令人困惑但它是标准符号。无论如何,我希望它看起来并不太可怕,因为它不是:电路正在给出一些初始输出\(f(x,y)\),然后我们将其中一个输入改变了很小的数量\(h \)并读取新输出\(f(x + h,y)\)。减去这两个数量告诉我们变化,除以\(h \)只是将这个变化归一化我们使用的(任意)调整量。换句话说,它正在表达我上面描述的内容并直接转换为此代码:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> <strong>-</strong><span style="color:#009999">2</span>, y <strong>=</strong> <span style="color:#009999">3</span>;
<strong>var</strong> out <strong>=</strong> forwardMultiplyGate(x, y); <span style="color:#999988"><em>// -6</em></span>
<strong>var</strong> h <strong>=</strong> <span style="color:#009999">0.0001</span>;

<span style="color:#999988"><em>// compute derivative with respect to x</em></span>
<strong>var</strong> xph <strong>=</strong> x <strong>+</strong> h; <span style="color:#999988"><em>// -1.9999</em></span>
<strong>var</strong> out2 <strong>=</strong> forwardMultiplyGate(xph, y); <span style="color:#999988"><em>// -5.9997</em></span>
<strong>var</strong> x_derivative <strong>=</strong> (out2 <strong>-</strong> out) <strong>/</strong> h; <span style="color:#999988"><em>// 3.0</em></span>

<span style="color:#999988"><em>// compute derivative with respect to y</em></span>
<strong>var</strong> yph <strong>=</strong> y <strong>+</strong> h; <span style="color:#999988"><em>// 3.0001</em></span>
<strong>var</strong> out3 <strong>=</strong> forwardMultiplyGate(x, yph); <span style="color:#999988"><em>// -6.0002</em></span>
<strong>var</strong> y_derivative <strong>=</strong> (out3 <strong>-</strong> out) <strong>/</strong> h; <span style="color:#999988"><em>// -2.0</em></span>
</code></span>

让我们来看看x。我们从原来的旋钮xx + h和电路回应给予更高的值(再次指出,是的,-5.9997更高的-6-5.9997 > -6)。除法h是通过h我们选择在这里使用的(任意)值来规范电路的响应。从技术上讲,你想要的值h是无穷小的(渐变的精确数学定义被定义为表达式的极限h为零),但在实践h=0.00001中,大多数情况下工作正常以获得良好的近似值。现在,我们看到衍生的wrt x+3。我正在明确表示正号,因为它表明电路正在拉动x变得更高。实际值,3可以解释为该拖船的力量

可以通过稍微调整该输入并观察输出值的变化来计算关于某些输入的导数。

顺便说一句,我们通常谈论单个输入的导数,或关于所有输入的梯度。梯度仅由向量(即列表)中连接的所有输入的导数组成。至关重要的是,请注意,如果我们让输入通过跟踪梯度很小的量(即我们只是在每个输入的顶部添加导数)来响应拖船,我们可以看到值增加,如预期的那样:

<span style="color:#000000"><code><strong>var</strong> step_size <strong>=</strong> <span style="color:#009999">0.01</span>;
<strong>var</strong> out <strong>=</strong> forwardMultiplyGate(x, y); <span style="color:#999988"><em>// before: -6</em></span>
x <strong>=</strong> x <strong>+</strong> step_size <strong>*</strong> x_derivative; <span style="color:#999988"><em>// x becomes -1.97</em></span>
y <strong>=</strong> y <strong>+</strong> step_size <strong>*</strong> y_derivative; <span style="color:#999988"><em>// y becomes 2.98</em></span>
<strong>var</strong> out_new <strong>=</strong> forwardMultiplyGate(x, y); <span style="color:#999988"><em>// -5.87! exciting.</em></span>
</code></span>

正如预期的那样,我们通过梯度更改输入,电路现在提供稍高的值(-5.87 > -6.0)。这是不是试图随机变化更简单xy,对不对?这里要理解的一个事实是,如果你采用微积分,你可以证明梯度实际上是函数最陡增长的方向。没有必要像在策略#1中那样尝试随机的细微差别。评估梯度只需要对我们电路的正向传递进行三次评估,而不是数百次评估,如果您有兴趣增加输出值,则可以提供您希望(本地)的最佳拖拽。

更大的步骤并不总是更好。让我澄清一下这一点。重要的是要注意,在这个非常简单的例子中,使用step_size大于0.01的值总是会更好。例如,step_size = 1.0给出输出-1(更高,更好!),实际上无限步长会产生无限好的结果。要实现的关键是,一旦我们的电路变得更复杂(例如整个神经网络),从输入到输出值的功能将更加混乱和摇摆。渐变保证如果你有一个非常小(实际上,无限小)的步长,那么当你按照它的方向​​你肯定会得到一个更高的数字,并且对于那个无限小的步长,没有其他方向可以更好地工作。但是如果你使用更大的步长(例如step_size = 0.01所有的赌注都没有了。我们可以通过比无穷小的步长更大的原因,我们的功能通常相对平滑。但实际上,我们正在交叉手指并希望最好。

爬山比喻。我之前听过的一个比喻是,我们的圈子的输出值就像山的高度,我们被蒙上眼睛,试图向上爬。我们可以感觉到我们脚下的山丘的陡峭程度(渐变),所以当我们稍微洗脚时,我们会向上走。但如果我们采取了一个过于自信的大步骤,我们就可以直接进入一个洞。

太好了,我希望我已经说服你,数值梯度确实是一个非常有用的评估,它很便宜。但。事实证明,我们可以做的,甚至更好。

策略#3:分析梯度

在上一节中,我们通过探测电路的输出值来评估梯度,独立于每个输入。这个过程为您提供了我们称之为数值梯度的东西。然而,这种方法仍然很昂贵,因为我们需要计算电路的输出,因为我们将每个输入值独立调整一小部分。因此,评估梯度的复杂性在输入数量上是线性的。但在实践中,我们将有数百,数千或(对于神经网络)甚至数十亿到数亿的输入,并且电路不仅仅是一个乘法门,而是计算成本高昂的巨大表达式。我们需要更好的东西。

幸运的是,有一种更简单,快速的方法来计算梯度:我们可以使用微积分来得到它的直接表达式,这将像电路的输出值一样简单。我们称之为分析梯度,不需要调整任何东西。您可能已经看到其他人教导神经网络在巨大的,坦率的,可怕的和令人困惑的数学方程中得出梯度(如果你不精通数学)。但这是不必要的。我已经编写了大量的神经网络代码,而且我很少需要进行长于两行的数学推导,并且95%的时间可以完成而无需编写任何内容。这是因为我们只会为非常小而简单的表达式推导出梯度(将其视为基本情况)然后我将向您展示我们如何使用链规则非常简单地构建这些来评估完整的梯度(想想归纳/递归情况)。

分析导数不需要调整输入。它可以使用数学(微积分)导出。

如果你还记得你的产品规则,权力规则,商数规则等(参见例如派生规则维基页面),那么很容易就两者xy小表达式(如x * y。但是假设你不记得你的微积分规则。我们可以回到定义。例如,这是导数wrt的表达式x

$$ \ frac {\ partial f(x,y)} {\ partial x} = \ frac {f(x + h,y) - f(x,y)} {h} $$

(从技术上讲,我没有写限制h为零,原谅我的数学人)。好的,让我们的函数(\(f(x,y)= xy \))插入表达式。准备好整篇文章最难的数学?开始了:

$$ \ frac {\ partial f(x,y)} {\ partial x} = \ frac {f(x + h,y) - f(x,y)} {h} = \ frac {(x + h )y - xy} {h} = \ frac {xy + hy - xy} {h} = \ frac {hy} {h} = y $$

那很有意思。相对于的导数x恰好等于y。您是否注意到上一节中的巧合?我们调整了xx+h和计算x_derivative = 3.0,这正是恰好是价值y的那个例子。事实证明,这根本不是巧合,因为这正是分析梯度告诉我们x衍生物应该是什么f(x,y) = x * yy顺便说一句,关于x对称性的衍生物并不令人惊讶。所以不需要任何调整!我们调用了强大的数学,现在可以将我们的导数计算转换为以下代码:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> <strong>-</strong><span style="color:#009999">2</span>, y <strong>=</strong> <span style="color:#009999">3</span>;
<strong>var</strong> out <strong>=</strong> forwardMultiplyGate(x, y); <span style="color:#999988"><em>// before: -6</em></span>
<strong>var</strong> x_gradient <strong>=</strong> y; <span style="color:#999988"><em>// by our complex mathematical derivation above</em></span>
<strong>var</strong> y_gradient <strong>=</strong> x;

<strong>var</strong> step_size <strong>=</strong> <span style="color:#009999">0.01</span>;
x <strong>+=</strong> step_size <strong>*</strong> x_gradient; <span style="color:#999988"><em>// -1.97</em></span>
y <strong>+=</strong> step_size <strong>*</strong> y_gradient; <span style="color:#999988"><em>// 2.98</em></span>
<strong>var</strong> out_new <strong>=</strong> forwardMultiplyGate(x, y); <span style="color:#999988"><em>// -5.87. Higher output! Nice.</em></span>
</code></span>

为了计算梯度,我们将电路转发数百次(策略#1),仅按照输入数量(策略#2)的两倍的顺序转发,转发它一次!它变得更好,因为更昂贵的策略(#1和#2)只给出了渐变的近似值,而#3(迄今为止最快的一个)给出了精确的梯度。没有近似值。唯一的缺点是你应该对一些微积分101感到满意。

让我们回顾一下我们学到的东西:

  • INPUT:我们给出一个电路,一些输入并计算输出值。
  • 输出:我们当时有兴趣找到每个输入的小变化(独立),这将使输出更高。
  • 策略#1:一种愚蠢的方式是随机搜索输入的小扰动并跟踪产生最高输出的因素。
  • 策略#2:我们看到通过计算渐变可以做得更好。无论电路有多复杂,数值梯度计算都非常简单(但相对昂贵)。我们通过探测电路的输出值来计算它,因为我们一次调整一个输入。
  • 策略#3:最后,我们看到我们可以更加聪明,并在分析上获得直接表达式来获得分析梯度。它与数值梯度相同,到目前为止速度最快,不需要任何调整。

在实践中(我们将在以后再次讨论),所有神经网络库总是计算分析梯度,但通过将其与数值梯度进行比较来验证实现的正确性。这是因为数值梯度非常容易评估(但计算起来可能有点贵),而分析梯度有时会包含错误,但通常计算效率非常高。正如我们将看到的那样,评估梯度(即在进行反向传递向后传球时)将花费与评估前向传球相同的成本。

递归案例:具有多个门的电路

但请坚持,你说:“分析梯度对于你的超简单表达来说是微不足道的。这没用。当表达式更大时我该怎么办?不要方程得到庞大而复杂的非常快?”。好问题。是的,表达式变得复杂得多。不,这不会让事情变得更难。正如我们将要看到的,每个门都将自行挂出,完全不知道它可能是其中一部分的巨大而复杂的电路的任何细节。它只会担心它的输入,它将计算其局部导数,如上一节所示,除非现在将需要一个额外的乘法。

单个额外的乘法会将单个(无用的门)变成复杂机器中的齿轮,这是整个神经网络。

我现在应该停止夸大它。我希望我引起你的兴趣!让我们深入了解细节,并在下一个示例中介绍两个门:

xyz+q*f

我们现在计算的表达式是\(f(x,y,z)=(x + y)z \)。让我们按如下方式构造代码,使门明确为函数:

<span style="color:#000000"><code><strong>var</strong> forwardMultiplyGate <strong>=</strong> <strong>function</strong>(a, b) { 
  <strong>return</strong> a <strong>*</strong> b;
};
<strong>var</strong> forwardAddGate <strong>=</strong> <strong>function</strong>(a, b) { 
  <strong>return</strong> a <strong>+</strong> b;
};
<strong>var</strong> forwardCircuit <strong>=</strong> <strong>function</strong>(x,y,z) { 
  <strong>var</strong> q <strong>=</strong> forwardAddGate(x, y);
  <strong>var</strong> f <strong>=</strong> forwardMultiplyGate(q, z);
  <strong>return</strong> f;
};

<strong>var</strong> x <strong>=</strong> <strong>-</strong><span style="color:#009999">2</span>, y <strong>=</strong> <span style="color:#009999">5</span>, z <strong>=</strong> <strong>-</strong><span style="color:#009999">4</span>;
<strong>var</strong> f <strong>=</strong> forwardCircuit(x, y, z); <span style="color:#999988"><em>// output is -12</em></span>
</code></span>

在上面,我使用ab作为门函数中的局部变量,这样我们就不会将这些与电路输入混淆x,y,z。和以前一样,我们有兴趣找到关于三个输入的衍生物x,y,z。但是,如果涉及多个门,我们如何计算呢?首先,让我们假装+门不存在,并且我们在电路中只有两个变量:q,z和一个*门。注意,qis是+门的输出。如果我们不担心xy但只关心qz,那么我们又回到了只有一个门,并且只有那个*对于门而言,我们知道(分析)派生词来自前一部分。我们可以把它们写下来(除了在这里我们要替换x,yq,z):

足够简单:这些是关于q和的渐变的表达式z。但是等等,我们不希望梯度相q对于输入而言:xy。幸运的是,q被计算为的函数xy(通过加入在我们的例子)。我们也可以写下加法门的渐变,它甚至更简单:

这是正确的,衍生品只是1,不管实际值的xy。如果你仔细想想,这是有道理的,因为使一个单一的附加栅的输出更高,我们预计双方积极拖船xy,不论其价值。

反向传播

我们终于准备调用链规则:我们知道如何计算的梯度q相对于xy(这是一个单门的情况下与+作为栅极)。我们知道如何计算最终输出的梯度q。链式法则告诉我们如何结合这些获得相对于最终输出的梯度xy,这正是我们在最重要的是,最终有兴趣,链式法则非常简单地指出,做正确的事情是简单地将梯度相乘以将它们链接起来。例如,最终衍生物x将是:

那里有许多符号,所以这可能会让人感到困惑,但实际上只有两个数字相乘。这是代码:

<span style="color:#000000"><code><span style="color:#999988"><em>// initial conditions</em></span>
<strong>var</strong> x <strong>=</strong> <strong>-</strong><span style="color:#009999">2</span>, y <strong>=</strong> <span style="color:#009999">5</span>, z <strong>=</strong> <strong>-</strong><span style="color:#009999">4</span>;
<strong>var</strong> q <strong>=</strong> forwardAddGate(x, y); <span style="color:#999988"><em>// q is 3</em></span>
<strong>var</strong> f <strong>=</strong> forwardMultiplyGate(q, z); <span style="color:#999988"><em>// output is -12</em></span>

<span style="color:#999988"><em>// gradient of the MULTIPLY gate with respect to its inputs</em></span>
<span style="color:#999988"><em>// wrt is short for "with respect to"</em></span>
<strong>var</strong> derivative_f_wrt_z <strong>=</strong> q; <span style="color:#999988"><em>// 3</em></span>
<strong>var</strong> derivative_f_wrt_q <strong>=</strong> z; <span style="color:#999988"><em>// -4</em></span>

<span style="color:#999988"><em>// derivative of the ADD gate with respect to its inputs</em></span>
<strong>var</strong> derivative_q_wrt_x <strong>=</strong> <span style="color:#009999">1.0</span>;
<strong>var</strong> derivative_q_wrt_y <strong>=</strong> <span style="color:#009999">1.0</span>;

<span style="color:#999988"><em>// chain rule</em></span>
<strong>var</strong> derivative_f_wrt_x <strong>=</strong> derivative_q_wrt_x <strong>*</strong> derivative_f_wrt_q; <span style="color:#999988"><em>// -4</em></span>
<strong>var</strong> derivative_f_wrt_y <strong>=</strong> derivative_q_wrt_y <strong>*</strong> derivative_f_wrt_q; <span style="color:#999988"><em>// -4</em></span>
</code></span>

而已。我们计算了梯度(力),现在我们可以让我们的输入稍微响应它。让我们在输入的顶部添加渐变。电路的输出值从-12增加到更高!

<span style="color:#000000"><code><span style="color:#999988"><em>// final gradient, from above: [-4, -4, 3]</em></span>
<strong>var</strong> gradient_f_wrt_xyz <strong>=</strong> [derivative_f_wrt_x, derivative_f_wrt_y, derivative_f_wrt_z]

<span style="color:#999988"><em>// let the inputs respond to the force/tug:</em></span>
<strong>var</strong> step_size <strong>=</strong> <span style="color:#009999">0.01</span>;
x <strong>=</strong> x <strong>+</strong> step_size <strong>*</strong> derivative_f_wrt_x; <span style="color:#999988"><em>// -2.04</em></span>
y <strong>=</strong> y <strong>+</strong> step_size <strong>*</strong> derivative_f_wrt_y; <span style="color:#999988"><em>// 4.96</em></span>
z <strong>=</strong> z <strong>+</strong> step_size <strong>*</strong> derivative_f_wrt_z; <span style="color:#999988"><em>// -3.97</em></span>

<span style="color:#999988"><em>// Our circuit now better give higher output:</em></span>
<strong>var</strong> q <strong>=</strong> forwardAddGate(x, y); <span style="color:#999988"><em>// q becomes 2.92</em></span>
<strong>var</strong> f <strong>=</strong> forwardMultiplyGate(q, z); <span style="color:#999988"><em>// output is -11.59, up from -12! Nice!</em></span>

</code></span>

看起来有用了!让我们试着直观地解释刚刚发生的事情。电路想要输出更高的值。最后一个门看到输入q = 3, z = -4和计算输出-12。在此输出值上“向上拉”会在两者上产生一个力,q并且z:为了增加输出值,电路“希望” z增加,这可以从导数(derivative_f_wrt_z = +3)的正值看出。同样,该导数的大小可以解释为力的大小。另一方面,q感受到更强大和向下的力量,因为derivative_f_wrt_q = -4。换句话说,电路想q用力减小4

现在我们到达第二个+输出的门q。默认情况下,+门计算及其衍生物,它告诉我们如何改变xy作出q更高。但!这里是关键的一点:在梯度q被计算为负(derivative_f_wrt_q = -4),所以电路要q降低,并与力4!因此,如果+门希望有助于使最终输出值更大,则需要监听来自顶部的梯度信号。在这种特殊情况下,它需要使用x,y与通常应用的相反的拖船,并且可以说是一种力量4。乘以-4在链式法则看出实现正是这种:而不是施加的正力+1在两个xy上(本地衍生物),全电路的梯度都xy1 x -4 = -4。这是有道理的:电路需要两者x并且y变得更小,因为这会变q小,这反过来会变f大。

如果这是有道理的,你就会理解反向传播。

让我们再次回顾一下我们学到的东西:

  • 在前一章中,我们看到在单个门(或单个表达式)的情况下,我们可以使用简单的微积分推导出分析梯度。我们将梯度解释为一个力,或者对输入进行拖拽,将它们拉向一个方向,使这个门的输出更高。

  • 在多个门的情况下,一切都保持几乎相同的方式:每个门都完全没有意识到它所嵌入的电路。一些输入进入并且门计算其输出和相对于输入的派生。现在唯一的区别是,突然间,某些东西可以从上方拉出这扇门。这是相对于此门计算的输出的最终电路输出值的梯度。正是电路要求门输出更高或更低的数字,并且有一些力。门只需要使用此力并将其乘以它为其输入之前计算的所有力(链规则)。这具有预期的效果:

  1. 如果一个门从上方经历强烈的正向拉动,那么它也会在自己的输入上更加努力,从上面所经历的力量来衡量
  2. 如果它经历了负面拖拽,这意味着电路希望其值减小而不是增加,因此它将翻转其输入上的拉力以使其自身的输出值更小。

一个很好的想法是,当我们在最后拉动电路的输出值时,这会导致整个电路向下拉动,一直向下到输入。

不漂亮吗?计算任意复杂表达式的单个门和多个交互门的情况之间的唯一区别是这个额外的乘法运算现在在每个门中发生。

模式在“向后”流动

让我们再看一下填写数字的示例电路。第一个电路显示原始值,第二个电路显示回流到输入的梯度,如上所述。请注意,渐变始终从+1最后开始,以从链开始。这是电路上的(默认)拉动,使其值增加。

(Values)-25-4+3*-12(Gradients)-4-43+-4*1

过了一会儿,你开始注意到渐变在电路中向后流动的模式。例如,+门总是在顶部采用渐变,并简单地将其传递给它的所有输入(注意例子,-4简单地传递给+门的两个输入)。这是因为+1无论输入的实际值是什么,它自己的输入导数都是正确的,所以在链规则中,上面的梯度只是乘以1并保持不变。类似的直觉适用于例如max(x,y)门。由于梯度max(x,y)相对于它的输入是+1用于取其之一xy是较大的和0 另一方面,这个门在backprop期间有效地只是一个渐变“切换”:它将从上方获取梯度并将其“路由”到前向传递期间具有更高值的输入。

数值梯度检查。在我们完成本节之前,让我们确保上面backprop计算的(解析)渐变是正确的,作为一个健全性检查。请记住,我们可以简单地通过计算数值梯度,并确保我们得到这样做[-4, -4, 3]x,y,z。这是代码:

<span style="color:#000000"><code><span style="color:#999988"><em>// initial conditions</em></span>
<strong>var</strong> x <strong>=</strong> <strong>-</strong><span style="color:#009999">2</span>, y <strong>=</strong> <span style="color:#009999">5</span>, z <strong>=</strong> <strong>-</strong><span style="color:#009999">4</span>;

<span style="color:#999988"><em>// numerical gradient check</em></span>
<strong>var</strong> h <strong>=</strong> <span style="color:#009999">0.0001</span>;
<strong>var</strong> x_derivative <strong>=</strong> (forwardCircuit(x<strong>+</strong>h,y,z) <strong>-</strong> forwardCircuit(x,y,z)) <strong>/</strong> h; <span style="color:#999988"><em>// -4</em></span>
<strong>var</strong> y_derivative <strong>=</strong> (forwardCircuit(x,y<strong>+</strong>h,z) <strong>-</strong> forwardCircuit(x,y,z)) <strong>/</strong> h; <span style="color:#999988"><em>// -4</em></span>
<strong>var</strong> z_derivative <strong>=</strong> (forwardCircuit(x,y,z<strong>+</strong>h) <strong>-</strong> forwardCircuit(x,y,z)) <strong>/</strong> h; <span style="color:#999988"><em>// 3</em></span>
</code></span>

我们得到[-4, -4, 3],用backprop计算。唷!:)

示例:单神经元

在上一节中,您希望得到反向传播背后的基本直觉。现在让我们看一个更复杂和边缘的实际例子。我们将考虑一个计算以下函数的二维神经元:

在这个表达式中,\(\ sigma \)是sigmoid函数。它最好被认为是一个“压缩函数”,因为它接受输入并将其压缩到0到1之间:非常负值被压缩为零,正值被压缩为1。例如,我们有sig(-5) = 0.006, sig(0) = 0.5, sig(5) = 0.993。Sigmoid函数定义为:

关于单个输入的渐变,您可以在维基百科上查看,或者如果您知道某些微积分则自己推导出这个表达式:

例如,如果sigmoid门的输入是x = 3,则门将计算输出f = 1.0 / (1.0 + Math.exp(-x)) = 0.95,然后其输入上的(局部)梯度将是dx = (0.95) * (1 - 0.95) = 0.0475

这就是我们需要使用这个门:我们知道如何获取输入并通过sigmoid门转发它,并且我们还有关于其输入的渐变表达式,因此我们也可以通过它进行反向调整。另外需要注意的是,从技术上讲,S形函数由一系列计算更多原子函数的门组成:取幂门,加法门和除法门。处理它将如此完美地工作但是对于这个例子我选择将所有这些门折叠成一个单独的门,一次只计算sigmoid,因为渐变表达式变得简单。

让我们借此机会以一种漂亮和模块化的方式仔细构建相关代码。首先,我想请你注意我们图表中的每一条线都有两个与之相关的数字:

  1. 它在前进过程中携带的价值
  2. 在向后传球中通过它回流的梯度(即拉力

让我们创建一个简单的Unit结构,将这两个值存储在每条线上。我们的大门现在将在Units上运行:它们将把它们作为输入并将它们作为输出创建。

<span style="color:#000000"><code><span style="color:#999988"><em>// every Unit corresponds to a wire in the diagrams</em></span>
<strong>var</strong> Unit <strong>=</strong> <strong>function</strong>(value, grad) {
  <span style="color:#999988"><em>// value computed in the forward pass</em></span>
  <strong>this</strong>.value <strong>=</strong> value; 
  <span style="color:#999988"><em>// the derivative of circuit output w.r.t this unit, computed in backward pass</em></span>
  <strong>this</strong>.grad <strong>=</strong> grad; 
}
</code></span>

除了单位我们还需要3门:+*sig(乙状结肠)。让我们开始实现一个乘法门。我在这里使用Javascript,它有一种使用函数模拟类的有趣方法。如果你不是一个Javascript熟悉的人,那么这里发生的一切就是我正在定义一个具有某些属性的类(使用this关键字访问),以及一些方法(在Javascript中放入函数的原型) 。试想一下这些类方法。还要记住,我们最终将使用这些的方式是,我们将首先forward逐个所有的门,然后是backward所有的门以相反的顺序。这是实施:

<span style="color:#000000"><code>
<strong>var</strong> multiplyGate <strong>=</strong> <strong>function</strong>(){ };
multiplyGate.prototype <strong>=</strong> {
  <span style="color:#008080">forward</span>: <strong>function</strong>(u0, u1) {
    <span style="color:#999988"><em>// store pointers to input Units u0 and u1 and output unit utop</em></span>
    <strong>this</strong>.u0 <strong>=</strong> u0; 
    <strong>this</strong>.u1 <strong>=</strong> u1; 
    <strong>this</strong>.utop <strong>=</strong> <strong>new</strong> Unit(u0.value <strong>*</strong> u1.value, <span style="color:#009999">0.0</span>);
    <strong>return</strong> <strong>this</strong>.utop;
  },
  <span style="color:#008080">backward</span>: <strong>function</strong>() {
    <span style="color:#999988"><em>// take the gradient in output unit and chain it with the</em></span>
    <span style="color:#999988"><em>// local gradients, which we derived for multiply gate before</em></span>
    <span style="color:#999988"><em>// then write those gradients to those Units.</em></span>
    <strong>this</strong>.u0.grad <strong>+=</strong> <strong>this</strong>.u1.value <strong>*</strong> <strong>this</strong>.utop.grad;
    <strong>this</strong>.u1.grad <strong>+=</strong> <strong>this</strong>.u0.value <strong>*</strong> <strong>this</strong>.utop.grad;
  }
}
</code></span>

乘法门采用两个单元,每个单元保存一个值并创建一个存储其输出的单元。梯度初始化为零。然后请注意,在backward函数调用中,我们从前向传递期间生成的输出单元获得渐变(现在希望填充其渐变),并将其与此门的局部渐变(链规则!)相乘。此门u0.value * u1.value在前向传递期间计算乘法(),因此请记住渐变wrt u0u1.value和wrt u1u0.value。另请注意,我们正在使用+=添加到渐变中backward功能。这将允许我们多次使用一个门的输出(将其视为线分支输出),因为事实证明,当计算相对于电路输出的最终梯度时,来自这些不同分支的梯度加起来。其他两个门类似地定义:

<span style="color:#000000"><code><strong>var</strong> addGate <strong>=</strong> <strong>function</strong>(){ };
addGate.prototype <strong>=</strong> {
  <span style="color:#008080">forward</span>: <strong>function</strong>(u0, u1) {
    <strong>this</strong>.u0 <strong>=</strong> u0; 
    <strong>this</strong>.u1 <strong>=</strong> u1; <span style="color:#999988"><em>// store pointers to input units</em></span>
    <strong>this</strong>.utop <strong>=</strong> <strong>new</strong> Unit(u0.value <strong>+</strong> u1.value, <span style="color:#009999">0.0</span>);
    <strong>return</strong> <strong>this</strong>.utop;
  },
  <span style="color:#008080">backward</span>: <strong>function</strong>() {
    <span style="color:#999988"><em>// add gate. derivative wrt both inputs is 1</em></span>
    <strong>this</strong>.u0.grad <strong>+=</strong> <span style="color:#009999">1</span> <strong>*</strong> <strong>this</strong>.utop.grad;
    <strong>this</strong>.u1.grad <strong>+=</strong> <span style="color:#009999">1</span> <strong>*</strong> <strong>this</strong>.utop.grad;
  }
}
</code></span>
<span style="color:#000000"><code><strong>var</strong> sigmoidGate <strong>=</strong> <strong>function</strong>() { 
  <span style="color:#999988"><em>// helper function</em></span>
  <strong>this</strong>.sig <strong>=</strong> <strong>function</strong>(x) { <strong>return</strong> <span style="color:#009999">1</span> <strong>/</strong> (<span style="color:#009999">1</span> <strong>+</strong> <span style="color:#0086b3">Math</span>.exp(<strong>-</strong>x)); };
};
sigmoidGate.prototype <strong>=</strong> {
  <span style="color:#008080">forward</span>: <strong>function</strong>(u0) {
    <strong>this</strong>.u0 <strong>=</strong> u0;
    <strong>this</strong>.utop <strong>=</strong> <strong>new</strong> Unit(<strong>this</strong>.sig(<strong>this</strong>.u0.value), <span style="color:#009999">0.0</span>);
    <strong>return</strong> <strong>this</strong>.utop;
  },
  <span style="color:#008080">backward</span>: <strong>function</strong>() {
    <strong>var</strong> s <strong>=</strong> <strong>this</strong>.sig(<strong>this</strong>.u0.value);
    <strong>this</strong>.u0.grad <strong>+=</strong> (s <strong>*</strong> (<span style="color:#009999">1</span> <strong>-</strong> s)) <strong>*</strong> <strong>this</strong>.utop.grad;
  }
}
</code></span>

请注意,同样,backward所有情况下的函数只计算与其输入相关的局部导数,然后乘以上面单位的梯度(即链规则)。要完全指定所有内容,最后使用一些示例值为我们的二维神经元写出前向和后向流:

<span style="color:#000000"><code><span style="color:#999988"><em>// create input units</em></span>
<strong>var</strong> a <strong>=</strong> <strong>new</strong> Unit(<span style="color:#009999">1.0</span>, <span style="color:#009999">0.0</span>);
<strong>var</strong> b <strong>=</strong> <strong>new</strong> Unit(<span style="color:#009999">2.0</span>, <span style="color:#009999">0.0</span>);
<strong>var</strong> c <strong>=</strong> <strong>new</strong> Unit(<strong>-</strong><span style="color:#009999">3.0</span>, <span style="color:#009999">0.0</span>);
<strong>var</strong> x <strong>=</strong> <strong>new</strong> Unit(<strong>-</strong><span style="color:#009999">1.0</span>, <span style="color:#009999">0.0</span>);
<strong>var</strong> y <strong>=</strong> <strong>new</strong> Unit(<span style="color:#009999">3.0</span>, <span style="color:#009999">0.0</span>);

<span style="color:#999988"><em>// create the gates</em></span>
<strong>var</strong> mulg0 <strong>=</strong> <strong>new</strong> multiplyGate();
<strong>var</strong> mulg1 <strong>=</strong> <strong>new</strong> multiplyGate();
<strong>var</strong> addg0 <strong>=</strong> <strong>new</strong> addGate();
<strong>var</strong> addg1 <strong>=</strong> <strong>new</strong> addGate();
<strong>var</strong> sg0 <strong>=</strong> <strong>new</strong> sigmoidGate();

<span style="color:#999988"><em>// do the forward pass</em></span>
<strong>var</strong> forwardNeuron <strong>=</strong> <strong>function</strong>() {
  ax <strong>=</strong> mulg0.forward(a, x); <span style="color:#999988"><em>// a*x = -1</em></span>
  by <strong>=</strong> mulg1.forward(b, y); <span style="color:#999988"><em>// b*y = 6</em></span>
  axpby <strong>=</strong> addg0.forward(ax, by); <span style="color:#999988"><em>// a*x + b*y = 5</em></span>
  axpbypc <strong>=</strong> addg1.forward(axpby, c); <span style="color:#999988"><em>// a*x + b*y + c = 2</em></span>
  s <strong>=</strong> sg0.forward(axpbypc); <span style="color:#999988"><em>// sig(a*x + b*y + c) = 0.8808</em></span>
};
forwardNeuron();

console.log(<span style="color:#dd1144">'circuit output: '</span> <strong>+</strong> s.value); <span style="color:#999988"><em>// prints 0.8808</em></span>
</code></span>

现在让我们计算梯度:简单地以相反的顺序迭代并调用backward函数!请记住,当我们进行正向传递时,我们将指针存储到单元中,因此每个门都可以访问其输入以及它先前生成的输出单元。

<span style="color:#000000"><code>s.grad <strong>=</strong> <span style="color:#009999">1.0</span>;
sg0.backward(); <span style="color:#999988"><em>// writes gradient into axpbypc</em></span>
addg1.backward(); <span style="color:#999988"><em>// writes gradients into axpby and c</em></span>
addg0.backward(); <span style="color:#999988"><em>// writes gradients into ax and by</em></span>
mulg1.backward(); <span style="color:#999988"><em>// writes gradients into b and y</em></span>
mulg0.backward(); <span style="color:#999988"><em>// writes gradients into a and x</em></span>
</code></span>

请注意,第一行将输出处的梯度(最后一个单位)设置1.0为从梯度链开始。这可以被解释为用最大的力拉扯最后一道门+1。换句话说,我们正在拉动整个电路以引发将增加输出值的力。如果我们没有将其设置为1,则由于链规则中的乘法,所有梯度将被计算为零。最后,让输入响应计算的梯度并检查函数是否增加:

<span style="color:#000000"><code><strong>var</strong> step_size <strong>=</strong> <span style="color:#009999">0.01</span>;
a.value <strong>+=</strong> step_size <strong>*</strong> a.grad; <span style="color:#999988"><em>// a.grad is -0.105</em></span>
b.value <strong>+=</strong> step_size <strong>*</strong> b.grad; <span style="color:#999988"><em>// b.grad is 0.315</em></span>
c.value <strong>+=</strong> step_size <strong>*</strong> c.grad; <span style="color:#999988"><em>// c.grad is 0.105</em></span>
x.value <strong>+=</strong> step_size <strong>*</strong> x.grad; <span style="color:#999988"><em>// x.grad is 0.105</em></span>
y.value <strong>+=</strong> step_size <strong>*</strong> y.grad; <span style="color:#999988"><em>// y.grad is 0.210</em></span>

forwardNeuron();
console.log(<span style="color:#dd1144">'circuit output after one backprop: '</span> <strong>+</strong> s.value); <span style="color:#999988"><em>// prints 0.8825</em></span>
</code></span>

成功!0.8825高于之前的值,0.8808。最后,让我们通过检查数值梯度验证我们是否正确实现了反向传播:

<span style="color:#000000"><code><strong>var</strong> forwardCircuitFast <strong>=</strong> <strong>function</strong>(a,b,c,x,y) { 
  <strong>return</strong> <span style="color:#009999">1</span><strong>/</strong>(<span style="color:#009999">1</span> <strong>+</strong> <span style="color:#0086b3">Math</span>.exp( <strong>-</strong> (a<strong>*</strong>x <strong>+</strong> b<strong>*</strong>y <strong>+</strong> c))); 
};
<strong>var</strong> a <strong>=</strong> <span style="color:#009999">1</span>, b <strong>=</strong> <span style="color:#009999">2</span>, c <strong>=</strong> <strong>-</strong><span style="color:#009999">3</span>, x <strong>=</strong> <strong>-</strong><span style="color:#009999">1</span>, y <strong>=</strong> <span style="color:#009999">3</span>;
<strong>var</strong> h <strong>=</strong> <span style="color:#009999">0.0001</span>;
<strong>var</strong> a_grad <strong>=</strong> (forwardCircuitFast(a<strong>+</strong>h,b,c,x,y) <strong>-</strong> forwardCircuitFast(a,b,c,x,y))<strong>/</strong>h;
<strong>var</strong> b_grad <strong>=</strong> (forwardCircuitFast(a,b<strong>+</strong>h,c,x,y) <strong>-</strong> forwardCircuitFast(a,b,c,x,y))<strong>/</strong>h;
<strong>var</strong> c_grad <strong>=</strong> (forwardCircuitFast(a,b,c<strong>+</strong>h,x,y) <strong>-</strong> forwardCircuitFast(a,b,c,x,y))<strong>/</strong>h;
<strong>var</strong> x_grad <strong>=</strong> (forwardCircuitFast(a,b,c,x<strong>+</strong>h,y) <strong>-</strong> forwardCircuitFast(a,b,c,x,y))<strong>/</strong>h;
<strong>var</strong> y_grad <strong>=</strong> (forwardCircuitFast(a,b,c,x,y<strong>+</strong>h) <strong>-</strong> forwardCircuitFast(a,b,c,x,y))<strong>/</strong>h;
</code></span>

实际上,这些都给出了与反向传播梯度相同的值[-0.105, 0.315, 0.105, 0.105, 0.210]。太好了!

我希望很明显,尽管我们只看了一个神经元的例子,但我上面给出的代码以非常直接的方式概括了计算任意表达式的渐变(包括非常深的表达式#foreshadowing)。您所要做的就是编写小门,根据输入计算局部,简单的导数,将其连接到图形中,执行前向传递以计算输出值,然后执行向后传递,将梯度链接到输入。

成为Backprop Ninja

随着时间的推移,即使对于复杂的电路和一次性写入,你也会更有效地编写反向传递。让我们稍微练习一些示例。在下文中,我们不要担心Unit,Circuit类,因为它们会混淆一些东西,并且只允许使用变量,例如a,b,c,xda,db,dc,dx分别引用它们的渐变。同样,我们认为变量是“前向流动”,它们的梯度是沿着每条线的“向后流动”。我们的第一个例子是*门:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> a <strong>*</strong> b;
<span style="color:#999988"><em>// and given gradient on x (dx), we saw that in backprop we would compute:</em></span>
<strong>var</strong> da <strong>=</strong> b <strong>*</strong> dx;
<strong>var</strong> db <strong>=</strong> a <strong>*</strong> dx;
</code></span>

在上面的代码中,我假设dx给出了变量,来自我们在电路中的某个地方,而我们正在做backprop(否则它默认为+1)。我正在写出来,因为我想明确地展示渐变如何链接在一起。从等式中注意到,由于缺少更好的单词,*门在后向传递期间充当切换器。它记得它的输入是什么,每个输入的梯度将是前进过程中另一个的值。当然,我们必须乘以上面的梯度,这是链规则。这是+这种浓缩形式的门:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> a <strong>+</strong> b;
<span style="color:#999988"><em>// -></em></span>
<strong>var</strong> da <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx;
<strong>var</strong> db <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx;
</code></span>

1.0局部梯度在哪里,乘法是我们的​​链规则。添加三个数字怎么样?:

<span style="color:#000000"><code><span style="color:#999988"><em>// lets compute x = a + b + c in two steps:</em></span>
<strong>var</strong> q <strong>=</strong> a <strong>+</strong> b; <span style="color:#999988"><em>// gate 1</em></span>
<strong>var</strong> x <strong>=</strong> q <strong>+</strong> c; <span style="color:#999988"><em>// gate 2</em></span>

<span style="color:#999988"><em>// backward pass:</em></span>
dc <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx; <span style="color:#999988"><em>// backprop gate 2</em></span>
dq <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx; 
da <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dq; <span style="color:#999988"><em>// backprop gate 1</em></span>
db <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dq;
</code></span>

你可以看到发生了什么,对吧?如果您还记得向后流图,则+门只需将梯度置于顶部并将其均等地路由到其所有输入(因为其局部梯度始终仅1.0适用于其所有输入,无论其实际值如何)。所以我们可以更快地完成它:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> a <strong>+</strong> b <strong>+</strong> c;
<strong>var</strong> da <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx; <strong>var</strong> db <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx; <strong>var</strong> dc <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx;
</code></span>

好的,如何组合门?:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> a <strong>*</strong> b <strong>+</strong> c;
<span style="color:#999988"><em>// given dx, backprop in-one-sweep would be =></em></span>
da <strong>=</strong> b <strong>*</strong> dx;
db <strong>=</strong> a <strong>*</strong> dx;
dc <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx;
</code></span>

如果您没有看到上述情况如何发生,请引入一个临时变量q = a * b,然后进行计算x = q + c以说服自己。这是我们的神经元,让我们分两步完成:

<span style="color:#000000"><code><span style="color:#999988"><em>// lets do our neuron in two steps:</em></span>
<strong>var</strong> q <strong>=</strong> a<strong>*</strong>x <strong>+</strong> b<strong>*</strong>y <strong>+</strong> c;
<strong>var</strong> f <strong>=</strong> sig(q); <span style="color:#999988"><em>// sig is the sigmoid function</em></span>
<span style="color:#999988"><em>// and now backward pass, we are given df, and:</em></span>
<strong>var</strong> df <strong>=</strong> <span style="color:#009999">1</span>;
<strong>var</strong> dq <strong>=</strong> (f <strong>*</strong> (<span style="color:#009999">1</span> <strong>-</strong> f)) <strong>*</strong> df;
<span style="color:#999988"><em>// and now we chain it to the inputs</em></span>
<strong>var</strong> da <strong>=</strong> x <strong>*</strong> dq;
<strong>var</strong> dx <strong>=</strong> a <strong>*</strong> dq;
<strong>var</strong> dy <strong>=</strong> b <strong>*</strong> dq;
<strong>var</strong> db <strong>=</strong> y <strong>*</strong> dq;
<strong>var</strong> dc <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dq;
</code></span>

我希望这开始变得更有意义。现在怎么样:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> a <strong>*</strong> a;
<strong>var</strong> da <strong>=</strong> <span style="color:#999988"><em>//???</em></span>
</code></span>

你可以把它想象成a流向*门的值,但是线被分开并成为两个输入。这实际上很简单,因为渐变的向后流动总是会增加。换句话说,没有任何变化:

<span style="color:#000000"><code><strong>var</strong> da <strong>=</strong> a <strong>*</strong> dx; <span style="color:#999988"><em>// gradient into a from first branch</em></span>
da <strong>+=</strong> a <strong>*</strong> dx; <span style="color:#999988"><em>// and add on the gradient from the second branch</em></span>

<span style="color:#999988"><em>// short form instead is:</em></span>
<strong>var</strong> da <strong>=</strong> <span style="color:#009999">2</span> <strong>*</strong> a <strong>*</strong> dx;
</code></span>

事实上,如果你从微积分中知道你的幂规则,你也会知道如果你有\(f(a)= a ^ 2 \)那么\(\ frac {\ partial f(a)} {\ partial a} = 2a \),如果我们将其视为分线并作为门的两个输入,这正是我们得到的。

让我们做另一个:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> a<strong>*</strong>a <strong>+</strong> b<strong>*</strong>b <strong>+</strong> c<strong>*</strong>c;
<span style="color:#999988"><em>// we get:</em></span>
<strong>var</strong> da <strong>=</strong> <span style="color:#009999">2</span><strong>*</strong>a<strong>*</strong>dx;
<strong>var</strong> db <strong>=</strong> <span style="color:#009999">2</span><strong>*</strong>b<strong>*</strong>dx;
<strong>var</strong> dc <strong>=</strong> <span style="color:#009999">2</span><strong>*</strong>c<strong>*</strong>dx;
</code></span>

好的,现在让我们开始变得更复杂:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> <span style="color:#0086b3">Math</span>.pow(((a <strong>*</strong> b <strong>+</strong> c) <strong>*</strong> d), <span style="color:#009999">2</span>); <span style="color:#999988"><em>// pow(x,2) squares the input JS</em></span>
</code></span>

当更复杂的情况出现在实践中时,我喜欢将表达式拆分为可管理的块,这些块几乎总是由更简单的表达式组成,然后我将它们与链规则链接在一起:

<span style="color:#000000"><code><strong>var</strong> x1 <strong>=</strong> a <strong>*</strong> b <strong>+</strong> c;
<strong>var</strong> x2 <strong>=</strong> x1 <strong>*</strong> d;
<strong>var</strong> x <strong>=</strong> x2 <strong>*</strong> x2; <span style="color:#999988"><em>// this is identical to the above expression for x</em></span>
<span style="color:#999988"><em>// and now in backprop we go backwards:</em></span>
<strong>var</strong> dx2 <strong>=</strong> <span style="color:#009999">2</span> <strong>*</strong> x2 <strong>*</strong> dx; <span style="color:#999988"><em>// backprop into x2</em></span>
<strong>var</strong> dd <strong>=</strong> x1 <strong>*</strong> dx2; <span style="color:#999988"><em>// backprop into d</em></span>
<strong>var</strong> dx1 <strong>=</strong> d <strong>*</strong> dx2; <span style="color:#999988"><em>// backprop into x1</em></span>
<strong>var</strong> da <strong>=</strong> b <strong>*</strong> dx1;
<strong>var</strong> db <strong>=</strong> a <strong>*</strong> dx1;
<strong>var</strong> dc <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx1; <span style="color:#999988"><em>// done!</em></span>
</code></span>

那不是太难!这些是整个表达式的backprop方程式,我们已经逐个完成它们并将其反馈到所有变量。再次注意,在正向传递期间,每个变量如何在向后传递期间具有等效变量,该变量包含相对于电路最终输出的梯度。以下是一些有用的函数及其在实践中有用的局部渐变:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> <span style="color:#009999">1.0</span><strong>/</strong>a; <span style="color:#999988"><em>// division</em></span>
<strong>var</strong> da <strong>=</strong> <strong>-</strong><span style="color:#009999">1.0</span><strong>/</strong>(a<strong>*</strong>a);
</code></span>

这是分区在实践中的样子:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> (a <strong>+</strong> b)<strong>/</strong>(c <strong>+</strong> d);
<span style="color:#999988"><em>// lets decompose it in steps:</em></span>
<strong>var</strong> x1 <strong>=</strong> a <strong>+</strong> b;
<strong>var</strong> x2 <strong>=</strong> c <strong>+</strong> d;
<strong>var</strong> x3 <strong>=</strong> <span style="color:#009999">1.0</span> <strong>/</strong> x2;
<strong>var</strong> x <strong>=</strong> x1 <strong>*</strong> x3; <span style="color:#999988"><em>// equivalent to above</em></span>
<span style="color:#999988"><em>// and now backprop, again in reverse order:</em></span>
<strong>var</strong> dx1 <strong>=</strong> x3 <strong>*</strong> dx;
<strong>var</strong> dx3 <strong>=</strong> x1 <strong>*</strong> dx;
<strong>var</strong> dx2 <strong>=</strong> (<strong>-</strong><span style="color:#009999">1.0</span><strong>/</strong>(x2<strong>*</strong>x2)) <strong>*</strong> dx3; <span style="color:#999988"><em>// local gradient as shown above, and chain rule</em></span>
<strong>var</strong> da <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx1; <span style="color:#999988"><em>// and finally into the original variables</em></span>
<strong>var</strong> db <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx1;
<strong>var</strong> dc <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx2;
<strong>var</strong> dd <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dx2;
</code></span>

希望你看到我们正在分解表达式,进行正向传递,然后对于每个变量(例如a),我们逐渐da向后推导它的渐变,应用简单的局部渐变并用上面的渐变链接它们。这是另一个:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> <span style="color:#0086b3">Math</span>.max(a, b);
<strong>var</strong> da <strong>=</strong> a <strong>===</strong> x ? <span style="color:#009999">1.0</span> <strong>*</strong> dx : <span style="color:#009999">0.0</span>;
<strong>var</strong> db <strong>=</strong> b <strong>===</strong> x ? <span style="color:#009999">1.0</span> <strong>*</strong> dx : <span style="color:#009999">0.0</span>;
</code></span>

好的,这是一个很难读的非常简单的事情。该max函数传递最大的输入值,忽略其他值。然后,在向后传递中,最大门将简单地将梯度置于顶部并将其路由到在前向传递期间实际流过它的输入。门作为一个简单的开关,根据哪个输入在正向传递期间具有最高值。其他输入将具有零梯度。这===是关于什么的,因为我们正在测试哪个输入是实际最大值并且仅将梯度路由到它。

最后,让我们看一下您可能听说过的整流线性单元非线性(或ReLU)。它用于神经网络代替S形函数。它只是在零处阈值:

<span style="color:#000000"><code><strong>var</strong> x <strong>=</strong> <span style="color:#0086b3">Math</span>.max(a, <span style="color:#009999">0</span>)
<span style="color:#999988"><em>// backprop through this gate will then be:</em></span>
<strong>var</strong> da <strong>=</strong> a <strong>></strong> <span style="color:#009999">0</span> ? <span style="color:#009999">1.0</span> <strong>*</strong> dx : <span style="color:#009999">0.0</span>;
</code></span>

换句话说,如果该值大于0,则该门只是通过值,或者它停止流并将其设置为零。在向后传递中,如果在forawrd传递期间激活门,则门将从顶部传递渐变,或者如果原始输入低于零,则将停止渐变流。

我会在这一刻停下来。我希望你有一些关于如何计算整个表达式(它是由沿途的许多门构成)以及如何为每个表达式计算backprop的直觉。

我们在本章中所做的一切都归结为:我们看到我们可以通过任意复杂的实值电路提供一些输入,用一些力在电路末端拉动,反向传播将整个电路分配到整个电路回到投入的方式。如果输入沿着拖船的最终方向略微响应,则电路将沿原始拉动方向“给予”一点。也许这不是很明显,但这种机器是机器学习的强大锤子

“也许这不是很明显,但这种机器对于机器学习来说是一个强有力的锤子。”

让我们现在好好利用这台机器。

第2章:机器学习

在最后一章中,我们关注的是实值电路,它们可能计算出输入的复杂表达式(正向通道),并且我们也可以在原始输入(后向通道)上计算这些表达式的梯度。在本章中,我们将看到这种极其简单的机制在机器学习中的用处。

二进制分类

正如我们之前所做的那样,让我们​​开始简单。机器学习中最简单,最常见且非常实用的问题是二元分类。可以减少许多非常有趣和重要的问题。设置如下:我们给出了一个N向量数据集,其中每个都用a +1或a 标记-1。例如,在二维中,我们的数据集看起来很简单:

<span style="color:#000000"><code>vector -> label
---------------
[1.2, 0.7] -> +1
[-0.3, 0.5] -> -1
[-3, -1] -> +1
[0.1, 1.0] -> -1
[3.0, 1.1] -> -1
[2.1, -3] -> +1
</code></span>

在这里,我们有N = 6 数据点,每个数据点都有两个特征D = 2)。其中三个数据点有标签 +1,另外三个有标签-1。这是一个愚蠢的玩具示例,但实际上,+ 1 / -1数据集确实非常有用:例如垃圾邮件/无垃圾邮件,其中向量以某种方式衡量电子邮件内容的各种功能,例如数字有时提到某些增强药物。

目标。我们在二进制分类中的目标是学习一个采用二维向量并预测标签的函数。此函数通常由一组参数进行参数化,我们希望调整函数的参数,使其输出与提供的数据集中的标签一致。最后,我们可以丢弃数据集并使用学习的参数来预测以前看不见的矢量的标签。

培训协议

我们最终将构建整个神经网络和复杂表达式,但让我们开始简单并训练一个线性分类器,它与我们在第1章末尾看到的单个神经元非常相似。唯一的区别是我们将摆脱它sigmoid因为它使事情变得不必要地复杂化(我在第1章中仅将它用作例子,因为Sigmoid神经​​元在历史上很流行,但现代神经网络很少,如果有的话,使用S形非线性)。无论如何,让我们使用一个简单的线性函数:

在这个表达式中,我们考虑xy作为输入(2D向量)和a,b,c我们想要学习的函数的参数。例如,如果a = 1, b = -2, c = -1,则该函数将采用第一个datapoint([1.2, 0.7])和输出1 * 1.2 + (-2) * 0.7 + (-1) = -1.2。以下是培训的工作方式:

  1. 我们选择一个随机数据点并通过电路馈送它
  2. 我们将把电路的输出解释为数据点具有类的置信度+1。(即非常高的值=电路是非常确定的数据点具有类+1和非常低的值=电路确定此数据点具有类-1。)
  3. 我们将测量预测与提供的标签对齐的程度。直观地说,例如,如果一个正面的例子得分非常低,我们就会想要在电路上拉出正方向,要求它应该为这个数据点输出更高的值。请注意,这是第一个数据点的情况:它被标记为+1但我们的预测器功能仅为其赋值-1.2。因此,我们将朝着积极的方向拉动电路; 我们希望价值更高。
  4. 该电路将采用拖船并反向传播它以计算输入上的拖船 a,b,c,x,y
  5. 由于我们认为x,y是(固定的)数据点,我们将忽略拉动x,y。如果你是我的物理类比的粉丝,可以将这些输入视为固定在地面上的钉子。
  6. 另一方面,我们将采用参数a,b,c并使它们响应他们的拖船(即我们将执行我们称之为参数更新的)。当然,这将使得电路将来在该特定数据点上输出稍高的分数。
  7. 重复!回到第1步。

我上面描述的训练方案通常称为随机梯度下降。我想重申的有趣部分是,a,b,c,x,y就电路而言,它们都是由相同的东西组成的:它们是电路的输入,电路将在某个方向上牵引所有电路。它不知道参数和数据点之间的区别。但是,在向后传递完成之后,我们忽略datapoints(x,y)上的所有拖动,并在我们迭代数据集中的示例时将它们交换进去。另一方面,我们保留参数(a,b,c每次我们采样数据点时,都会一直拖着它们。随着时间的推移,这些参数的拉动将调整这些值,使得函数输出正例的高分和负例的低分。

学习支持向量机

作为一个具体的例子,让我们学习支持向量机。SVM是一种非常流行的线性分类器; 它的功能形式与我在上一节中所描述的完全相同,\(f(x,y)= ax + by + c \)。在这一点上,如果您已经看过SVM的解释,您可能希望我定义SVM损失函数并深入解释松弛变量,大边距,内核,二元性等的几何直觉。但在这里,我我想采取不同的方法。我不想确定损失函数,而是根据力规范来解释(我只是顺便说一下这个术语)支持向量机,我个人觉得它更直观。正如我们将要看到的,谈论力规范和损失函数是看到同样问题的相同方式。无论如何,这里是:

支持向量机“强制规范”:

  • 如果我们通过SVM电路馈送正数据点并且输出值小于1,则用力拉动电路+1。这是一个积极的例子,所以我们希望得分更高。
  • 相反,如果我们通过SVM输入负数据点并且输出大于-1,那么电路会给这个数据点带来危险的高分:用力向下拉电路-1
  • 除了上面的拉力,总是在参数上添加少量拉力a,b(注意,而不是c!),将它们拉向零。您可以将两者都a,b视为附着在零附着的物理弹簧上。就像一个物理弹簧一样,这将使每个人的价值a,b(欧胡克的物理定律,任何人?)都有所提升。例如,如果a变得非常高,它将经历强度拉|a|回到零。这种拉力是我们称之为正则化的东西,它确保我们的参数都不会过大ab不成比例地大。这是不合需要的,因为它们a,b都会与输入特征相乘x,y(请记住等式是a*x + b*y + c),所以如果它们中的任何一个太高,我们的分类器就会对这些特征过于敏感。这不是一个很好的属性,因为在实践中功能通常会很嘈杂,所以我们希望我们的分类器在它们摆动时相对平滑地改变。

让我们快速通过一个小但具体的例子。假设我们从随机参数设置开始,比如说a = 1, b = -2, c = -1。然后:

  • 如果我们提供点[1.2, 0.7],SVM将计算得分1 * 1.2 + (-2) * 0.7 - 1 = -1.2。这一点+1在训练数据中被标记,因此我们希望得分高于1.因此,电路顶部的梯度将是正的:+1,它将反向传播到a,b,c。此外,也将有一个正则拉a-1(以使它更小)和正规化拉了一b+2使其变大,趋向于零。
  • 假设我们将数据点[-0.3, 0.5]输入SVM。它计算1 * (-0.3) + (-2) * 0.5 - 1 = -2.3。这一点的标签是-1,并且因为-2.3小于-1,我们看到根据我们的力量规范,SVM应该很高兴:计算得分非常负,与本例的负标签一致。电路末端没有拉力(即它为零),因为不需要进行任何改变。然而,将仍然是对正规化拉a-1和对b+2

好的,文字太多了。让我们编写SVM代码并利用第1章中的电路机制:

<span style="color:#000000"><code><span style="color:#999988"><em>// A circuit: it takes 5 Units (x,y,a,b,c) and outputs a single Unit</em></span>
<span style="color:#999988"><em>// It can also compute the gradient w.r.t. its inputs</em></span>
<strong>var</strong> Circuit <strong>=</strong> <strong>function</strong>() {
  <span style="color:#999988"><em>// create some gates</em></span>
  <strong>this</strong>.mulg0 <strong>=</strong> <strong>new</strong> multiplyGate();
  <strong>this</strong>.mulg1 <strong>=</strong> <strong>new</strong> multiplyGate();
  <strong>this</strong>.addg0 <strong>=</strong> <strong>new</strong> addGate();
  <strong>this</strong>.addg1 <strong>=</strong> <strong>new</strong> addGate();
};
Circuit.prototype <strong>=</strong> {
  <span style="color:#008080">forward</span>: <strong>function</strong>(x,y,a,b,c) {
    <strong>this</strong>.ax <strong>=</strong> <strong>this</strong>.mulg0.forward(a, x); <span style="color:#999988"><em>// a*x</em></span>
    <strong>this</strong>.by <strong>=</strong> <strong>this</strong>.mulg1.forward(b, y); <span style="color:#999988"><em>// b*y</em></span>
    <strong>this</strong>.axpby <strong>=</strong> <strong>this</strong>.addg0.forward(<strong>this</strong>.ax, <strong>this</strong>.by); <span style="color:#999988"><em>// a*x + b*y</em></span>
    <strong>this</strong>.axpbypc <strong>=</strong> <strong>this</strong>.addg1.forward(<strong>this</strong>.axpby, c); <span style="color:#999988"><em>// a*x + b*y + c</em></span>
    <strong>return</strong> <strong>this</strong>.axpbypc;
  },
  <span style="color:#008080">backward</span>: <strong>function</strong>(gradient_top) { <span style="color:#999988"><em>// takes pull from above</em></span>
    <strong>this</strong>.axpbypc.grad <strong>=</strong> gradient_top;
    <strong>this</strong>.addg1.backward(); <span style="color:#999988"><em>// sets gradient in axpby and c</em></span>
    <strong>this</strong>.addg0.backward(); <span style="color:#999988"><em>// sets gradient in ax and by</em></span>
    <strong>this</strong>.mulg1.backward(); <span style="color:#999988"><em>// sets gradient in b and y</em></span>
    <strong>this</strong>.mulg0.backward(); <span style="color:#999988"><em>// sets gradient in a and x</em></span>
  }
}
</code></span>

这是一个简单计算a*x + b*y + c并且还可以计算梯度的电路。它使用我们在第1章中开发的门代码。现在让我们编写SVM,它不关心实际的电路。它只关注它产生的价值,它会拉动电路。

<span style="color:#000000"><code><span style="color:#999988"><em>// SVM class</em></span>
<strong>var</strong> SVM <strong>=</strong> <strong>function</strong>() {
  
  <span style="color:#999988"><em>// random initial parameter values</em></span>
  <strong>this</strong>.a <strong>=</strong> <strong>new</strong> Unit(<span style="color:#009999">1.0</span>, <span style="color:#009999">0.0</span>); 
  <strong>this</strong>.b <strong>=</strong> <strong>new</strong> Unit(<strong>-</strong><span style="color:#009999">2.0</span>, <span style="color:#009999">0.0</span>);
  <strong>this</strong>.c <strong>=</strong> <strong>new</strong> Unit(<strong>-</strong><span style="color:#009999">1.0</span>, <span style="color:#009999">0.0</span>);

  <strong>this</strong>.circuit <strong>=</strong> <strong>new</strong> Circuit();
};
SVM.prototype <strong>=</strong> {
  <span style="color:#008080">forward</span>: <strong>function</strong>(x, y) { <span style="color:#999988"><em>// assume x and y are Units</em></span>
    <strong>this</strong>.unit_out <strong>=</strong> <strong>this</strong>.circuit.forward(x, y, <strong>this</strong>.a, <strong>this</strong>.b, <strong>this</strong>.c);
    <strong>return</strong> <strong>this</strong>.unit_out;
  },
  <span style="color:#008080">backward</span>: <strong>function</strong>(label) { <span style="color:#999988"><em>// label is +1 or -1</em></span>

    <span style="color:#999988"><em>// reset pulls on a,b,c</em></span>
    <strong>this</strong>.a.grad <strong>=</strong> <span style="color:#009999">0.0</span>; 
    <strong>this</strong>.b.grad <strong>=</strong> <span style="color:#009999">0.0</span>; 
    <strong>this</strong>.c.grad <strong>=</strong> <span style="color:#009999">0.0</span>;

    <span style="color:#999988"><em>// compute the pull based on what the circuit output was</em></span>
    <strong>var</strong> pull <strong>=</strong> <span style="color:#009999">0.0</span>;
    <strong>if</strong>(label <strong>===</strong> <span style="color:#009999">1</span> <strong>&&</strong> <strong>this</strong>.unit_out.value <strong><</strong> <span style="color:#009999">1</span>) { 
      pull <strong>=</strong> <span style="color:#009999">1</span>; <span style="color:#999988"><em>// the score was too low: pull up</em></span>
    }
    <strong>if</strong>(label <strong>===</strong> <strong>-</strong><span style="color:#009999">1</span> <strong>&&</strong> <strong>this</strong>.unit_out.value <strong>></strong> <strong>-</strong><span style="color:#009999">1</span>) {
      pull <strong>=</strong> <strong>-</strong><span style="color:#009999">1</span>; <span style="color:#999988"><em>// the score was too high for a positive example, pull down</em></span>
    }
    <strong>this</strong>.circuit.backward(pull); <span style="color:#999988"><em>// writes gradient into x,y,a,b,c</em></span>
    
    <span style="color:#999988"><em>// add regularization pull for parameters: towards zero and proportional to value</em></span>
    <strong>this</strong>.a.grad <strong>+=</strong> <strong>-</strong><strong>this</strong>.a.value;
    <strong>this</strong>.b.grad <strong>+=</strong> <strong>-</strong><strong>this</strong>.b.value;
  },
  <span style="color:#008080">learnFrom</span>: <strong>function</strong>(x, y, label) {
    <strong>this</strong>.forward(x, y); <span style="color:#999988"><em>// forward pass (set .value in all Units)</em></span>
    <strong>this</strong>.backward(label); <span style="color:#999988"><em>// backward pass (set .grad in all Units)</em></span>
    <strong>this</strong>.parameterUpdate(); <span style="color:#999988"><em>// parameters respond to tug</em></span>
  },
  <span style="color:#008080">parameterUpdate</span>: <strong>function</strong>() {
    <strong>var</strong> step_size <strong>=</strong> <span style="color:#009999">0.01</span>;
    <strong>this</strong>.a.value <strong>+=</strong> step_size <strong>*</strong> <strong>this</strong>.a.grad;
    <strong>this</strong>.b.value <strong>+=</strong> step_size <strong>*</strong> <strong>this</strong>.b.grad;
    <strong>this</strong>.c.value <strong>+=</strong> step_size <strong>*</strong> <strong>this</strong>.c.grad;
  }
};
</code></span>

现在让我们用随机梯度下降训练SVM:

<span style="color:#000000"><code><strong>var</strong> data <strong>=</strong> []; <strong>var</strong> labels <strong>=</strong> [];
data.push([<span style="color:#009999">1.2</span>, <span style="color:#009999">0.7</span>]); labels.push(<span style="color:#009999">1</span>);
data.push([<strong>-</strong><span style="color:#009999">0.3</span>, <strong>-</strong><span style="color:#009999">0.5</span>]); labels.push(<strong>-</strong><span style="color:#009999">1</span>);
data.push([<span style="color:#009999">3.0</span>, <span style="color:#009999">0.1</span>]); labels.push(<span style="color:#009999">1</span>);
data.push([<strong>-</strong><span style="color:#009999">0.1</span>, <strong>-</strong><span style="color:#009999">1.0</span>]); labels.push(<strong>-</strong><span style="color:#009999">1</span>);
data.push([<strong>-</strong><span style="color:#009999">1.0</span>, <span style="color:#009999">1.1</span>]); labels.push(<strong>-</strong><span style="color:#009999">1</span>);
data.push([<span style="color:#009999">2.1</span>, <strong>-</strong><span style="color:#009999">3</span>]); labels.push(<span style="color:#009999">1</span>);
<strong>var</strong> svm <strong>=</strong> <strong>new</strong> SVM();

<span style="color:#999988"><em>// a function that computes the classification accuracy</em></span>
<strong>var</strong> evalTrainingAccuracy <strong>=</strong> <strong>function</strong>() {
  <strong>var</strong> num_correct <strong>=</strong> <span style="color:#009999">0</span>;
  <strong>for</strong>(<strong>var</strong> i <strong>=</strong> <span style="color:#009999">0</span>; i <strong><</strong> data.length; i<strong>++</strong>) {
    <strong>var</strong> x <strong>=</strong> <strong>new</strong> Unit(data[i][<span style="color:#009999">0</span>], <span style="color:#009999">0.0</span>);
    <strong>var</strong> y <strong>=</strong> <strong>new</strong> Unit(data[i][<span style="color:#009999">1</span>], <span style="color:#009999">0.0</span>);
    <strong>var</strong> true_label <strong>=</strong> labels[i];

    <span style="color:#999988"><em>// see if the prediction matches the provided label</em></span>
    <strong>var</strong> predicted_label <strong>=</strong> svm.forward(x, y).value <strong>></strong> <span style="color:#009999">0</span> ? <span style="color:#009999">1</span> : <strong>-</strong><span style="color:#009999">1</span>;
    <strong>if</strong>(predicted_label <strong>===</strong> true_label) {
      num_correct<strong>++</strong>;
    }
  }
  <strong>return</strong> num_correct <strong>/</strong> data.length;
};

<span style="color:#999988"><em>// the learning loop</em></span>
<strong>for</strong>(<strong>var</strong> iter <strong>=</strong> <span style="color:#009999">0</span>; iter <strong><</strong> <span style="color:#009999">400</span>; iter<strong>++</strong>) {
  <span style="color:#999988"><em>// pick a random data point</em></span>
  <strong>var</strong> i <strong>=</strong> <span style="color:#0086b3">Math</span>.floor(<span style="color:#0086b3">Math</span>.random() <strong>*</strong> data.length);
  <strong>var</strong> x <strong>=</strong> <strong>new</strong> Unit(data[i][<span style="color:#009999">0</span>], <span style="color:#009999">0.0</span>);
  <strong>var</strong> y <strong>=</strong> <strong>new</strong> Unit(data[i][<span style="color:#009999">1</span>], <span style="color:#009999">0.0</span>);
  <strong>var</strong> label <strong>=</strong> labels[i];
  svm.learnFrom(x, y, label);

  <strong>if</strong>(iter <strong>%</strong> <span style="color:#009999">25</span> <strong>==</strong> <span style="color:#009999">0</span>) { <span style="color:#999988"><em>// every 10 iterations... </em></span>
    console.log(<span style="color:#dd1144">'training accuracy at iter '</span> <strong>+</strong> iter <strong>+</strong> <span style="color:#dd1144">': '</span> <strong>+</strong> evalTrainingAccuracy());
  }
}
</code></span>

此代码打印以下输出:

<span style="color:#000000"><code>training accuracy at iteration 0: 0.3333333333333333
training accuracy at iteration 25: 0.3333333333333333
training accuracy at iteration 50: 0.5
training accuracy at iteration 75: 0.5
training accuracy at iteration 100: 0.3333333333333333
training accuracy at iteration 125: 0.5
training accuracy at iteration 150: 0.5
training accuracy at iteration 175: 0.5
training accuracy at iteration 200: 0.5
training accuracy at iteration 225: 0.6666666666666666
training accuracy at iteration 250: 0.6666666666666666
training accuracy at iteration 275: 0.8333333333333334
training accuracy at iteration 300: 1
training accuracy at iteration 325: 1
training accuracy at iteration 350: 1
training accuracy at iteration 375: 1 
</code></span>

我们看到,最初我们的分类器只有33%的训练精度,但最后所有训练样例都是正确的分类器,因为参数a,b,c根据我们施加的拉力调整了它们的值。我们刚刚训练了一个SVM!但请不要在生产中的任何地方使用此代码:)我们将看到,一旦我们了解核心的内容,我们将如何使事情变得更有效率。

需要的迭代次数。使用此示例数据,通过此示例初始化,以及我们使用的步长设置,需要大约300次迭代来训练SVM。在实践中,这可能会更多或更少,具体取决于问题的严重程度,初始化方式,数据规范化,您正在使用的步长等等。这只是一个玩具演示,但稍后我们将介绍在实践中实际训练这些分类器的所有最佳实践。例如,事实证明步长的设置是非常重要和棘手的。小步长将使您的模型训练缓慢。大步长训练会更快,但如果它太大,它会使你的分类器混乱地跳跃而不会收敛到一个好的最终结果。

我希望您了解的一点是,电路可以是任意表达式,而不仅仅是我们在本例中使用的线性预测函数。例如,它可以是整个神经网络。

顺便说一下,我故意以模块化方式构建代码,但我们可以用更简单的代码训练SVM。以下是所有这些类和计算归结为:

<span style="color:#000000"><code><strong>var</strong> a <strong>=</strong> <span style="color:#009999">1</span>, b <strong>=</strong> <strong>-</strong><span style="color:#009999">2</span>, c <strong>=</strong> <strong>-</strong><span style="color:#009999">1</span>; <span style="color:#999988"><em>// initial parameters</em></span>
<strong>for</strong>(<strong>var</strong> iter <strong>=</strong> <span style="color:#009999">0</span>; iter <strong><</strong> <span style="color:#009999">400</span>; iter<strong>++</strong>) {
  <span style="color:#999988"><em>// pick a random data point</em></span>
  <strong>var</strong> i <strong>=</strong> <span style="color:#0086b3">Math</span>.floor(<span style="color:#0086b3">Math</span>.random() <strong>*</strong> data.length);
  <strong>var</strong> x <strong>=</strong> data[i][<span style="color:#009999">0</span>];
  <strong>var</strong> y <strong>=</strong> data[i][<span style="color:#009999">1</span>];
  <strong>var</strong> label <strong>=</strong> labels[i];

  <span style="color:#999988"><em>// compute pull</em></span>
  <strong>var</strong> score <strong>=</strong> a<strong>*</strong>x <strong>+</strong> b<strong>*</strong>y <strong>+</strong> c;
  <strong>var</strong> pull <strong>=</strong> <span style="color:#009999">0.0</span>;
  <strong>if</strong>(label <strong>===</strong> <span style="color:#009999">1</span> <strong>&&</strong> score <strong><</strong> <span style="color:#009999">1</span>) pull <strong>=</strong> <span style="color:#009999">1</span>;
  <strong>if</strong>(label <strong>===</strong> <strong>-</strong><span style="color:#009999">1</span> <strong>&&</strong> score <strong>></strong> <strong>-</strong><span style="color:#009999">1</span>) pull <strong>=</strong> <strong>-</strong><span style="color:#009999">1</span>;

  <span style="color:#999988"><em>// compute gradient and update parameters</em></span>
  <strong>var</strong> step_size <strong>=</strong> <span style="color:#009999">0.01</span>;
  a <strong>+=</strong> step_size <strong>*</strong> (x <strong>*</strong> pull <strong>-</strong> a); <span style="color:#999988"><em>// -a is from the regularization</em></span>
  b <strong>+=</strong> step_size <strong>*</strong> (y <strong>*</strong> pull <strong>-</strong> b); <span style="color:#999988"><em>// -b is from the regularization</em></span>
  c <strong>+=</strong> step_size <strong>*</strong> (<span style="color:#009999">1</span> <strong>*</strong> pull);
}
</code></span>

此代码给出了相同的结果。也许现在你可以看一下代码,看看这些方程是如何产生的。

可变拉?此时要做的快速说明:您可能已经注意到拉力总是为1,0或-1。你可以想象做其他事情,例如让这个拉力与错误的程度成正比。这导致SVM的变化,有些人将其称为平方铰链损失 SVM,其原因稍后将变得清晰。根据数据集的各种功能,可能会更好或更差。例如,如果您的数据中存在非常糟糕的异常值,例如得分的负数据点+100,则其对我们的分类器的影响相对较小,因为-1无论错误有多严重,我们都只会用力拉动。在实践中,我们将分类器的这个属性称为对异常值的鲁棒性

让我们回顾一下。我们引入了二元分类问题,其中给出了N个D维向量和每个标签+ 1 / -1。我们看到我们可以将这些特征与实值电路内的一组参数(例如我们示例中的支持向量机电路)结合起来。然后,我们可以重复通过电路传递数据,每次调整参数,使电路的输出值与提供的标签一致。重要的是,调整依赖于我们通过电路反向传播梯度的能力。最后,最终电路可用于预测看不见的实例的值!

将SVM推广到神经网络

令人感兴趣的是,SVM只是一种特殊类型的非常简单的电路(计算权重和数据点的score = a*x + b*y + c位置的电路)。这可以很容易地扩展到更复杂的功能。例如,让我们编写一个执行二进制分类的2层神经网络。前锋将如下所示:a,b,cx,y

<span style="color:#000000"><code><span style="color:#999988"><em>// assume inputs x,y</em></span>
<strong>var</strong> n1 <strong>=</strong> <span style="color:#0086b3">Math</span>.max(<span style="color:#009999">0</span>, a1<strong>*</strong>x <strong>+</strong> b1<strong>*</strong>y <strong>+</strong> c1); <span style="color:#999988"><em>// activation of 1st hidden neuron</em></span>
<strong>var</strong> n2 <strong>=</strong> <span style="color:#0086b3">Math</span>.max(<span style="color:#009999">0</span>, a2<strong>*</strong>x <strong>+</strong> b2<strong>*</strong>y <strong>+</strong> c2); <span style="color:#999988"><em>// 2nd neuron</em></span>
<strong>var</strong> n3 <strong>=</strong> <span style="color:#0086b3">Math</span>.max(<span style="color:#009999">0</span>, a3<strong>*</strong>x <strong>+</strong> b3<strong>*</strong>y <strong>+</strong> c3); <span style="color:#999988"><em>// 3rd neuron</em></span>
<strong>var</strong> score <strong>=</strong> a4<strong>*</strong>n1 <strong>+</strong> b4<strong>*</strong>n2 <strong>+</strong> c4<strong>*</strong>n3 <strong>+</strong> d4; <span style="color:#999988"><em>// the score</em></span>
</code></span>

上述规范是一个2层神经网络,具有3个隐藏神经元(n1,n2,n3),在每个隐藏神经元上使用整流线性单位(ReLU)非线性。如您所见,现在涉及到几个参数,这意味着我们的分类器更复杂,并且可以表示比简单的线性决策规则(如SVM)更复杂的决策边界。考虑它的另一种方式是三个隐藏神经元中的每一个都是线性分类器,现在我们在它上面放置一个额外的线性分类器。现在我们开始深入了解 :)。好吧,让我们训练这个2层神经网络。代码看起来与上面的SVM示例代码非常相似,我们只需要更改前向传递和后向传递:

<span style="color:#000000"><code><span style="color:#999988"><em>// random initial parameters</em></span>
<strong>var</strong> a1 <strong>=</strong> <span style="color:#0086b3">Math</span>.random() <strong>-</strong> <span style="color:#009999">0.5</span>; <span style="color:#999988"><em>// a random number between -0.5 and 0.5</em></span>
<span style="color:#999988"><em>// ... similarly initialize all other parameters to randoms</em></span>
<strong>for</strong>(<strong>var</strong> iter <strong>=</strong> <span style="color:#009999">0</span>; iter <strong><</strong> <span style="color:#009999">400</span>; iter<strong>++</strong>) {
  <span style="color:#999988"><em>// pick a random data point</em></span>
  <strong>var</strong> i <strong>=</strong> <span style="color:#0086b3">Math</span>.floor(<span style="color:#0086b3">Math</span>.random() <strong>*</strong> data.length);
  <strong>var</strong> x <strong>=</strong> data[i][<span style="color:#009999">0</span>];
  <strong>var</strong> y <strong>=</strong> data[i][<span style="color:#009999">1</span>];
  <strong>var</strong> label <strong>=</strong> labels[i];

  <span style="color:#999988"><em>// compute forward pass</em></span>
  <strong>var</strong> n1 <strong>=</strong> <span style="color:#0086b3">Math</span>.max(<span style="color:#009999">0</span>, a1<strong>*</strong>x <strong>+</strong> b1<strong>*</strong>y <strong>+</strong> c1); <span style="color:#999988"><em>// activation of 1st hidden neuron</em></span>
  <strong>var</strong> n2 <strong>=</strong> <span style="color:#0086b3">Math</span>.max(<span style="color:#009999">0</span>, a2<strong>*</strong>x <strong>+</strong> b2<strong>*</strong>y <strong>+</strong> c2); <span style="color:#999988"><em>// 2nd neuron</em></span>
  <strong>var</strong> n3 <strong>=</strong> <span style="color:#0086b3">Math</span>.max(<span style="color:#009999">0</span>, a3<strong>*</strong>x <strong>+</strong> b3<strong>*</strong>y <strong>+</strong> c3); <span style="color:#999988"><em>// 3rd neuron</em></span>
  <strong>var</strong> score <strong>=</strong> a4<strong>*</strong>n1 <strong>+</strong> b4<strong>*</strong>n2 <strong>+</strong> c4<strong>*</strong>n3 <strong>+</strong> d4; <span style="color:#999988"><em>// the score</em></span>

  <span style="color:#999988"><em>// compute the pull on top</em></span>
  <strong>var</strong> pull <strong>=</strong> <span style="color:#009999">0.0</span>;
  <strong>if</strong>(label <strong>===</strong> <span style="color:#009999">1</span> <strong>&&</strong> score <strong><</strong> <span style="color:#009999">1</span>) pull <strong>=</strong> <span style="color:#009999">1</span>; <span style="color:#999988"><em>// we want higher output! Pull up.</em></span>
  <strong>if</strong>(label <strong>===</strong> <strong>-</strong><span style="color:#009999">1</span> <strong>&&</strong> score <strong>></strong> <strong>-</strong><span style="color:#009999">1</span>) pull <strong>=</strong> <strong>-</strong><span style="color:#009999">1</span>; <span style="color:#999988"><em>// we want lower output! Pull down.</em></span>

  <span style="color:#999988"><em>// now compute backward pass to all parameters of the model</em></span>

  <span style="color:#999988"><em>// backprop through the last "score" neuron</em></span>
  <strong>var</strong> dscore <strong>=</strong> pull;
  <strong>var</strong> da4 <strong>=</strong> n1 <strong>*</strong> dscore;
  <strong>var</strong> dn1 <strong>=</strong> a4 <strong>*</strong> dscore;
  <strong>var</strong> db4 <strong>=</strong> n2 <strong>*</strong> dscore;
  <strong>var</strong> dn2 <strong>=</strong> b4 <strong>*</strong> dscore;
  <strong>var</strong> dc4 <strong>=</strong> n3 <strong>*</strong> dscore;
  <strong>var</strong> dn3 <strong>=</strong> c4 <strong>*</strong> dscore;
  <strong>var</strong> dd4 <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dscore; <span style="color:#999988"><em>// phew</em></span>

  <span style="color:#999988"><em>// backprop the ReLU non-linearities, in place</em></span>
  <span style="color:#999988"><em>// i.e. just set gradients to zero if the neurons did not "fire"</em></span>
  <strong>var</strong> dn3 <strong>=</strong> n3 <strong>===</strong> <span style="color:#009999">0</span> ? <span style="color:#009999">0</span> : dn3;
  <strong>var</strong> dn2 <strong>=</strong> n2 <strong>===</strong> <span style="color:#009999">0</span> ? <span style="color:#009999">0</span> : dn2;
  <strong>var</strong> dn1 <strong>=</strong> n1 <strong>===</strong> <span style="color:#009999">0</span> ? <span style="color:#009999">0</span> : dn1;

  <span style="color:#999988"><em>// backprop to parameters of neuron 1</em></span>
  <strong>var</strong> da1 <strong>=</strong> x <strong>*</strong> dn1;
  <strong>var</strong> db1 <strong>=</strong> y <strong>*</strong> dn1;
  <strong>var</strong> dc1 <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dn1;
  
  <span style="color:#999988"><em>// backprop to parameters of neuron 2</em></span>
  <strong>var</strong> da2 <strong>=</strong> x <strong>*</strong> dn2;
  <strong>var</strong> db2 <strong>=</strong> y <strong>*</strong> dn2;
  <strong>var</strong> dc2 <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dn2;

  <span style="color:#999988"><em>// backprop to parameters of neuron 3</em></span>
  <strong>var</strong> da3 <strong>=</strong> x <strong>*</strong> dn3;
  <strong>var</strong> db3 <strong>=</strong> y <strong>*</strong> dn3;
  <strong>var</strong> dc3 <strong>=</strong> <span style="color:#009999">1.0</span> <strong>*</strong> dn3;

  <span style="color:#999988"><em>// phew! End of backprop!</em></span>
  <span style="color:#999988"><em>// note we could have also backpropped into x,y</em></span>
  <span style="color:#999988"><em>// but we do not need these gradients. We only use the gradients</em></span>
  <span style="color:#999988"><em>// on our parameters in the parameter update, and we discard x,y</em></span>

  <span style="color:#999988"><em>// add the pulls from the regularization, tugging all multiplicative</em></span>
  <span style="color:#999988"><em>// parameters (i.e. not the biases) downward, proportional to their value</em></span>
  da1 <strong>+=</strong> <strong>-</strong>a1; da2 <strong>+=</strong> <strong>-</strong>a2; da3 <strong>+=</strong> <strong>-</strong>a3;
  db1 <strong>+=</strong> <strong>-</strong>b1; db2 <strong>+=</strong> <strong>-</strong>b2; db3 <strong>+=</strong> <strong>-</strong>b3;
  da4 <strong>+=</strong> <strong>-</strong>a4; db4 <strong>+=</strong> <strong>-</strong>b4; dc4 <strong>+=</strong> <strong>-</strong>c4;

  <span style="color:#999988"><em>// finally, do the parameter update</em></span>
  <strong>var</strong> step_size <strong>=</strong> <span style="color:#009999">0.01</span>;
  a1 <strong>+=</strong> step_size <strong>*</strong> da1; 
  b1 <strong>+=</strong> step_size <strong>*</strong> db1; 
  c1 <strong>+=</strong> step_size <strong>*</strong> dc1;
  a2 <strong>+=</strong> step_size <strong>*</strong> da2; 
  b2 <strong>+=</strong> step_size <strong>*</strong> db2;
  c2 <strong>+=</strong> step_size <strong>*</strong> dc2;
  a3 <strong>+=</strong> step_size <strong>*</strong> da3; 
  b3 <strong>+=</strong> step_size <strong>*</strong> db3; 
  c3 <strong>+=</strong> step_size <strong>*</strong> dc3;
  a4 <strong>+=</strong> step_size <strong>*</strong> da4; 
  b4 <strong>+=</strong> step_size <strong>*</strong> db4; 
  c4 <strong>+=</strong> step_size <strong>*</strong> dc4; 
  d4 <strong>+=</strong> step_size <strong>*</strong> dd4;
  <span style="color:#999988"><em>// wow this is tedious, please use for loops in prod.</em></span>
  <span style="color:#999988"><em>// we're done!</em></span>
}
</code></span>

这就是你训练神经网络的方法。显然,你想要很好地模块化你的代码,但我为你花了这个例子,希望它能使事情变得更加具体和简单。稍后,我们将在实现这些网络时查看最佳实践,并且我们将以模块化和更合理的方式更加整齐地构建代码。

但是现在,我希望你的意思是2层神经网络真的不是那么可怕的东西:我们写一个正向传递表达式,将最后的值解释为分数,然后我们在正面或负面的方向取决于我们希望当前特定示例的值。backprop之后的参数更新将确保当我们将来看到这个特定的例子时,网络将更有可能给我们一个我们想要的值,而不是它在更新之前给出的值。

更传统的方法:损失函数

现在我们已经了解了这些电路如何与数据一起工作的基础知识,让我们采用更传统的方法,您可能会在互联网上以及其他教程和书籍中看到它们。你不会看到人们谈论力量规格太多。相反,机器学习算法是根据损失函数(或成本函数目标)指定的。

当我开发这种形式主义时,我也想开始对我们如何命名变量和参数更加小心。我希望这些方程看起来与您在书中或其他教程中看到的类似,所以让我使用更多标准命名约定。

示例:二维支持向量机

让我们从一个二维SVM的例子开始。我们给出了一个\(N \)例子\((x_ {i0},x_ {i1})\)的数据集及其对应的标签\(y_ {i} \),它们可以是\(+ 1 / -1 \)分别为正面或负面的例子。最重要的是,你记得我们有三个参数\((w_0,w_1,w_2)\)。然后,SVM丢失函数定义如下:

请注意,由于第一个表达式中的阈值为零,正则化中的平方值,此表达式始终为正。我们的想法是希望这个表达式尽可能小。在我们深入研究它的一些细微之处之前,让我先把它翻译成代码:

<span style="color:#000000"><code><strong>var</strong> X <strong>=</strong> [ [<span style="color:#009999">1.2</span>, <span style="color:#009999">0.7</span>], [<strong>-</strong><span style="color:#009999">0.3</span>, <span style="color:#009999">0.5</span>], [<span style="color:#009999">3</span>, <span style="color:#009999">2.5</span>] ] <span style="color:#999988"><em>// array of 2-dimensional data</em></span>
<strong>var</strong> y <strong>=</strong> [<span style="color:#009999">1</span>, <strong>-</strong><span style="color:#009999">1</span>, <span style="color:#009999">1</span>] <span style="color:#999988"><em>// array of labels</em></span>
<strong>var</strong> w <strong>=</strong> [<span style="color:#009999">0.1</span>, <span style="color:#009999">0.2</span>, <span style="color:#009999">0.3</span>] <span style="color:#999988"><em>// example: random numbers</em></span>
<strong>var</strong> alpha <strong>=</strong> <span style="color:#009999">0.1</span>; <span style="color:#999988"><em>// regularization strength</em></span>

<strong>function</strong> cost(X, y, w) {
  
  <strong>var</strong> total_cost <strong>=</strong> <span style="color:#009999">0.0</span>; <span style="color:#999988"><em>// L, in SVM loss function above</em></span>
  N <strong>=</strong> X.length;
  <strong>for</strong>(<strong>var</strong> i<strong>=</strong><span style="color:#009999">0</span>;i<strong><</strong>N;i<strong>++</strong>) {
    <span style="color:#999988"><em>// loop over all data points and compute their score</em></span>
    <strong>var</strong> xi <strong>=</strong> X[i];
    <strong>var</strong> score <strong>=</strong> w[<span style="color:#009999">0</span>] <strong>*</strong> xi[<span style="color:#009999">0</span>] <strong>+</strong> w[<span style="color:#009999">1</span>] <strong>*</strong> xi[<span style="color:#009999">1</span>] <strong>+</strong> w[<span style="color:#009999">2</span>];
    
    <span style="color:#999988"><em>// accumulate cost based on how compatible the score is with the label</em></span>
    <strong>var</strong> yi <strong>=</strong> y[i]; <span style="color:#999988"><em>// label</em></span>
    <strong>var</strong> costi <strong>=</strong> <span style="color:#0086b3">Math</span>.max(<span style="color:#009999">0</span>, <strong>-</strong> yi <strong>*</strong> score <strong>+</strong> <span style="color:#009999">1</span>);
    console.log(<span style="color:#dd1144">'example '</span> <strong>+</strong> i <strong>+</strong> <span style="color:#dd1144">': xi = ('</span> <strong>+</strong> xi <strong>+</strong> <span style="color:#dd1144">') and label = '</span> <strong>+</strong> yi);
    console.log(<span style="color:#dd1144">'  score computed to be '</span> <strong>+</strong> score.toFixed(<span style="color:#009999">3</span>));
    console.log(<span style="color:#dd1144">'  => cost computed to be '</span> <strong>+</strong> costi.toFixed(<span style="color:#009999">3</span>));
    total_cost <strong>+=</strong> costi;
  }

  <span style="color:#999988"><em>// regularization cost: we want small weights</em></span>
  reg_cost <strong>=</strong> alpha <strong>*</strong> (w[<span style="color:#009999">0</span>]<strong>*</strong>w[<span style="color:#009999">0</span>] <strong>+</strong> w[<span style="color:#009999">1</span>]<strong>*</strong>w[<span style="color:#009999">1</span>])
  console.log(<span style="color:#dd1144">'regularization cost for current model is '</span> <strong>+</strong> reg_cost.toFixed(<span style="color:#009999">3</span>));
  total_cost <strong>+=</strong> reg_cost;

  console.log(<span style="color:#dd1144">'total cost is '</span> <strong>+</strong> total_cost.toFixed(<span style="color:#009999">3</span>));
  <strong>return</strong> total_cost;
}
</code></span>

这是输出:

<span style="color:#000000"><code>cost for example 0 is 0.440
cost for example 1 is 1.370
cost for example 2 is 0.000
regularization cost for current model is 0.005
total cost is 1.815 
</code></span>

注意这个表达式是如何工作的:它衡量我们的SVM分类器有多糟糕。让我们明确地逐步完成:

  • xi = [1.2, 0.7]带标签的第一个数据yi = 1点将给出分数0.1*1.2 + 0.2*0.7 + 0.3,即0.56。请注意,这是一个积极的例子,所以我们希望得分大于+10.56是不足够的。实际上,此数据点的成本表达式将计算:costi = Math.max(0, -1*0.56 + 1),即0.44。您可以将成本视为量化SVM的不幸。
  • xi = [-0.3, 0.5]带标签的第二个数据yi = -1点将给出分数0.1*(-0.3) + 0.2*0.5 + 0.3,即0.37。这看起来不太好:这个分数对于一个负面的例子非常高。它应该小于-1。实际上,当我们计算成本时costi = Math.max(0, 1*0.37 + 1),我们得到了1.37。从这个例子来看,这是一个非常高的成本,因为它被错误分类。
  • xi = [3, 2.5]带标签的最后一个例子yi = 1给出了得分0.1*3 + 0.2*2.5 + 0.3,即1.1。在这种情况下,SVM将计算costi = Math.max(0, -1*1.1 + 1),实际上为零。此数据点正确分类,并且没有与之相关的成本。

成本函数是一种表达式,用于衡量分类器的糟糕程度。当训练集完全分类时,成本(忽略正则化)将为零。

请注意,损失中的最后一项是正则化成本,它表示我们的模型参数应该是小值。由于这个术语,成本实际上永远不会变为零(因为这意味着模型的所有参数除了偏差都恰好为零),但是越接近,我们的分类器就越好。

机器学习中的大多数成本函数由两部分组成:1。衡量模型与数据拟合程度的部分; 2:正则化,衡量模型复杂程度或可能性的一些概念。

我希望我说服你,为了获得一个非常好的SVM,我们真的希望尽可能地降低成本。听起来很熟悉?我们确切知道该做什么:上面写的成本函数是我们的电路。我们将通过电路转发所有示例,计算反向通道并更新所有参数,以便电路将来输出更低的成本。具体来说,我们将计算梯度,然后在梯度相反方向更新参数(因为我们希望使成本变小,而不是很大)。

“我们确切地知道该怎么做:上面写的成本函数就是我们的电路。”

todo:清理这一部分并将其充实...

第3章:实践中的Backprop

建立一个图书馆

示例:实用神经网络分类器

  • 多类:结构化SVM
  • 多类:Logistic回归,Softmax

示例:回归

成本函数需要微小的变化。L2正规化。

示例:结构化预测

基本思想是训练(非标准化)能量模型

矢量化实现

使用numpy在Python中编写神经网络类文件。

Backprop在实践中:提示/技巧

  • 监控成本函数
  • 监控培训/验证性能
  • 调整初始学习率,学习率计划
  • 优化:使用动量
  • 优化:LBFGS,Nesterov加速梯度
  • 初始化的重要性:权重和偏差
  • 正规化:L2,L1,群稀疏,辍学
  • 超参数搜索,交叉验证
  • 常见的陷阱:(例如死亡的ReLUs)
  • 处理不平衡的数据集
  • 当某些东西不起作用时调试网络的方法

第4章:野外网络

模型的案例研究在实践中运作良好并已在野外部署。

案例研究:图像的卷积神经网络

卷积层,汇集,AlexNet等

案例研究:语音和文本的递归神经网络

香草复发网,双向复发网。也许是LSTM的概述

案例研究:Word2Vec

训练在NLP的词传染媒介表示

案例研究:t-SNE

培训嵌入以可视化数据

致谢

非常感谢以下让这本指南更好的人:wodenokoto(HN),zackmorris(HN)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值