1. 引言

上一节深度学习与计算机视觉系列(3)_线性SVM与SoftMax分类器中提到两个对图像识别至关重要的概念:

  1. 用于把原始像素信息映射到不同类别得分的得分函数/score function
  2. 用于评估参数W效果(评估该参数下每类得分和实际得分的吻合度)的损失函数/loss function

其中对于线性SVM,我们有:

  1. 得分函数f(xi,W)=Wxi
  2. 损失函数L=1N∑i∑j≠yi[max(0,f(xi;W)j−f(xi;W)yi+1)]+αR(W)

在取到合适的参数W的情况下,我们根据原始像素计算得到的预测结果和实际结果吻合度非常高,这时候损失函数得到的值就很小。

这节我们就讲讲,怎么得到这个合适的参数W,使得损失函数取值最小化。也就是最优化的过程。

2. 损失函数可视化

我们在计算机视觉中看到的损失函数,通常都是定义在非常高维的空间里的(比如CIFAR-10的例子里一个线性分类器的权重矩阵W是10 x 3073维的,总共有30730个参数 -_-||),人要直接『看到』它的形状/变化是非常困难的。但是机智的同学们,总是能想出一些办法,把损失函数在某种程度上可视化的。比如说,我们可以把高维投射到一个向量/方向(1维)或者一个面(2维)上,从而能直观地『观察』到一些变化。

举个例子说,我们可以对一个权重矩阵W(例如CIFAR−10中是30730个参数),可以找到W维度空间中的一条直线,然后沿着这条线,计算一下损失函数值的变化情况。具体一点说,就是我们找到一个方向W1(维度要和W一样,才能表示W的维度空间的一个方向/一条直线),然后我们给不同的a值,计算L(W+aW1),这样,如果a取得足够密,其实我们就能够在一定程度上描绘出损失函数沿着这个方向的变化了。

同样,如果我们给两个方向W1和W2,那么我们可以确定一个平面,我们再取不同值的a和b,计算L(W+aW1+bW2)的值,那么我们就可以大致绘出在这个平面上,损失函数的变化情况了。

根据上面的方法,我们画出了下面3个图。最上面的图是调整a的不同取值,绘出的损失函数变化曲线(越高值越大);中间和最后一个图是调整a与b的取值,绘出的损失函数变化图(蓝色表示损失小,红色表示损失大),中间是在一个图片样本上计算的损失结果,最下图为100张图片上计算的损失结果的一个平均。显然沿着直线方向得到的曲线底端为最小的损失值点,而曲面呈现的碗状图形碗底为损失函数取值最小处。

 
 

我们从数学的角度,来尝试解释一下,上面的凹曲线是怎么出来的。对于第i个样本,我们知道它的损失函数值为:

Li=∑j≠yi[max(0,wTjxi−wTyixi+1)]

在所有的样本上的损失函数值,是它们损失函数值( max(0,-) ,因此最小值为0)的平均值。为了更好理解,我们假定训练集里面有3个样本,都是1维的,同时总共有3个类别。所以SVM损失(暂时不考虑正则化项)可以表示为如下的式子:

L0=L1=L2=L=max(0,wT1x0−wT0x0+1)+max(0,wT2x0−wT0x0+1)max(0,wT0x1−wT1x1+1)+max(0,wT2x1−wT1x1+1)max(0,wT0x2−wT2x2+1)+max(0,wT1x2−wT2x2+1)(L0+L1+L2)/3

因为这个例子里的样本都是1维的,因此其实xi和wj都是实数。拿w0举例,损失函数里,大于0的值其实都和w0是线性关系的,而最小值为0。因此,我们可以想象成,三条折线『合体』得到的最终曲线,如下图所示: 

插几句题外话,从之前碗状结构的示意图,你可能会猜到SVM损失函数是一个凸函数,而对于凸函数的最小值求解方法有很多种。但之后当我们把损失函数f扩充到神经网络之后,损失函数将变成一个非凸函数,而如果依旧可视化的话,我们看到的将不再是一个碗状结构,而是凹凸不平的。

3. 最优化

在我们现在这个问题中,所谓的『最优化』其实指的就是找到能让损失函数最小的参数W。如果大家看过或者了解凸优化的话,我们下面介绍的方法,对你而言可能太简单了,有点原始,但是大家别忘了,我们后期要处理的是神经网络的损失函数,那可不是一个凸函数哦,所以我们还是一步步来一起看看,如果去实现最优化问题。

3.1 策略1:随机搜寻(不太实用)

以一个笨方法开始,我们知道,当我们手头上有参数W后,我们是可以计算损失函数,评估参数合适程度的。所以最直接粗暴的方法就是,我们尽量多地去试参数,然后从里面选那个让损失函数最小的,作为最后的W。代码当然很简单,如下:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 假设 X_train 是训练集 (例如. 3073 x 50,000)</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 假设 Y_train 是类别结果 (例如. 1D array of 50,000)</span>bestloss = float(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"inf"</span>) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 初始化一个最大的float值</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span> num <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">in</span> xrange(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1000</span>):W = np.random.randn(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3073</span>) * <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.0001</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 随机生成一组参数</span>loss = L(X_train, Y_train, W) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 计算损失函数</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> loss < bestloss: <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 比对已搜寻中最好的结果</span>bestloss = lossbestW = W<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">print</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'in attempt %d the loss was %f, best %f'</span> % (num, loss, bestloss)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># prints:</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># in attempt 0 the loss was 9.401632, best 9.401632</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># in attempt 1 the loss was 8.959668, best 8.959668</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># in attempt 2 the loss was 9.044034, best 8.959668</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># in attempt 3 the loss was 9.278948, best 8.959668</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># in attempt 4 the loss was 8.857370, best 8.857370</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># in attempt 5 the loss was 8.943151, best 8.857370</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># in attempt 6 the loss was 8.605604, best 8.605604</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># ... (trunctated: continues for 1000 lines)</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li></ul>

一通随机试验和搜寻之后,我们会拿到试验结果中最好的参数W,然后在测试集上看看效果:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 假定 X_test 为 [3073 x 10000], Y_test 为 [10000 x 1]</span>
scores = Wbest.dot(Xte_cols) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 10 x 10000, 计算类别得分</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 找到最高得分作为结果</span>
Yte_predict = np.argmax(scores, axis = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>)
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 计算准确度</span>
np.mean(Yte_predict == Yte)
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 返回 0.1555</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul>

随机搜寻得到的参数W,在测试集上的准确率为15.5%,总共10各类别,我们不做任何预测只是随机猜的结果应该是10%,好像稍高一点,但是…大家也看到了…这个准确率…实在是没办法在实际应用中使用。

3.2 策略2:随机局部搜索

上一个策略完全就是盲搜,要想找到全局最优的那个结果基本是不可能的。它最大的缺点,就在于下一次搜索完全是随机进行的,没有一个指引方向。那我们多想想,就能想出一个在上个策略的基础上,优化的版本,叫做『随机局部搜索』。

这个策略的意思是,我们不每次都随机产生一个参数矩阵W了,而是在现有的参数W基础上,搜寻一下周边临近的参数,有没有比现在参数更好的W,然后我们用新的W替换现在的W,接着在周围继续小范围搜寻。这个过程呢,可以想象成,我们在一座山上,现在要下山,然后我们每次都伸脚探一探周边,找一个比现在的位置下降一些的位置,然后迈一步,接着在新的位置上做同样的操作,一步步直至下山。

从代码实现的角度看,以上的过程,实际上就是对于一个当前W,我们每次实验和添加δW′,然后看看损失函数是否比当前要低,如果是,就替换掉当前的W,代码如下:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">W = np.random.randn(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3073</span>) * <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.001</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 初始化权重矩阵W</span>
bestloss = float(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"inf"</span>)
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span> i <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">in</span> xrange(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1000</span>):step_size = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.0001</span>Wtry = W + np.random.randn(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3073</span>) * step_sizeloss = L(Xtr_cols, Ytr, Wtry)<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> loss < bestloss:W = Wtrybestloss = loss<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">print</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'iter %d loss is %f'</span> % (i, bestloss)</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>

我们做了这么个小小的修正之后,我们再拿刚才一样的测试集来测一下效果,结果发现准确率提升至21.4%,虽然离实际应用差很远,但只是比刚才要进步一点点了。

但是还是有个问题,我们每次测试周边点的损失函数,是一件非常耗时的事情。我们有没有办法能够直接找到我们应该迭代的方向呢?

3.3 策略3:顺着梯度下滑

刚才的策略,我们说了,最大的缺点是非常耗时,且计算量也很大。我们一直在做的事情,就是在当前的位置基础上,想找到一个最合适的下降方向。我们依旧回到我们假设的那个情境,如果我们在山顶,要以最快的方式下山,我们会怎么做?我们可能会环顾四周,然后找到最陡的方向,迈一小步,然后再找当前位置最陡的下山方向,再迈一小步…

而这里提到的最陡的方向,其实对应的就是数学里『梯度』的概念,也就是说,其实我们无需『伸脚试探』周边的陡峭程度,而是可以通过计算损失函数的梯度,直接取得这个方向。

我们知道在1个变量的函数里,某点的斜率/导数代表其变化率最大的方向。而对于多元的情况,梯度是上面情况的一个扩展,只不过这时候的变量不再是一个,而是多个,同时我们计算得到的『梯度方向』也是一个多维的向量。大家都知道数学上计算1维/元函数『梯度/导数』的表达式如下:

df(x)dx=limh →0f(x+h)−f(x)h

对于多元的情况,这个时候我们需要求的东西扩展成每个方向的『偏导数』,然后把它们合在一块组成我们的梯度向量。

我们用几张图来说明这个过程:

 
 

4. 计算梯度

有两种计算梯度的方法:

  1. 慢一些但是简单一些的数值梯度/numerical gradient
  2. 速度快但是更容易出错的解析梯度/analytic gradient

4.1 数值梯度

根据上面提到的导数求解公式,我们可以得到数值梯度计算法。下面是一段简单的代码,对于一个给定的函数f和一个向量x,求解这个点上的梯度:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> <span class="hljs-title" style="box-sizing: border-box;">eval_numerical_gradient</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(f, x)</span>:</span><span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">""" 一个最基本的计算x点上f的梯度的算法 - f 为参数为x的函数- x 是一个numpy的vector"""</span> fx = f(x) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 计算原始点上函数值</span>grad = np.zeros(x.shape)h = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.00001</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 对x的每个维度都计算一遍</span>it = np.nditer(x, flags=[<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'multi_index'</span>], op_flags=[<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'readwrite'</span>])<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">while</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">not</span> it.finished:<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 计算x+h处的函数值</span>ix = it.multi_indexold_value = x[ix]x[ix] = old_value + h <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 加h</span>fxh = f(x) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 计算f(x + h)</span>x[ix] = old_value <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 存储之前的函数值</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 计算偏导数</span>grad[ix] = (fxh - fx) / h <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 斜率</span>it.iternext() <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 开始下一个维度上的偏导计算</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> grad</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li></ul>

代码的方法很简单,对每个维度,都在原始值上加上一个很小的h,然后计算这个维度/方向上的偏导,最后组在一起得到梯度grad

4.1.1 实际计算中的提示

我们仔细看看导数求解的公式,会发现数学定义上h是要趋于0的,但实际我们计算的时候我们只要取一个足够小的数(比如1e-5)作为h就行了,所以我们要精准计算偏导的话,要尽量取到不会带来数值计算问题,同时又能很小的h。另外,其实实际计算中,我们用另外一个公式用得更多[f(x+h)−f(x−h)]/2h

下面我们用上面的公式在CIFAR-10数据集上,试一试吧:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> <span class="hljs-title" style="box-sizing: border-box;">CIFAR10_loss_fun</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(W)</span>:</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> L(X_train, Y_train, W)W = np.random.rand(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3073</span>) * <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.001</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 随机权重向量</span>
df = eval_numerical_gradient(CIFAR10_loss_fun, W) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 计算梯度</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li></ul>

计算到的梯度(准确地说,梯度的方向是函数增大方向,负梯度才是下降方向)告诉我们,我们应该『下山』的方向是啥,接着我们就沿着它小步迈进:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">loss_original = CIFAR10_loss_fun(W) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 原始点上的损失</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">print</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'original loss: %f'</span> % (loss_original, )<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 多大步伐迈进好呢?我们选一些步长试试</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span> step_size_log <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">in</span> [-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>, -<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">9</span>, -<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">8</span>, -<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">7</span>, -<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>, -<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>,-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4</span>,-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>,-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>,-<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>]:step_size = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span> ** step_size_logW_new = W - step_size * df <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 新的权重</span>loss_new = CIFAR10_loss_fun(W_new)<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">print</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'for step size %f new loss: %f'</span> % (step_size, loss_new)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 输出:</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># original loss: 2.200718</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-10 new loss: 2.200652</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-09 new loss: 2.200057</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-08 new loss: 2.194116</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-07 new loss: 2.135493</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-06 new loss: 1.647802</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-05 new loss: 2.844355</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-04 new loss: 25.558142</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-03 new loss: 254.086573</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-02 new loss: 2539.370888</span>
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># for step size 1.000000e-01 new loss: 25392.214036</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li></ul>

4.1.2 关于迭代的细节

如果大家仔细看上述代码的话,会发现我们step_size设的都是负的,确实我们每次update权重W的时候,是用原来的W减掉梯度方向的一个较小的值,这样损失函数才能减小。

4.1.3 关于迭代的步长

我们计算得到梯度之后,就确定了幅度变化最快(负梯度是下降方向)的方向,但是它并没有告诉我们,我朝着这个方向,应该迈进多远啊。之后的章节会提到,选择正确的迭代步长(有时候我们也把它叫做学习速率)是训练过程中最重要(也是最让人头疼)的一个待设定参数。就像我想以最快的速度下山,我们能感知到最陡的方向,却不知道应该迈多大的步子。如果我们小步迈进,那确实每一步都能比上一步下降一些,但是速度太慢了亲!!但是如果我们以非常非常大的步伐迈进(假如腿巨长 -_-||),那你猜怎么着,你一不小心可能就迈过山脚迈到另一座山山腰上了…

下图是对以上情况的一个描述和解释:

图上红色的值很大,蓝色的值很小,我们想逐步下降至蓝色中心。如果迈进的步伐太小,收敛和行进的速度就会很慢,如果迈进的步伐太大,可能直接越过去了。

4.1.4 效率问题

如果你再回过头去看看上面计算数值梯度的程序,你会发现,这个计算方法的复杂度,基本是和我们的参数个数成线性关系的。这意味着什么呢?在我们的CIFAR-10例子中,我们总共有30730个参数,因此我们单次迭代总共就需要计算30731次损失函数。这个问题在之后会提到的神经网络中更为严重,很可能两层神经元之间就有百万级别的参数权重,所以,计算机算起来都很耗时…人也要等结果等到哭瞎…

4.2 解析法计算梯度

数值梯度发非常容易实现,但是从公式里面我们就看得出来,梯度实际上是一个近似(毕竟你没办法把h取到非常小),同时这也是一个计算非常耗时的算法。第二种计算梯度的方法是解析法,它可以让我们直接得到梯度的一个公式(代入就可以计算,非常快),但是呢,不像数值梯度法,这种方法更容易出现错误。so,聪明的同学们,就想了一个办法,我们可以先计算解析梯度和数值梯度,然后比对结果和校正,在确定我们解析梯度实现正确之后,我们就可以大胆地进行解析法计算了(这个过程叫做梯度检查/检测)

我们拿一个样本点的SVM损失函数举例:

Li=∑j≠yi[max(0,wTjxi−wTyixi+Δ)]

我们可以求它对每个权重的偏导数,比如说,我们求它对 wyi 的偏导,我们得到:

∇wyiLi=−⎛⎝∑j≠yi1(wTjxi−wTyixi+Δ>0)⎞⎠xi

其中 1 是一个bool函数,在括号内的条件为真的时候取值为1,否则为0。看起来似乎很吓人,但实际上要写代码完成的话,你只需要计算不满足指定SVM最小距离的类(对损失函数有贡献的类)的个数,然后用这个值会对数据向量 xi 做缩放即可得到梯度。但是要注意只是 W 中对应正确的类别的列的梯度。对于其他的 j≠yi 的情况,梯度为:

∇wjLi=1(wTjxi−wTyixi+Δ>0)xi

一旦得到梯度的表达式,那计算梯度和调整权重就变得非常直接和简单。熟练掌握如何在loss expression下计算梯度是非常重要的一个技巧,贯穿整个神经网络的训练实现过程,关于这个内容,下次会详细讲到。

5. 梯度下降

在我们有办法计算得到梯度之后,使用梯度去更新已有权重参数的过程叫做『梯度下降』,伪代码其实就是如下的样子:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">while</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">True</span>:weights_grad = evaluate_gradient(loss_fun, data, weights)weights += - step_size * weights_grad <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 梯度下降更新参数</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

这个简单的循环实质上就是很多神经网络库的核心。当然,我们也有其他的方式去实现最优化(比如说L-BFGS),但是梯度下降确实是当前使用最广泛,也相对最稳定的神经网络损失函数最优化方法。

5.1 Mini-batch gradient descent

在大型的应用当中(比如ILSVRC),训练数据可能是百万千万级别的。因此,对整个训练数据集的样本都算一遍损失函数,以完成参数迭代是一件非常耗时的事情,一个我们通常会用到的替代方法是,采样出一个子集在其上计算梯度。现在比较前沿的神经网络结构基本都是这么做的,例如ConvNets是每256张作为一个batch去完成参数的更新。参数更新的代码如下:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">while</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">True</span>:data_batch = sample_training_data(data, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">256</span>) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 抽样256个样本作为一个batch</span>weights_grad = evaluate_gradient(loss_fun, data_batch, weights)weights += - step_size * weights_grad <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 参数更新</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li></ul>

之所以可以这么做,是因为训练数据之间其实是关联的。我们简化一下这个问题,你想想,如果ILSVRC中的120w图片,如果只是1000张不同的图片,一直复制1200次得到的。那么其实我们在这1000张图片上算得的损失函数和120w的平均其实是一致的。当然,当然,在实际场景中,我们肯定很少遇到这种多次重复的情况,但是原数据的一个子集(mini-batch)上的梯度,其实也是对整体数据上梯度的一个很好的近似。因此,只在mini-batch上计算和更新参数,会有快得多的收敛速度。

上述算法的一个极端的情况是,如果我们的一个mini-batch里面只有一张图片。那这个过程就变成『随机梯度下降/Stochastic Gradient Descent (SGD)』,说起来,这个其实在实际应用中倒也没那么常见,原因是向量化之后,一次计算100张图片,其实比计算一张图片100次,要快得多。所以即使从定义上来说,SGD表示我们用一张图片上的梯度近似全局梯度,但是很多时候人们提到SGD的时候,其实他们指的是mini-batch梯度下降,也就是说,我们把一个batch当做1份了。额,还要稍微提一句的是,有些同学可能会问,这个batch size本身不是一个需要实验的参数吗,取多大的batch size好啊?但实际应用中,我们倒很少会用cross-validation去选择这个参数。这么说吧,我们一般是基于我们内存限制去取这个值的,比如设成100左右。

6. 总结

  • 把损失函数在各参数上的取值,想象成我们所在山峰的高度。那么我们要最小化损失函数,实际上就是『要想办法下山』。
  • 我们采取的下山策略是,一次迈一小步,只要每次都往下走了,那么最后就会到山底。
  • 梯度对应函数变化最快的方向,负梯度的方向就是我们下山,环顾四周之后,发现最陡的下山路方向。
  • 我们的步长(也叫学习率),会影响我们的收敛速度(下山速度),如果步伐特别特别大,甚至可能跃过最低点,跑到另外一个高值位置了。
  • 我们用mini-batch的方式,用一小部分的样本子集,计算和更新参数,减少计算量,加快收敛速度。

1. 引言

上一节我们讲完了各种激励函数的优缺点和选择,以及网络的大小以及正则化对神经网络的影响。这一节我们讲一讲输入数据预处理、正则化以及损失函数设定的一些事情。

2. 数据与网络的设定

前一节提到前向计算涉及到的组件(主要是神经元)设定。神经网络结构和参数设定完毕之后,我们就得到得分函数/score function(忘记的同学们可以翻看一下之前的博文),总体说来,一个完整的神经网络就是在不断地进行线性映射(权重和input的内积)和非线性映射(部分激励函数作用)的过程。这一节我们会展开来讲讲数据预处理,权重初始化和损失函数的事情。

2.1 数据预处理

在卷积神经网处理图像问题的时候,图像数据有3种常见的预处理可能会用到,如下。我们假定数据表示成矩阵为X,其中我们假定X是[N*D]维矩阵(N是样本数据量,D为单张图片的数据向量长度)。

  • 去均值,这是最常见的图片数据预处理,简单说来,它做的事情就是,对待训练的每一张图片的特征,都减去全部训练集图片的特征均值,这么做的直观意义就是,我们把输入数据各个维度的数据都中心化到0了。使用python的numpy工具包,这一步可以用X -= np.mean(X, axis = 0)轻松实现。当然,其实这里也有不同的做法:简单一点,我们可以直接求出所有像素的均值,然后每个像素点都减掉这个相同的值;稍微优化一下,我们在RGB三个颜色通道分别做这件事。
  • 归一化,归一化的直观理解含义是,我们做一些工作去保证所有的维度上数据都在一个变化幅度上。通常我们有两种方法来实现归一化。一个是在数据都去均值之后,每个维度上的数据都除以这个维度上数据的标准差(X /= np.std(X, axis = 0))。另外一种方式是我们除以数据绝对值最大值,以保证所有的数据归一化后都在-1到1之间。多说一句,其实在任何你觉得各维度幅度变化非常大的数据集上,你都可以考虑归一化处理。不过对于图像而言,其实这一步反倒可做可不做,因为大家都知道,像素的值变化区间都在[0,255]之间,所以其实图像输入数据天生幅度就是一致的。

上述两个操作对于数据的作用,画成示意图,如下: 

  • PCA和白化/whitening,这是另外一种形式的数据预处理。在经过去均值操作之后,我们可以计算数据的协方差矩阵,从而可以知道数据各个维度之间的相关性,简单示例代码如下:
<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 假定输入数据矩阵X是[N*D]维的</span>
X -= np.mean(X, axis = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 去均值</span>
cov = np.dot(X.T, X) / X.shape[<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>] <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 计算协方差</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

得到的结果矩阵中元素(i,j)表示原始数据中,第i维和第j维直接爱你的相关性。有意思的是,其实协方差矩阵的对角线包含了每个维度的变化幅度。另外,我们都知道协方差矩阵是对称的,我们可以在其上做矩阵奇异值分解(SVD factorization):

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">U,S,V = np.linalg.svd(cov)</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

其中U为特征向量,我们如果相对原始数据(去均值之后)做去相关操作,只需要进行如下运算:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">Xrot = np.dot(X, U)</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

这么理解一下可能更好,U是一组正交基向量。所以我们可以看做把原始数据X投射到这组维度保持不变的正交基底上,从而也就完成了对原始数据的去相关。如果去相关之后你再求一下Xrot的协方差矩阵,你会发现这时候的协方差矩阵是一个对角矩阵了。而numpy中的np.linalg.svd更好的一个特性是,它返回的U是对特征值排序过的,这也就意味着,我们可以用它进行降维操作。我们可以只取top的一些特征向量,然后做和原始数据做矩阵乘法,这个时候既降维减少了计算量,同时又保存下了绝大多数的原始数据信息,这就是所谓的主成分分析/PCA:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">Xrot_reduced = np.dot(X, U[:,:<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">100</span>])</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

这个操作之后,我们把原始数据集矩阵从[N*D]降维到[N*100],保存了前100个能包含绝大多数数据信息的维度。实际应用中,你在PCA降维之后的数据集上,做各种机器学习的训练,在节省空间和时间的前提下,依旧能有很好的训练准确度。

最后我们再提一下whitening操作。所谓whitening,就是把各个特征轴上的数据除以特征向量,从而达到在每个特征轴上都归一化幅度的结果。whitening变换的几何意义和理解是,如果输入的数据是多变量高斯,那whitening之后的 数据是一个均值为0而不同方差的高斯矩阵。这一步简单代码实现如下:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">#白化数据</span>
Xwhite = Xrot / np.sqrt(S + <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1e-5</span>)</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>

提个醒:whitening操作会有严重化噪声的可能。注意到我们在上述代码中,分母的部分加入了一个很小的数1e-5,以防止出现除以0的情况。但是数据中的噪声部分可能会因whitening操作而变大,因为这个操作的本质是把输入的每个维度都拉到差不多的幅度,那么本不相关的有微弱幅度变化的噪声维度,也被拉到了和其他维度同样的幅度。当然,我们适当提高坟墓中的安全因子(1e-5)可以在一定程度上缓解这个问题。

下图为原始数据到去相关到白化之后的数据分布示意图: 

我们来看看真实数据集上的操作与得到的结果,也许能对这些过程有更清晰一些的认识。大家都还记得CIFAR-10图像数据集吧。训练集大小为50000*3072,也就是说,每张图片都被展成一个3072维度的列向量了。然后我们对原始50000*3072数据矩阵做SVD分解,进行上述一些操作,再可视化一下,得到的结果示意图如下:

我们稍加解释一下,最左边是49张原始图片;左起第2幅图是最3072个特征向量中最top的144个,这144个特征向量包含了绝大多数数据变量信息,而其实它们代表的是图片中低频的信息;左起第3幅图表示PCA降维操作之后的49张图片,使用上面求得的144个特征向量。我们可以观察到图片好像被蒙上了一层东西一样,模糊化了,这也就表明了我们的top144个特征向量捕捉到的都是图像的低频信息,不过我们发现图像的绝大多数信息确实被保留下来了;最右图是whitening的144个数通过乘以U.transpose()[:144,:]还原回图片的样子,有趣的是,我们发现,现在低频信息基本都被滤掉了,剩下一些高频信息被放大呈现。

实际工程中,因为这个部分讲到数据预处理,我们就把基本的几种数据预处理都讲了一遍,但实际卷积神经网中,我们并没有用到去相关和whitening操作。当然,去均值是非常非常重要的,而每个像素维度的归一化也是常用的操作。

特别说明,需要特别说明的一点是,上述的预处理操作,一定都是在训练集上先预算的,然后应用在交叉验证/测试集上的。举个例子,有些同学会先把所有的图片放一起,求均值,然后减掉均值,再把这份数据分作训练集和测试集,这是不对的亲!!!

2.2 权重初始化

我们之前已经看过一个完整的神经网络,是怎么样通过神经元和连接搭建起来的,以及如何对数据做预处理。在训练神经网络之前,我们还有一个任务要做,那就是初始化参数。

错误的想法:全部初始化为0,有些同学说,那既然要训练和收敛嘛,初始值就随便设定,简单一点就全设为0好了。亲,这样是绝对不行的!!!为啥呢?我们在神经网络训练完成之前,是不可能预知神经网络最后的权重具体结果的,但是根据我们归一化后的数据,我们可以假定,大概有半数左右的权重是正数,而另外的半数是负数。但设定全部初始权重都为0的结果是,网络中每个神经元都计算出一样的结果,然后在反向传播中有一样的梯度结果,因此迭代之后的变化情况也都一样,这意味着这个神经网络的权重没有办法差异化,也就没有办法学习到东西。

很小的随机数,其实我们依旧希望初始的权重是较小的数,趋于0,但是就像我们刚刚讨论过的一样,不要真的是0。综合上述想法,在实际场景中,我们通常会把初始权重设定为非常小的数字,然后正负尽量一半一半。这样,初始的时候权重都是不一样的很小随机数,然后迭代过程中不会再出现迭代一致的情况。举个例子,我们可能可以这样初始化一个权重矩阵W=0.0001*np.random.randn(D,H)。这个初始化的过程,使得每个神经元的权重向量初始化为多维高斯中的随机采样向量,所以神经元的初始权重值指向空间中的随机方向。

特别说明:其实不一定更小的初始值会比大值有更好的效果。我们这么想,一个有着非常小的权重的神经网络在后向传播过程中,回传的梯度也是非常小的。这样回传的”信号”流会相对也较弱,对于层数非常多的深度神经网络,这也是一个问题,回传到最前的迭代梯度已经很小了。

方差归一化,上面提到的建议有一个小问题,对于随机初始化的神经元参数下的输出,其分布的方差随着输入的数量,会增长。我们实际上可以通过除以总输入数目的平方根,归一化每个神经元的输出方差到1。也就是说,我们倾向于初始化神经元的权重向量为w = np.random.randn(n) / sqrt(n),其中n为输入数。

我们从数学的角度,简单解释一下,为什么上述操作可以归一化方差。考虑在激励函数之前的权重w与输入x的内积s=∑niwixi部分,我们计算一下s的方差:

Var(s)=Var(∑inwixi)=∑inVar(wixi)=∑in[E(wi)]2Var(xi)+E[(xi)]2Var(wi)+Var(xi)Var(wi)=∑inVar(xi)Var(wi)=(nVar(w))Var(x)

注意,这个推导的前2步用到了方差的性质。第3步我们假定输入均值为0,因此E[xi]=E[wi]=0。不过这是我们的一个假设,实际情况下并不一定是这样的,比如ReLU单元的均值就是正的。最后一步我们假定wi,xi是独立分布。我们想让s的方差和输入x的方差一致,因此我们想让w的方差取值为1/n,又因为我们有公式Var(aX)=a2Var(X),所以a应该取值为a=1/n−−−√,numpy里的实现为w = np.random.randn(n) / sqrt(n)

对于初始化权重还有一些类似的研究和建议,比如说Glorot在论文Understanding the difficulty of training deep feedforward neural networks就推荐使用能满足Var(w)=2/(nin+nout)的权重初始化。其中nin,nout是前一层和后一层的神经元个数。而另外一篇比较新的论文Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification,则指出尤其对于ReLU神经元,我们初始化方差应该为2.0/n,也就是w = np.random.randn(n) * sqrt(2.0/n),目前的神经网络中使用了很多ReLU单元,因此这个设定其实在实际应用中使用最多。

偏移量/bias初始化:相对而言,bias项初始化就简单一些。我们很多时候简单起见,直接就把它们都设为0.在ReLU单元中,有些同学会使用很小的数字(比如0.01)来代替0作为所有bias项的初始值,他们解释说这样也能保证ReLU单元一开始就是被激活的,因此反向传播过程中不会终止掉回传的梯度。不过似乎实际的实验过程中,这个优化并不是每次都能起到作用的,因此很多时候我们还是直接把bias项都初始化为0。

2.3 正则化

在前一节里我们说了我们要通过正则化来控制神经网络,使得它不那么容易过拟合。有几种正则化的类型供选择:

  • L2正则化,这个我们之前就提到过,非常常见。实现起来也很简单,我们在损失函数里,加入对每个参数的惩罚度。也就是说,对于每个权重w,我们在损失函数里加入一项12λw2,其中λ是我们可调整的正则化强度。顺便说一句,这里在前面加上1/2的原因是,求导/梯度的时候,刚好变成λw而不是2λw。L2正则化理解起来也很简单,它对于特别大的权重有很高的惩罚度,以求让权重的分配均匀一些,而不是集中在某一小部分的维度上。我们再想想,加入L2正则化项,其实意味着,在梯度下降参数更新的时候,每个权重以W += -lambda*W的程度被拉向0。

  • L1正则化,这也是一种很常见的正则化形式。在L1正则化中,我们对于每个权重w的惩罚项为λ|w|。有时候,你甚至可以看到大神们混着L1和L2正则化用,也就是说加入惩罚项λ1∣w∣+λ2w2,L1正则化有其独特的特性,它会让模型训练过程中,权重特征向量逐渐地稀疏化,这意味着到最后,我们只留下了对结果影响最大的一部分权重,而其他不相关的输入(例如『噪声』)因为得不到权重被抑制。所以通常L2正则化后的特征向量是一组很分散的小值,而L1正则化只留下影响较大的权重。在实际应用中,如果你不是特别要求只保留部分特征,那么L2正则化通常能得到比L1正则化更好的效果

  • 最大范数约束,另外一种正则化叫做最大范数约束,它直接限制了一个上行的权重边界,然后约束每个神经元上的权重都要满足这个约束。实际应用中是这样实现的,我们不添加任何的惩罚项,就按照正常的损失函数计算,只不过在得到每个神经元的权重向量w⃗ 之后约束它满足∥w⃗ ∥2<c。有些人提到这种正则化方式帮助他们提高最后的模型效果。另外,这种正则化方式倒是有一点很吸引人:在神经网络训练学习率设定很高的时候,它也能很好地约束住权重更新变化,不至于直接挂掉。

  • Dropout,亲,这个是我们实际神经网络训练中,用的非常多的一种正则化手段,同时也相当有效。Srivastava等人的论文Dropout: A Simple Way to Prevent Neural Networks from Overfitting最早提到用dropout这种方式作为正则化手段。一句话概括它,就是:在训练过程中,我们对每个神经元,都以概率p保持它是激活状态,1-p的概率直接关闭它。

下图是一个3层的神经网络的dropout示意图:

可以这么理解,在训练过程中呢,我们对全体神经元,以概率p做了一个采样,只有选出的神经元要进行参数更新。所以最后就从左图的全连接到右图的Dropout过后神经元连接图了。需要多说一句的是,在测试阶段,我们不用dropout,而是直接从概率的角度,对权重配以一个概率值。

简单的Dropout代码如下(这是简易实现版本,但是不建议使用,我们会分析为啥,并在之后给出优化版):

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">
p = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 设定dropout的概率,也就是保持一个神经元激活状态的概率</span><span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> <span class="hljs-title" style="box-sizing: border-box;">train_step</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(X)</span>:</span><span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">""" X contains the data """</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 3层神经网络前向计算</span>H1 = np.maximum(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, np.dot(W1, X) + b1)U1 = np.random.rand(*H1.shape) < p <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 第一次Dropout</span>H1 *= U1 <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># drop!</span>H2 = np.maximum(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, np.dot(W2, H1) + b2)U2 = np.random.rand(*H2.shape) < p <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 第二次Dropout</span>H2 *= U2 <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># drop!</span>out = np.dot(W3, H2) + b3<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 反向传播: 计算梯度... (这里省略)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 参数更新... (这里省略)</span><span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> <span class="hljs-title" style="box-sizing: border-box;">predict</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(X)</span>:</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 加上Dropout之后的前向计算</span>H1 = np.maximum(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, np.dot(W1, X) + b1) * p H2 = np.maximum(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, np.dot(W2, H1) + b2) * p out = np.dot(W3, H2) + b3</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li></ul>

上述代码中,在train_step函数中,我们做了2次Dropout。我们甚至可以在输入层做一次dropout。反向传播过程保持不变,除了我们要考虑一下U1,U2

很重要的一点是,大家仔细看predict函数部分,我们不再dropout了,而是对于每个隐层的输出,都用概率p做了一个幅度变换。可以从数学期望的角度去理解这个做法,我们考虑一个神经元的输出为x(没有dropout的情况下),它的输出的数学期望为px+(1−p)0,那我们在测试阶段,如果直接把每个输出x都做变换x→px,其实是可以保持一样的数学期望的。

上述代码的写法有一些缺陷,我们必须在测试阶段对每个神经的输出都以p的概率输出。考虑到实际应用中,测试阶段对于时间的要求非常高,我们可以考虑反着来,代码实现的时候用inverted dropout,即在训练阶段就做相反的幅度变换/scaling(除以p),这样在测试阶段,我们可以直接把权重拿来使用,而不用附加很多步用p做scaling的过程。inverted dropout的示例代码如下:

<code class="language-python hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"""
Inverted Dropout的版本,把本该花在测试阶段的时间,转移到训练阶段,从而提高testing部分的速度
"""</span>p = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># dropout的概率,也就是保持一个神经元激活状态的概率</span><span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> <span class="hljs-title" style="box-sizing: border-box;">train_step</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(X)</span>:</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># f3层神经网络前向计算</span>H1 = np.maximum(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, np.dot(W1, X) + b1)U1 = (np.random.rand(*H1.shape) < p) / p <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 注意到这个dropout中我们除以p,做了一个inverted dropout</span>H1 *= U1 <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># drop!</span>H2 = np.maximum(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, np.dot(W2, H1) + b2)U2 = (np.random.rand(*H2.shape) < p) / p <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 这个dropout中我们除以p,做了一个inverted dropout</span>H2 *= U2 <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># drop!</span>out = np.dot(W3, H2) + b3<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 反向传播: 计算梯度... (这里省略)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 参数更新... (这里省略)</span><span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> <span class="hljs-title" style="box-sizing: border-box;">predict</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(X)</span>:</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 直接前向计算,无需再乘以p</span>H1 = np.maximum(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, np.dot(W1, X) + b1) H2 = np.maximum(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, np.dot(W2, H1) + b2)out = np.dot(W3, H2) + b3</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li></ul>

对于dropout这个部分如果你有更深的兴趣,欢迎阅读以下文献: 
1) 2014 Srivastava 的论文Dropout paper 
2) Dropout Training as Adaptive Regularization

  • bias项的正则化,其实我们在之前的博客中提到过,我们大部分时候并不对偏移量项做正则化,因为它们也没有和数据直接有乘法等交互,也就自然不会影响到最后结果中某个数据维度的作用。不过如果你愿意对它做正则化,倒也不会影响最后结果,毕竟总共有那么多权重项,才那么些bias项,所以一般也不会影响结果。

实际应用中:我们最常见到的是,在全部的交叉验证集上使用L2正则化,同时我们在每一层之后用dropout,很常见的dropout概率为p=0.5,你也可以通过交叉验证去调整这个值。

2.4 损失函数

刚才讨论了数据预处理、权重初始化与正则化相关的问题。现在我们回到训练需要的关键之一:损失函数。对于这么复杂的神经网络,我们也得有一个评估准则去评估预测值和真实结果之间的吻合度,也就是损失函数。神经网络里的损失函数,实际上是计算出了每个样本上的loss,再求平均之后的一个形式,即L=1N∑iLi,其中N是训练样本数。

2.4.1 分类问题

  • 分类问题是到目前为止我们一直在讨论的。我们假定一个数据集中每个样本都有唯一一个正确的标签/类别。我们之前提到过有两种损失函数可以使用,其一是SVM的hinge loss:
Li=∑j≠yimax(0,fj−fyi+1)

另外一个是Softmax分类器中用到的互熵损失:

Li=−log⎛⎝efyi∑jefj⎞⎠
  • 问题:特别多的类别数。当类别标签特别特别多的时候(比如ImageNet包含22000个类别),层次化的Softmax,它将类别标签建成了一棵树,这样任何一个类别,其实就对应tree的一条路径,然后我们在每个树的结点上都训练一个Softmax以区分是左分支还是右分支。

  • 属性分类,上述的两种损失函数都假定,对于每个样本,我们只有一个正确的答案yi。但是在有些场景下,yi是一个二值的向量,每个元素都代表有没有某个属性,这时候我们怎么办呢?举个例子说,Instagram上的图片可以看作一大堆hashtag里的一个tag子集,所有一张图片可以有多个tag。对于这种情况,大家可能会想到一个最简单的处理方法,就是对每个属性值都建一个二分类的分类器。比如,对应某个类别的二分类器可能有如下形式的损失函数:

Li=∑jmax(0,1−yijfj)

其中的求和是针对有所的类别j,而yij是1或者-1(取决于第i个样本是否有第j个属性的标签),打分向量fj在类别/标签被预测到的情况下为正,其他情况为负。注意到如果正样本有比+1小的得分,或者负样本有比-1大的得分,那么损失/loss就一直在累积。

另外一个也许有效的解决办法是,我们可以对每个属性,都单独训练一个逻辑回归分类器,一个二分类的逻辑回归分类器只有0,1两个类别,属于1的概率为:

P(y=1∣x;w,b)=11+e−(wTx+b)=σ(wTx+b)

又因为0,1两类的概率和为1,所以归属于类别0的概率为P(y=0∣x;w,b)=1−P(y=1∣x;w,b)。一个样本在σ(wTx+b)>0.5的情况下被判定为1,对应sigmoid函数化简一下,对应的是得分wTx+b>0。这时候的损失函数可以定义为最大化似然概率的形式,也就是:

Li=∑jyijlog(σ(fj))+(1−yij)log(1−σ(fj))

其中标签yij为1(正样本)或者0(负样本),而δ是sigmoid函数。

2.4.2 回归问题

回归是另外一类机器学习问题,主要用于预测连续值属性,比如房子的价格或者图像中某些东西的长度等。对于回归问题,我们一般计算预测值和实际值之间的差值,然后再求L2范数或者L1范数用于衡量。其中对一个样本(一张图片)计算的L2范数损失为:

Li=∥f−yi∥22

而L1范数损失函数是如下的形式:

Li=∥f−yi∥1=∑j∣fj−(yi)j∣

注意:

  • 回归问题中用到的L2范数损失,比分类问题中的Softmax分类器用到的损失函数,更难优化。直观想一想这个问题,一个神经网络最后输出离散的判定类别,比训练它去输出一个个和样本结果对应的连续值,要简单多了。
  • 我们前面的博文中提到过,其实Softmax这种分类器,对于输出的打分结果具体值是不怎么在乎的,它只在乎各个类别之间的打分幅度有没有差很多(比如二分类两个类别的得分是1和9,与0.1和0.9)。
  • 再一个,L2范数损失健壮性更差一些,异常点和噪声都可能改变损失函数的幅度,而带来大的梯度偏差。
  • 一般情况下,对于回归问题,我们都会首先考虑,这个问题能否转化成对应的分类问题,比如说我们把输出值划分成不同的区域(切成一些桶)。举个例子,如果我们要预测一部电影的豆瓣打分,我们可以考虑把得分结果分成1-5颗星,而转化成一个分类问题。
  • 如果你觉得问题确实没办法转化成分类问题,那要小心使用L2范数损失:举个例子,在神经网络中,在L2损失函数之前使用dropout是不合适的。

如果我们遇到回归问题,首先要想想,是否完全没有可能把结果离散化之后,把这个问题转化成一个分类问题。

3. 总结

总结一下:

  • 在很多神经网络的问题中,我们都建议对数据特征做预处理,去均值,然后归一化到[-1,1]之间。
  • 从一个标准差为2/n−−−√的高斯分布中初始化权重,其中n为输入的个数。
  • 使用L2正则化(或者最大范数约束)和dropout来减少神经网络的过拟合。
  • 对于分类问题,我们最常见的损失函数依旧是SVM hinge loss和Softmax互熵损失。

深度学习与计算机视觉系列(4)_最优化与随机梯度下降\数据预处理,正则化与损失函数相关推荐

  1. 深度学习与计算机视觉系列(8)_神经网络训练与注意点

    深度学习与计算机视觉系列(8)_神经网络训练与注意点 作者:寒小阳  时间:2016年1月.  出处:http://blog.csdn.net/han_xiaoyang/article/details ...

  2. 深度学习与计算机视觉系列(9)_串一串神经网络之动手实现小例子

    深度学习与计算机视觉系列(9)_串一串神经网络之动手实现小例子 作者:寒小阳  时间:2016年1月.  出处:http://blog.csdn.net/han_xiaoyang/article/de ...

  3. 深度学习与计算机视觉系列(10)_细说卷积神经网络

    转载自: 深度学习与计算机视觉系列(10)_细说卷积神经网络 - 龙心尘 - 博客频道 - CSDN.NET http://blog.csdn.net/longxinchen_ml/article/d ...

  4. 深度学习与计算机视觉系列(1)_基础介绍

    转载自: 深度学习与计算机视觉系列(1)_基础介绍 - 龙心尘 - 博客频道 - CSDN.NET http://blog.csdn.net/longxinchen_ml/article/detail ...

  5. 深度学习与计算机视觉系列(3)_线性SVM与SoftMax分类器--在深度学习的视觉分类中的,这两个分类器的原理和比较

    作者: 寒小阳  时间:2015年11月.  出处:http://blog.csdn.net/han_xiaoyang/article/details/49999299  声明:版权所有,转载请注明出 ...

  6. 深度学习与计算机视觉系列(3)_线性SVM与SoftMax分类器

    作者: 寒小阳 &&龙心尘 时间:2015年11月. 出处:http://blog.csdn.net/han_xiaoyang/article/details/49999299 声明: ...

  7. 深度学习与计算机视觉系列(2)_图像分类与KNN

    作者: 寒小阳 &&龙心尘  时间:2015年11月.  出处:  http://blog.csdn.net/han_xiaoyang/article/details/49949535 ...

  8. 深度学习与计算机视觉系列(7)_神经网络数据预处理,正则化与损失函数

    作者:寒小阳 && 龙心尘  时间:2016年1月.  出处:  http://blog.csdn.net/han_xiaoyang/article/details/50451460  ...

  9. 深度学习与计算机视觉系列(5)_反向传播与它的直观理解

    作者:寒小阳 时间:2015年12月. 出处:http://blog.csdn.net/han_xiaoyang/article/details/50321873 声明:版权所有,转载请联系作者并注明 ...

最新文章

  1. 安装 ArcGISAPI31forSilverlight an i/o error occured while installing a file错误解决
  2. python综合学习七之TensorFlow初识
  3. 微信小程序开发之路(三)
  4. a推b等价于非a或b_分级基金谢幕倒计时 警惕分级B高溢价风险
  5. 如何在阿里云linux上部署java项目
  6. FPI(Fixed-point Iteration)不动点迭代法——迭代求方程的方法
  7. linux 内存对齐,Windows和Linux下的字节对齐(转载)
  8. Material使用09 MdCheckboxModule、MdMenuModule、MdTooltipModule
  9. Node.js中zlib模块的压缩/解压详解
  10. 免费申请国外免费域名超详细教程
  11. 三个“清洁代码”技巧将使您的开发团队效率提高 50%
  12. 笔记十一:提升高效技术领导的创新能力
  13. c语言智能手环程序,c+智能手环怎么用 c+智能手环使用图文教程
  14. 第一台计算机英语怎么说,世界第一台计算机英文缩写名为
  15. 微信公众号聊天底部菜单动画
  16. WORD程序失去焦点再获得焦点后无法使用鼠标功能的解决办法(已解决)
  17. java中函数编程的妙用
  18. matexs不支持鸿蒙,华为mateXs为何从兼容安卓改成基于安卓,是鸿蒙不行还是另有隐情...
  19. TCP/IP Socket 服务器对接
  20. T语言与快速开发平台之天宇联iOS开发手册

热门文章

  1. Java笔记14:泛型初探
  2. entity framework 6 我写了一个公用数据类
  3. 一直都想总结一下自己遇到过的web安全方面的问题--2015-03-31
  4. 在C#中使用代理的方式触发事件 (委托和事件 ) (转)
  5. Photoshop剪切板故障修复
  6. 长城电脑或收购夏新电子笔记本业务
  7. Go语言 Redis的使用
  8. Design Pattern Explained 读书笔记二——设计模式序言
  9. Windows 远程桌面管理
  10. __attribute__ ((packed))