本章的定位
在前面的章节中我们讨论了神经网络的静态部分:我们怎么设置网络连接、怎么设置数据、怎么设置损失函数。这一章我们研究一下动态部分,也就是参数的学习过程以及寻找最优参数的过程。
一、梯度检查
梯度检查的核心思想就一句话:把梯度的解析解和数值解拿过来进行比较就行了。【候选翻译:理论上,梯度检查非常简单,就比较梯度的解析解和数值解就行了。】但是实际上,这个过程非常的复杂和易错。以下是一些技巧、提示以及可能出现的问题:
1.1 用对称形式的求导公式
当计算梯度的数值解时,你可能见过这样一种有限差分的近似形式:
d
f
(
x
)
d
x
=
f
(
x
+
h
)
−
f
(
x
)
h
(不好,别用这个)
\frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h} \hspace{0.1in} \text{(不好,别用这个)}
dxdf(x)=hf(x+h)−f(x)(不好,别用这个)
其中h是非常小的数,实际应用中这个值大概是10的负5次方左右。
但是实践中,用对称形式差分公式更好:
d
f
(
x
)
d
x
=
f
(
x
+
h
)
−
f
(
x
−
h
)
2
h
(这个好,用这个)
\frac{df(x)}{dx} = \frac{f(x + h) - f(x - h)}{2h} \hspace{0.1in} \text{(这个好,用这个)}
dxdf(x)=2hf(x+h)−f(x−h)(这个好,用这个)
这个公式开销是单边形式的差分公式的两倍,因为你要对梯度的每一个纬度都计算两遍,但是它的精确度更高。为了证明这一点,你可以对f(x+h)和f(x-h)进行泰勒公式展开,之后你会发现f(x+h)的误差在O(h)这样一个阶数,而f(x-h)的误差在
O
(
h
2
)
O(h^2)
O(h2)这样一个阶数。
1.2 利用相对误差进行比较
对于梯度的数值解
f
n
′
f'_n
fn′以及梯度的解析解
f
a
′
f'_a
fa′这两个量说来,怎么比较?也就是我们到底以什么来衡量这两个量差不多一样还是相差很大。聪明如你可能马上就能想到这样一种方法,度量一下
f
a
′
f'_a
fa′与
f
n
′
f'_n
fn′之间的距离,利用这两个量差的绝对值或者平方,当这个值大于某一个阈值的时候就说明两者之间不匹配(这叫梯度检查失败)。但是这种方法有问题啊。假设这两个量的差是10的负4次方。这个值看起来不错吧。当两个量都是在1.0这个量级上的时候确实不错。但是当两种梯度的量级在10的负5次方的时候呢?10的负4次方这样一个差距可是比他们本身都要大100倍啊,这是妥妥的失败啊。所以更合理的方式是在比较前把量级先归一化,用下面的公式:
∣
f
a
′
−
f
n
′
∣
max
(
∣
f
a
′
∣
,
∣
f
n
′
∣
)
\frac{\mid f'_a - f'_n \mid}{\max(\mid f'_a \mid, \mid f'_n \mid)}
max(∣fa′∣,∣fn′∣)∣fa′−fn′∣
其实一般相对误差公式分母部分其实只要任意一项就行了。但是这里我倾向于用max()(或者add()),一方面显得对称,另一方面可以避免0作为除数的情况(在激活函数利用ReLUs时,这种特别容易出现)。尽管如此,还有一种极端情况必须留意,那就是两个量都是0的情况下通过了梯度测试。
- 相对误差 > 1 0 − 2 10^{-2} 10−2 一般都是有问题的
- 1 0 − 2 10^{-2} 10−2 > 相对误差 > 1 0 − 4 10^{-4} 10−4 这个结果会令你感到不安
- 1 0 − 4 10^{-4} 10−4 > 相对误差 如果目标函数有奇异点这个结果还行,但是如果目标函数没有奇异点 1 0 − 4 10^{-4} 10−4可是有点太高了
- 1 0 − 7 10^{-7} 10−7 >= 相对误差 完美,可以高兴一把
要记住神经网络越深,相对误差越高。如果你正打算训练一个10层的神经网络,对于输入的梯度检查来说,相对误差大概在 1 0 − 2 10^{-2} 10−2也许就可以了。因为误差随着深度的累计还会继续增大。但是 1 0 − 2 10^{-2} 10−2对于一个单层的差分函数来说还是有点太大了。
1.3 利用双精度
利用单精度浮点运算来计算梯度是一个常见的坑。如果你是利用单精度而不是双精度来计算梯度,即使你的梯度实现是没有问题的,相对误差也会非常高(高到 1 0 − 2 10^{-2} 10−2)。我自己就经历过,当我把单精度换成双精度之后,相对误差一下子就从 1 0 − 2 10^{-2} 10−2降到 1 0 − 8 10^{-8} 10−8。
1.4 尽可能在浮点数的有效范围内计算
What Every Computer Scientist Should Know About Floating-Point Arithmetic这篇文章非常值得你好好读读,它深入浅出的阐述了各种可能的错误,避免了这些错误,你的代码会更优质。举个例子,在神经网络里,在每个数据批次都对梯度进行正则化是非常常见的。【注:这句话原文有误,请参考Stick around active range of floating point 出现的一个是错误.md】也就是正则化项是要除以这个批量的数据点个数的。如果每个数据点的梯度都很小,那么当梯度除以这个批量所包含的数据个数时,结果也会变得很小。这就会导致很多数值上的问题啊。
所以,我个人经常把原始的数值梯度值和解析梯度值都打印出来看看。看看用于比较的这些数是不是太小了(
1
0
−
10
10^{-10}
10−10左右或者更小的数就非常令人担忧了)
数太小了可能出现溢出的情况。如果你想用一个常数暂时性的把它们(指前面的说道的数值太小的“解析解梯度”以及“数值解梯度”)把他们提到一个更好的浮点数范围内,在这个范围内,浮点数显得比较“稠密”。那么理想的范围是,这个提高后的这个数值在1.0的范围内,也就是10的0次方。【注:这里参考注释,怎么理解“稠密”注释:Stick around active range of floating point 怎么理解让浮点数更密集.md】
1.5 目标函数上的不可导点
需要注意一种引起误差的因素,也就是在“不可导点”上可能出现的问题。【关于kink参考注释:注释:kinks in the objective.md】
“kink”就是目标函数上的不可微分的部分,比如RuLU函数,SVM中的折页损失函数,Maxout激活函数。
其实这一小节阐述的问题其实是由““数值解”终究只是“解析解”的近似替代,总是会有误差”这样一个根本矛盾造成的。这个问题在《精英日课》中《宇宙是数字的吗》有着非常好的阐述。
咱们就以ReLU这个函数为例,我们讨论
X
=
−
1
0
−
6
X = -10^{-6}
X=−10−6这点。首先这点并不是0,虽然它离0很近,因此从理论上来说这点是可导的,存在解析解是吧。我们再看数值解的求解过程,能求得什么的结果取决你的h取多大,如果
h
>
1
0
−
6
h > 10^{-6}
h>10−6 那么f(x+h)这一项就越过了0点,它可就不是0啦,这个结果就不对啦。这就是这个问题的机制。
而且这个问题非常普遍,比如在CIFAR-10上利用SVM这个模型,将会包括450000个 max(0,x)项。这是由于CIFAR-10上包含50000个样本,而每个样本都会生成9个这样的项。这还不算什么,由于ReLU的存在,一个包含SVM的神经网络可能会用更多的kinks。
那么解决方法是什么呢?利用max(a,b)这个随时跟踪f(x+h)以及f(x-h)之间的相对大小关系,如果是在一个可导函数上,虽然单调性会发生变化,但是在单调性改变之前,单调性是稳定的,那么你会发现 f(x+h) 在某个阶段始终是大于(或者小于)f(x-h)的。然后你突然发现他们的大小关系发生变化了。那就说明要不然是单调性发生变化了,要不然是越过不可导点了。
1.6 利用少量数据点检测足矣
还有一种解决“由于不可导点存在造成的问题”的方法。目标函数中包含的数据点越少那么不可少点也越少。【这点可以参考注释:use only few datapoints.md】如果梯度验证在一个批次上的2到3个点上都通过了,那么这个批次也基本上没有问题。这样确实会节省很多开销,但是问题来了,那么要挑那些样本点进行检查呢?
1.7 数值解计算公式中的h
这个标题的原文是“be careful with the step size h”,可是h不是步进长度啊,小球移动到哪个点,主要看梯度更新到哪里啊。这个h就在计算数值解中才存在,就是一个临域的意思,没有别的什么意思。
h大多比较合适,上面在讲不可导点时候已经出现过一次,那个时候用的是
h
=
1
0
−
6
h=10^{-6}
h=10−6。h其实体现了“在多大程度上数值解能够逼近解析解”,它更像一个精度。那么精度是越精确越好吗(h越小越好吗)?不是的,h太小可能会受制于计算机本身精度的问题引起下溢。一个实践经验是,h在
1
0
−
6
10^{-6}
10−6到
1
0
−
4
10^{-4}
10−4比较好。维基上有篇文章描述了h的大小和梯度检查准确率之间的关系Numerical differentiation
1.8 在“典型”模式下进行梯度检查
梯度检查是作用于参数空间中的某些点上的(具体哪些点可能是随机的)。所以即使梯度检查在一些点上成功了,也不意味着梯度在全局上都成功。我们知道参数最早的取值是随机初始化的,但是这样的值在整个参数空间里不一定具备“典型性”。什么叫“典型性”,这个可是很重要的概念,这里就被作者一笔带过,我理解的是,参数空间内在还是有一些模式的,什么叫做模式,就是它的空间呈现某些规律性的特点,比如正弦函数,就是一种规律的曲线,但是损失函数是高维函数,他的图形是没法想象的,但是肯定很复杂,我们用二维的情况来做一个假设,有这么样一个曲线
这个曲线刚开始的时候是平的,到了中间才波浪起伏,而到了最后又平了。那么这个曲线的主要特征肯定句那段波浪起伏,如果随机初始化的点在A和在B都不是在非常典型的位置。而运行了一段的C位置可能是比较典型的位置。这就是我理解的“参数空间的典型性”。在不典型的参数空间上通过的梯度检测很可能在其它地方就通不过。
以SVM为例,当SVM的权重以一个非常小的值被初始化后,所有的数据点得到的分数几乎都为0。因此梯度会显示出一种特殊的模式。一个错误的梯度实现也能产生某种模式。但是随着参数进入一个有“典型性”的空间之后,在这个地方有些分值就比其它的分值大。这个模式是不具备在这个空间的泛化特性的。
基于上述原因,一个比较稳妥的做法是:预热一小段时间。也就是让学习进程跑一会儿。当loss开始下降的时候再进行梯度检查。
在开始的几次迭代中这样做存在危险,它可能会引入病态的边缘情况并且掩盖了梯度的错误实现。【在第一次迭代就进行梯度检查的危险就在于,此时可能正处在不正常的边界情况,从而掩盖了梯度没有正确实现的事实。 这一句知乎翻译的好】
1.9 不要让正则化覆盖了数据
因为一般损失函数都带着正则化项的。所以有一种情况要注意,就是正则化项盖过了数据损失项,以至于梯度基本上来自于正则化项
(正则化项的梯度表达式会更简单一些)。所以这样会掩盖数据损失项的梯度的错误实现。
解决的方案是:首先关掉正则化项单独的检测数据项,之后在单独的检查正则化项。检查正则化项的时候直接进入到代码中把数据损失项删掉。
还有一个方案是在梯度检查中增加正则化的强度到无法忽略的程度,然后不正确的实现就能被发现了。
1.10 记得关掉随机失活/数据扩张
在进行梯度检查的时候,记得关掉所有非决定因素比如随机失活、数据扩张等等。否则这些因素会形成干扰。关掉这些特性的缺点是,没法对他们进行梯度检查(比如说随机失活没法正确的反向传播)。因此更好的解决方案就是在计算f(x+h)和f(x-h)之前强制一个随机种子,计算解析梯度前也是这样
1.11 检查少数维度
在实践中,梯度有上百万的参数的规模。因此,一个可行的做法是检查梯度的一部分。其他没有检查的部分我们认为它是正确的。注意:要在每个不同的参数上的一些维度上进行梯度检查。在一些应用场景中,为了方便,人们会把参数放到一起抻成一个常常的向量。在这种情况下,偏置只占了整个向量中非常小的一部分,所以一定不要随机采样,而是把偏置的这种情况考虑进来并且检查全部的参数都收到正确的梯度。
二、合理性检查
在你一头扎进费时费力的优化过程之前,还是有些事值得你去认真对待的,比如说合理性检查。
2.1 观察特定执行状态的损失值是否正确
当你利用小参数初始化的时候,确保你获得期望的损失值。最好先单独的测试损失(也就是让正则化强度为0)。我们以softmax分类器在CIFAR-10上的表现为例,假设有十个分类,那么每个分类的扩散先验概率是0.1(跟均匀分布一样)。由于softmax 损失是概率的负对数,-ln(0.1) = 2.302,因此softmax分类器在一开始的损失值应该是2.302。对于 Weston Watkins SVM,我们假设一开始分类器对所有的分类都错,也就是所有的点都没有在边界外面呆着,也就是从图形上看,所有的样本点要不然是在边界内部,要不然是跑到分类器那头了,这叫“违反边界”(violate margin),这种情况下所有的计分总和为0,因此损失为9(因为对于每一个错误的分类来说边界为1)。如果你在初始化的时候没有看到这些值,那么可能在什么地方就有问题了。
2.2 增加正则化强度
作为合理性验证的第二步,增加正则化强度也会增加损失值。
为啥会增加呢?因为我们的L2正则化,L2正则化项一定是个正数,所以打开了正则化的损失肯定要大一点,因为你加了一个正数嘛。
加上正则化的目的就是无缘无故的给你惩罚而已。
2.3 在整个数据集上找一个小子集,在这个子集上实现过拟合
最后也是最重要的一点,在我们在完整的数据集上进行训练的之前,在一个小子集(比如说20个样本)进行训练,并且要达到损失值为0.
在这个过程中正则化强度最好也设置为0,否则你无法得到0损失。记住,一定要到你完成了这个测试之后再在在全量数据上进行训练,否则就没有意义。
注意,有可能即使你通过了这一步测试,你的实现也可能有问题。因为你的数据集上的数据特征分布是随机的,那么导致你在小样本上得到的学习效果无法泛化到整个数据集中。
三、像照顾baby一样仔细关注学习过程中的每一个细节
在训练过程中,我们需要监控某些量的变化情况。这些图表像一扇窗户一样能够让我们深入到训练的内部一探究竟,借助它我们能够对不同的参数所起的作用有一个直观的认识。
图表的横坐标代表时间一般用epoch作为单位。epoch代表这样一个时间——在训练的过程中对所有样本完成一次遍历所需要的时间。而在深度学习中,iteration代表的是一个batch的样本被完全遍历一遍的时间,而batch的大小的设置虽然有讲究但是理论上也是可以任意设置的。
3.1 损失函数
第一个要监控的值就是损失函数的函数值,这个值在前向传播的时候就已经在计算了,而且是在每个batch上都计算,这就意味着可能对其它参数来说是每个epoch计算一次,而这个参数是每个epoch会有很多个值,更准确点不好吗?
震荡的幅度与batch的大小有关。当batch size =1 震荡非常大。而当batch size = 整个数据集的时候,震荡幅度是最小的,因为每一次梯度更新导致loss的变化都是单调的。
也有的人喜欢在对数坐标下画loss function的图,因为这个曲线类似于一个对数曲线所以在对数坐标下画出来的图像应该是一条直线。这个看起来会比看起来像曲棍球一样的对数曲线看起来舒服一点。
当多个交叉验证模型的损失函数图线画在同一个图上时,他们之间的差异可以看得非常清晰。
什么样的曲线代表什么样的意思可以参考这个网站 lossfunctions.tumblr.com
3.2 训练精度/验证精度
第二个要监控的值是 训练精度/验证精度。这个图像能够对模型的过拟合程度有一个量化的描述。
训练精度和验证精度之间的落差体现了过拟合的程度。
一种情况是:蓝色的曲线精度很低,显示过拟合程度很大。当你看到这种情况出现的时候,你需要增加正则化的强度(增大L2权重惩罚,更多的随机失活)或者收集更多的数据。
另一种情况是:验证精度曲线和测试曲线紧贴着。这说明模型的能力不够强,需要再增加一些参数来增加模型的能力。
3.3 参数更新比
我们构造这样一个监控量——参数的变化与参数整体(变化之前)的比值。我称之为参数更新比。
那么什么是参数的变化,以vanilla SGD为例,参数的变化或者叫参数的更新(update)是参数在那点的梯度与学习率的乘积。
参数更新比的大小大概在10的负3次方。如果小于10的负3次方,学习率可能有点低。反之,学习率可能有点大。
我们还是以vanilla SGD为例,来说明具体的落地方法。在真实的代码中。
- 首先我们把权重矩阵铺平展开成一个一维的向量。
- 然后求这个向量的2范式或者2阶模。
- 求这个权重的更新,也就是损失函数在参数这点的梯度再乘以学习率
- 对这个权重的更新进行取模
- 最后是权重更新的模与更新之前的权重进行比,这是两个标量的比
# assume parameter vector W and its gradient vector dW
param_scale = np.linalg.norm(W.ravel())
update = -learning_rate*dW # simple SGD update
update_scale = np.linalg.norm(update.ravel())
W += update # the actual update
print update_scale / param_scale # want ~1e-3
3.4 每一层的激活梯度比分布情况
不好的初始化可以会降低学习的速度,甚至让学习停止。但是这个问题却有非常容易的诊断方法。为此我们再构造一个统计量。每一层神经元的激活值与“这层神经元梯度值”的比值。“这层神经元的梯度值”是一个简化的说法,更准确的说法是——损失函数在这层神经元与前一层神经元之间的连接上的权重所表示的这点的梯度。有了这个统计量之后,我们观察这个统计量在每一层神经元上的分布情况(用直方图)。
聊一聊深度学习的weight initialization这个就是一个例子
3.5 第一层可视化
左边的图明显有很多的噪点是吧,这说明这个是由问题(症状)的:网络没有收敛,学习率设置的不合理,正则化强度不够。
右边的图非常好,干净、平滑说明这个训练过程良好
四、参数更新
一旦利用反向传播计算出梯度的解析形式,我们就可以利用梯度来更新参数了。以下是一些参数更新的方法。
4.1 SGD与各种改进
4.1.1 串行更新
串行更新 这是一种最简单的更新方式,直接沿着负梯度方向更新参数(因为梯度指向的是增加的方向,但是这里我们是为了最小化损失函数,所以利用负梯度)。假设有有一个向量x,以及对应的梯度dx,最简单的更新方式如下:
# Vanilla update
x += - learning-rate * dx
其中 learning-rate 是一个固定值的超参数。当我们在整个数据集上进行计算,并且学习率足够小的时候,这个参数能够保证更新能够沿着损失函数的负梯度方向前进。
4.1.2 动量更新
动量更新 是另一种在深度学习过程中经常能获得不错收敛率的学习方法。这个方法的灵感来自物理学【我就意译了】,想象优化空间如同一个延绵起伏不断延伸的丘陵地带,优化过程类似于把一个小球轻轻地放在这个地带任何一个位置,让它自然运动直到停止,一般来说,当它停下来的时候这个小球肯定是在某一个最低点,不一定是全局最低点,但是一定是局部的最低点。
从物理的角度来理解:
位移:用符号x来表示
势能:U=Fx(势能的定义应该是mgh,这里我们用x代替h,用F代替mg)
速度:用符号v来表示
摩擦系数:用符号
μ
\mu
μ来表示
第一个公式,速度更新公式:
v
n
e
w
←
μ
v
o
l
d
−
α
d
x
v_{new} \leftarrow \mu v_{old} - \alpha dx
vnew←μvold−αdx
解释:dx 是 dU/dx的简写,因此dU/dx = F。而F = ma,当 m=1时,F=a。而v = at ,当t =1时,v=a。所以v = dx。而这个v的物理意义是在某一时刻速度的变化,也就是
Δ
v
\Delta v
Δv。
我来描述一下小球的动力学过程,一开始小球在山顶的时候,初始速度为0,而势能是最大的。随着小球往下滚,势能不断的转化为动能,小球的速度越来越快,当到达局部最低点的时候,速度应该变为最快,而最低点的势能为0,这个时候加速度最小变为0。根据牛一定律,如果没有外力存在,小球将不断的重复动能和势能的转化过程。因此这里加了一个摩擦力,以便让小球最终能够停下。最后小球在地步晃荡了几下之后停下。加速度随时间变化的曲线以及速度随时间变化的曲线如下图所示。
第二个公式,位移更新公式:
x
n
e
w
←
x
o
l
d
+
v
n
e
w
t
x_{new} \leftarrow x_{old} + v_{new}t
xnew←xold+vnewt
这个就不解释了。t认为是1。所以还可以写成
x
n
e
w
←
x
o
l
d
+
v
n
e
w
x_{new} \leftarrow x_{old} + v_{new}
xnew←xold+vnew
如果把这套物理的解释映射到优化过程中去。
物理中的位移x就对应优化空间中的参数w,
势能U对应的是优化空间中的损失函数L,
那么速度更新公式和位移更新公式,就可以改写为
v
n
e
w
←
μ
v
o
l
d
−
α
d
w
v_{new} \leftarrow \mu v_{old} - \alpha dw
vnew←μvold−αdw
w
n
e
w
←
x
o
l
d
+
v
n
e
w
w_{new} \leftarrow x_{old} + v_{new}
wnew←xold+vnew
其实这个算是一种启发式的方法吧。【这是第二版对于这部分的一个笔记,我感觉比第一版更清楚一点】
写成代码如下
# Momentum update
v = mu * v - learning_rate * dx # integrate velocity
x += v # integrate position
当交叉验证的时候,我们经常会从[0.5,0.9,0.95,0.99]这几个数里挑一个作动量参数。
类似于学习率的退火策略,在学习的最后阶段动量(摩擦系数)会增加,这种方法对整个优化过程也会有点帮助。一般用法是这样:一开始的时候动量设置为0.5,当经过了多轮(epoch)训练之后把它变成0.99。
利用动量更新方法,参数向量就可以沿着和梯度方向积累速度。
【这一部分第一版笔记】
动量更新 是另一种在深度学习过程中经常能获得不错收敛率的学习方法。这个方法的灵感来自物理学【我就意译了】,想象优化空间如同一个延绵起伏不断延伸的丘陵地带,优化过程类似于把一个小球轻轻地放在这个地带任何一个位置,让它自然运动直到停止,一般来说,当它停下来的时候这个小球肯定是在某一个最低点,不一定是全局最低点,但是一定是局部的最低点。我们把损失函数类比这个小球的重力势能(
U
=
m
g
h
U=mgh
U=mgh);由于力是能量函数相对于位移的梯度【
F
=
−
∇
U
F = - \nabla U
F=−∇U这个类似于速度定义S=Vt,S=f(t),v = df/dt。W = F S,W = f(s),f = dw/ds,可以参考大学物理】,所以损失函数的梯度可以看成作用在小球上的力;又由于加速度和力成正比(F=ma),当m=1的时候我们认为数值上力和加速度一样,我们就认为梯度就是加速度;接下来,我们利用这个公式$v =at $加速度是均匀的时候,加速度在时间上积累就是速度(其实准确来说加速度速度函数对时间的梯度),在时间是单位时间的情况下,速度又可以等于加速度,而加速度又等于力,力等于梯度,所以速度就以这样一个方式和梯度挂上钩了。之后还有再加上一个条件——牛一定律告诉我们,如果不施以外力,物体会始终保持匀速运动或静止状态——对于这个小球来说如果不施加一个外力,那么高处的势能是无法耗尽的,这个小球会在这个空间中永远运动下去,所以我们给速度增加一个衰减因子,让速度在每次迭代的时候都适当的衰减,那么最后小球就能慢慢的停下来了。这个衰减因子被习惯称之为“momentum”动量,这个就是动量更新的来源,其实从上面的分析来看,这个更应该是现实世界中的摩擦系数,叫“动量”应该算是一个约定俗成的“误传”
4.1.3 Nesterov 动量更新
Nesterov Momentmum
因为有凸优化理论作为基础,它的收敛性更好,而且在实际应用中表现一向也比标准的动量方法要好。
为了解释Nesterov动量更新的原理,我们从速度更新表达式开始说起。
v = mu * v - learning_rate * dx
(为了说明下面的推论还是需要一些前提的,为了不影响整体感,把这些放到本小节的后面)
其实这个式子背后提现了这样一个原理:下一时刻的速度应该由 「当前的速度」以及「质点因受力产生的速度变化」两部分共同构成。
式子的前一部分( mu * v)代表质点当前的速度状态,它是由上一个时刻的速度乘以摩擦系数得到,后一部分(learning_rate * dx)代表了作用在这个质点因重力产生的加速度进而产生了速度的变化,但是dx中的x是上一时刻的位置,dx确切的含义是上一时刻的位置带入到梯度解析式中得到关于这一时刻的重力对质点的速度影响。
这样你就看出毛病了把——利用上一时刻质点的位置信息计算这一时刻的加速度是否合适?是不是觉得有点不妥?总觉得这一时刻的影响效果应该由这一时刻的相关信息计算出来,也就是这一时刻的加速度应该用这一时刻的位置信息来计算比较合适。但是这一时刻的位置信息又取决于当前的速度信息,当前的速度信息中的一部分又需要当前的位置信息,有点死循环了。
解决的方法是引入一个新的概念——“趋势点”。(这是我对lookahead这个词的意译)
利用趋势点来近似的代替“这一时刻的位置”从而计算出当前时刻重力对质点的速度影响(重力加速度),之后在用这个速度和这一时刻的「当前速度」相结合得到下一时刻的速度。
“趋势点”是这样定义的:如果没有重力(也就是没有后一项),质点下一时刻的位置就是x + mu*v,我们可以称这个位置叫做“趋势位置”(lookahead)。这里也可以借助一下极限的思想,如果时间比较短的时候,x + mu * v 所表示的位置是质点最终到达的位置的一个临域。
代码表示成下面:
x_ahead = x + mu * v # 先计算出趋势点
v = mu * v - learning_rate * dx_ahead # 将趋势点带入梯度中作为当前的加速度效果
x += v
原理就是这样,但是在实践中人们希望用新方法中更新表达式的样子和之前方法中表达式样子接近。换句话说我们是要通过一些数学技巧把当前这个式子形式转换一下。
核心技巧就是把上面式子中的x_head用x来进行代替,换句话说并不让x_head显式的出现在更新的式子里,而是变通的用其他的量来代替。这里涉及到好几个变量,而且还涉及到这些变量在不同的迭代时刻,为了掰扯清楚这件事情,我们定义下面的变量:
真实位置: TP(Ture Postion)
趋势位置:LH(lookahead)
速度:V(这没啥可说的)
与时刻相关的下标: last(上一次) now(这一次) next(下一次)
摩擦系数 :
μ
\mu
μ
学习率:
α
\alpha
α
在一开始我们的关系是这样(允许“趋势位置”显式出现)
L
H
n
o
w
=
T
P
l
a
s
t
+
μ
V
l
a
s
t
LH_{now} = TP_{last} + {\mu}V_{last}
LHnow=TPlast+μVlast
V
n
o
w
=
μ
V
l
a
s
t
−
α
d
(
L
H
n
o
w
)
V_{now}={\mu}V_{last} - {\alpha}d(LH_{now})
Vnow=μVlast−αd(LHnow)
T
P
n
o
w
=
T
P
l
a
s
t
+
V
n
o
w
TP_{now} =TP_{last} + V_{now}
TPnow=TPlast+Vnow
这其实就是把上面的更新代码翻译成数学语言表达的样子,下标是特别有用的东西,这个方法灵感来自于SVM中SMO方法的推导,其实仔细想想,迭代这个概念我们在数学中还真的没有明确的遇到过,我们从来没有在数学中用一个变量表示不同时刻的值,或者说很少用一个明确的表示方法来将同一个自变量的不同时刻区别开来如果他们需要出现在同一个式子里。
在这三个式子里,
T
P
l
a
s
t
TP_{last}
TPlast,
V
l
a
s
t
V_{last}
Vlast是已知量,需要推导出
L
H
n
o
w
LH_{now}
LHnow,
V
n
o
w
V_{now}
Vnow ,
T
P
n
o
w
TP_{now}
TPnow
下面我们考虑这样一个问题
已知
L
H
n
o
w
LH_{now}
LHnow(将来会用x来代替),
V
l
a
s
t
V_{last}
Vlast,需要推导出
V
n
o
w
V_{now}
Vnow,
L
H
n
e
x
t
LH_{next}
LHnext。
要推导
L
H
LH
LH,
T
P
TP
TP是不可缺少中间变量的,但是不能出现在式子中,需要变通的方式代替它。
根据上面的式子我们知道
L
H
n
e
x
t
=
T
P
n
o
w
+
μ
V
n
o
w
LH_{next} = TP_{now} + {\mu}V_{now}
LHnext=TPnow+μVnow
而
T
P
n
o
w
=
T
P
l
a
s
t
+
V
n
o
w
TP_{now} = TP_{last} + V_{now}
TPnow=TPlast+Vnow
其中$TP_{last} = LH_{now} - {\mu}V_{last}
(
由
上
面
的
(由 上面的
(由上面的 LH_{now} =TP_{last} + {\mu}V_{last} $ 推导而来)
带入得到:
L
H
n
e
x
t
=
L
H
n
o
w
−
μ
V
l
a
s
t
+
V
n
o
w
+
μ
V
n
o
w
LH_{next} = LH_{now} - {\mu}V_{last} +V_{now} + {\mu}V_{now}
LHnext=LHnow−μVlast+Vnow+μVnow
其中
V
n
o
w
=
μ
V
l
a
s
t
−
α
d
(
L
H
n
o
w
)
V_{now} = {\mu}V_{last}-{\alpha}d(LH_{now})
Vnow=μVlast−αd(LHnow)
于是更新的主要式子就是:
1
V
n
o
w
=
μ
V
l
a
s
t
−
α
d
(
L
H
n
o
w
)
V_{now} = {\mu}V_{last}-{\alpha}d(LH_{now})
Vnow=μVlast−αd(LHnow)
2
L
H
n
e
x
t
=
L
H
n
o
w
−
μ
V
l
a
s
t
+
V
n
o
w
+
μ
V
n
o
w
LH_{next} = LH_{now} - {\mu}V_{last} +V_{now} + {\mu}V_{now}
LHnext=LHnow−μVlast+Vnow+μVnow
你会发现第二个式子既需要
V
n
o
w
V_{now}
Vnow也需要
V
l
a
s
t
V_{last}
Vlast
所以需要一个单独的变量「v_prev」来保存
V
l
a
s
t
V_{last}
Vlast ,之后再把这两个式子中的
L
H
LH
LH换成「x」,V换成「v」,就得到了更新代码:
v_prev = v # 需要一个单独的变量保存上一次的速度
v = mu * v - learning_rate * dx # 对应上面的1式
x += -mu * v_prev + (1 + mu) * v # 对应上面的2式
本小结用到推论前提
- 由于这里设质点的质量为1,时间也为1,工具力学公式,有些量之间的大小是一样的,在推理的时候以下概念可能会混用。
- 损失函数相当于重力势能
- 梯度是能量函数的导数也就是重力,或者说是重力作用力
- F=ma 所以 力作用 = 加速度
- v=at 所以 加速度 = 速度
- s = vt 所以 速度 = 位移
- p = mv 所以 动量 = 速度
- v = mu * v - learning_rate * dx 这个式子到底叫做动量更新表达式还是速度更新表达式呢?在质点质量是1的情况下不区分这两种说法。
- x表示参数,也可以看成是质点某一时刻的位置;dx是求出损失函数的梯度解析式再带入x所代表的坐标得到的值,我们知道某一点的梯度代表的是那一点的切线,也可以称之为趋势。
我们推荐进一步阅读以搞懂这些问题的本源以及 NAG(Nesterov’s Accelerated Momentum)的数学公式
- Advances in optimizing Recurrent Networks Yoshua Bengio,Section 3.5
- Ilya Sutskever’s thesis 在section7.2 对这个主题有更长的解释
4.2 学习率退火
在训练深度网络的时候,随着训练时间进行,对学习率进行退火是有帮助的。你应该始终记住这点:学习率太高意味着系统包含了太多的动能以至于参数向量蹦来蹦去,而没法逐渐的停留在损失函数的“低谷”地区(deep and narrow,准确的翻译应该是地势低并且狭长的地带)。至于什么时候开始衰减学习率则是一个技术活:衰减太慢不行(太慢的话意味着你浪费了太长时间计算资源,这段时间里参数变化非常剧烈但是收敛非常缓慢),衰减太快也不行(系统太早停下来可能导致质点还没有达到最佳的位置)。下面是三种常用的衰减手段:
- 步进衰减: 每过几轮就把学习率调低一点(依据某些原则)。典型数值设置可以这样——每5轮就把学习率削减一半,或者每20轮削减0.1。这些数值的设置非常依赖于问题的类型或者模型的类型。在实践当中,有一种启发式的方法,就是观察“验证集精度”(validation error)曲线变化。首先先用一个固定的学习率训练,当“验证集精读”无法再变得更好的时候就在原有的学习率上减去某一个常数。
- 指数衰减: 指数衰减是自然界普遍的一种变化规律,比如放射性元素的半衰期,药物在人体内的代谢动力学都符合这个规律,包括推导逻辑回归用到的“几率”也是这种思想的一个体现。衰减率的数学表达是 α = α 0 e − k t \alpha = \alpha_0 e^{-k t} α=α0e−kt,其中 a l p h a 0 alpha_0 alpha0和 K K K是超参数,t可以用迭代次数或者训练轮数来代入。
- 1/t 衰减: 其数学表达式为 α = α 0 / ( 1 + k t ) \alpha = \alpha_0 / (1 + k t ) α=α0/(1+kt),其中 a l p h a 0 alpha_0 alpha0和 K K K是超参数,t可以用迭代次数或者训练轮数来代入。
在实践中,我们发现“步进衰减”可能稍微更合适一点。因为“步进衰减”中包含的超参数(包含两个:一个是每次衰减多少、一个是走几步衰减)可能比K有更好的可解释性。最后,如果你的计算资源充沛,哪怕是慢一点也最好选择较慢的衰减和较长时间的训练。
4.3 二阶方法
在深度学习中,还有一类流行的优化方法是基于“牛顿法”的发展起来的。这类方法属于二阶方法。它的更行迭代方式如下:
x
←
x
−
[
H
f
(
x
)
]
−
1
∇
f
(
x
)
x \leftarrow x - [H f(x)]^{-1} \nabla f(x)
x←x−[Hf(x)]−1∇f(x)
这里,
H
f
(
x
)
H f(x)
Hf(x)是海塞矩阵,它是由损失函数的二阶偏导构成的矩阵。
∇
f
(
x
)
\nabla f(x)
∇f(x) 是一个由梯度构成的向量,在“梯度下降”那一节中我们已经遇见过。直观上讲,海塞矩阵描述损失函数的局部曲率,它会帮我们更有效的进行更新。尤其是当乘以海塞矩阵的逆矩阵的时候可以让优化的过程得到改进,质点在比较平缓的曲面上行进的时候会用比较大的步子走,而在比较陡峭的曲面上行进的时候会用较小的步子走。还有一点非常重要,在这个二阶的更新公式中不存在任何超参数,这个特点被这类方法的拥趸们认为是比一阶方法强太多的地方。
但是,在实践中这种方法在深度学习领域里非常的不实用,主要原因就在于计算海塞矩阵的开销太大(不管是时间上的还是空间上的)。举个例子,一个包含100万个参数的神经网络,其海塞矩阵的规模会达到 100 万 X 100 万这个量级,大约会占用3725GB的运行内存空间。因此,为了近似的求解逆矩阵,人们开发了多种多样的算法,这一类算法基本都是拟牛顿算法。在其中最著名的称之为“L-BFGS”,它利用随时间变化的梯度信息隐式的构造估计值。
但是,即便是解决了内存方面的问题,原生的“L-BFGS”有一个巨大的缺陷就是它必须在完整的训练集上进行计算。要知道一个完整的训练集可以有上百万个样本啊。不像 mini-batch SGD,让“L-BFGS”能够在mini-batches运行是件非常具有技术含量的事情而且也是现在研究的热点。
在实践中,在大规模深度学习网络或者卷积神经网络中使用"L-BFGS”以及类似的二阶方法还比较少见。相应的,基于(Nesterov)动量更新的各种变体更常见因为比较简单而且易于扩展。
更多参考:
- Large Scale Distributed Deep Networks 来自于谷歌大脑的文章,在大规模分布式的环境中比较了 L-BFGS 以及 SGD 各种变体的优化情况。
- SFO 尝试将SGD和L-BFGS的优点综合起来
4.4 逐个参数自适应学习率方法
到目前为止我们之前讨论的所有方法在调整学习率的时候都是全局性的调整,也就是对每个参数来说都一样,都用一样的学习率。调整学习率耗时耗力,因此人们设计了很多方法以期望能够自动调整学习率,甚至自动调整每一个参数。虽然许多方法仍然需要其它超参数设置,但是其核心关注点是,相对于原始学习参数来说,它们在更大范围内的超参数范围内表现良好。在这一节中,我们着重介绍一些常见的自适应方法。
4.4.1 Adagrad
Adagrad 最早是由 Duchi 等人提出的一种自适应学习率方法。
# Assume the gradient dx and parameter vector x
cache += dx**2
x += -learning_rate * dx / (np.sqrt(cache) + eps)
变量“cache”占据和梯度一样的空间大小,跟踪每一个参数与梯度平方的和。然后,它将用来规范化参数更新步骤,以“element-wise”的方式。注意那些收到高梯度的权重将减小它们的有效学习率,那些收到很小更新的权重将增加它们的有效学习率。有趣的是,平方根操作似乎非常重要,如果没有它算法的性能会变差。平滑项eps(常常设置成1e-4到1e-8)的使用避免了除以0的情况发生。Adagrad的一个缺点是,在深度学习的任务中,单调学习率被证明太过激进而且过早停止学习。
4.4.2 RMSprop
RMSprop 是一种非常有效,但是当前没有公布的自适应学习率方法。有趣的是,所有用这个方法的人都注明他们这个方法出自于Geoff Hinton在coursera课程中的讲解。RMSprop用了一些简单的方法在Adagrad方法上做了一些调整,使其变得不那么激进,单调地减少学习率。特别地,它利用梯度平方的滑动平均值作为替代:
cache = decay_reate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx /(np.sqrt(cache) + eps)
这里,decay_rate是一个超参数,常在[0.9,0.99,0.999]中取值。注意 x + = 更新和 Adagrad一样,但是变量 cache 是“泄露的”(这里是一语双关,缓存斜率了,但是其实cache 只是滑动平均变化,相对于原来那种不变的更新方式)。因此,RMSprop依然是基于各个权重的量级来调整每个权重的学习率,这种方式会有一种有益的均衡化效果,但是不像 Adagrad,其更新并不是单调的变小的。
4.4.3 Adam
Adam 是最近才被提出的更新方法。它有点像结合了动量更新的 RMSprop 。(简化的)更新方法如下:
m = beta1*m + (1 - beta1)*dx
v = beta2*v + (1-beta2)*(dx**2)
x += - learning_rate * m / ( np.sqrt(v) + eps )
注意这个更新方式和 RMSprop 非常像,除了替代原始的梯度向量而改用梯度的“滑动”版本(也就是m)之外。相关的推荐值是 eps = 1e-8 , beta1 = 0.9 , beta2 = 0.999 . 在实践中 Adam方法被推荐作为默认的优化方法而被使用,而且效果比 RMSprop 好那么一点点。 但是, 作为一种替代方案,利用 SGD + Nesterov 也是值得尝试的。 完整的 因为在“完全进入状态”之前、更新进程最开始那几步,由于向量m ,v 都被初始化导致偏置为0,作为一种“补偿作用”,Adam 包含了这用这样一种称之为“偏置校准”的机制。包含“偏置校准”的更新如下:
# t 是迭代次数,范围从1到正无穷
m = beta1*m + (1 - beta1)*dx
mt = m / (1 - beta1**t)
v = beta2*v + (1-beta2)*(dx**2)
vt = v / (1-beta2**t)
x += - learning_rate * mt / ( np.sqrt(vt) + eps )
注意,如同其他参数一样,更新式也变成了迭代的函数。我们会让读者查阅论文细节,或者课程幻灯片上对此进行展开。
更多的参考:
- Unit Tests for Stochastic Optimization 针对随机优化设计了一系列测试,这些测试结果可以作为基线。
动画能够帮你对学习过程中的变化过程有一个直观的印象,左边:损失函数的等高线以及不同的优化算法随时间变化的过程。注意基于动量方法的“过火”行为,它是优化看起来像小球滚下山一样。右边:这是某个优化空间中的一个鞍点的图像,不同维度方向上的曲率有不同的符号(一个维度向上弯曲,一个维度向下弯曲)
【这句话这么理解:
咱们就以note上的这个三维的优化空间为例,这是一个比较简单的情况,损失函数只有两个参数需要优化。
曲率是什么概念,是反映曲线弯曲程度的一个值,具体的公式我忘了,但是弯曲程度越大的曲线相同的角度扫过的线段长度也越长嘛,但是我记得好像曲率也有一个微积分定义的。这里不管了啊,应该不影响理解,因为曲率这个东西最好还是从形象的角度去理解。
纬度是什么感觉,在这张图上就是X轴和Y轴嘛。这张图也没标哪个轴是X轴哪个轴是Y轴。那我就随便定一下了。
以鞍点(鞍点也正好是X轴、Y轴的原点)为出发点,沿着X的方向是往下走的嘛。如果看剖面图,就是一个向下的曲线。同理沿着Y轴是向上的。
】
注意到,SGD很难打破对称性,而且在最高点停留了很久。相反,类似于RMSprop则在鞍点的方向达到了相当低的梯度。由于RMSprop更新式中的分母部分会增加沿着这个方向的学习速率,因此加速了RMSprop进程。图片来源:Alec Radford
五、超参数优化
训练神经网络涉及大量的超参数设置。最常见的超参数包含:
- 初始学习速率
- 学习率衰减策略(比如设置衰减率常数)
- 正则化强度(L2 惩罚,随机失活强度)
但是如同我们看到的那样,有很多不那么敏感的超参数,比如“逐个参数适应学习方法”,动量与其策略的设置。在这一部分中,就超参数的搜索问题我们会描述一些更多的技巧和注意事项。
5.1 实现
大型的神经网络一般需要很长时间训练,因此调参这事一干就是好多天(好多星期)。记住这一点很重要,因为它会影响代码底层的设计。有一种设计是让worker连续地随机设置超参数并执行优化。在训练的过程中,worker对每一轮训练后的验证集的准确率进行跟踪,并且将模型的检查点(这里包含各种各样的训练统计数据比如随时间变化的损失情况)写入到一个文件中,最好是共享文件系统中的文件中。最好将验证集的准确率也写到那个检查点的文件名里面,这样方便我们后期进行查看和分类。第二个程序我们称之为master,它的作用是在计算机集群中启动或者终止worker,也可能对worker保存的检查点进行进一步的检查,绘制训练统计数据的图表。
【注:这一段如果你真的看过现代的代码是怎么跑的,你就知道了。光靠这点说没有用。】
5.2 选择一个验证集进行验证而不用交叉验证
在大多数情况下,一个单一的包含客观数量的验证集就能起到作用了,并不需要利用多折来进行交叉验证。【注:这里我把simplifies the code base 进行意译了,因为你就用但一个一个验证集进行验证嘛,自然代码就可以少写很多,其实这里想表示的本质意思是,一个验证集就足以起到作用了】。你可能会听到人们说他们“交叉验证”一种参数,但是实际上他们只用了一个验证集。
5.3 超参数范围
在对数数量级内寻找超参数。比如说一个典型的学习率设置可能看上去这样: learning_rate = 10 ** uniform(-6, 1)。 也就是说我们从均匀分布里生成了一个随机数,但是把它提到幂的位置上(相对于10的)。这个策略也可以用在正则化强度的设置上。直观上,这是因为学习率和正则化强度对于训练的动态进程有乘的效果。比如说,对于同一个增加量0.01来说,当学习率是0.001时,增加这样一个值会对训练过程产生巨大的影响。但是当学习率是10的时候,这个增加量几乎起不了什么作用。这是由于在更新的过程中,学习率与更新好梯度是相乘的关系(梯度更新里是有学习率这个参数的,嵌套的带带入一下,学习率就是相乘的)。因此我们很自然的想到学习率的范围乘以或者除以某一个值,而不是学习率的范围加上或减去某一个值。有一些参数(比如失活强度)则是在原有的数量级中调整(eg. dropout = uniform(0,1))
5.4 选择随机搜索而不是网格搜索
如同 Bergstra 和 Bengio 在Random Search for Hyper-Parameter Optimization论证的那样,随机选择的试验比网格上的试验更有效地进行超参数优化。就想它所表现出来的,也更容易实现。
Bergstra 和 Bengio所著的Random Search for Hyper-Parameter Optimization中说明其核心思想的图片。大多数情况下,一些超参数要比其它超参数重要的多。利用随机搜索而不是网格搜索可以让你更准确的发现那些更重要的超参数好数值0。
5.5 留意在边界处的最优值
有的时候会发生这种情况:你从一个非常坏的区域寻找超参数。比如说,假设我们利用 learning_rate = 10 ** uniform(-6,1)。一旦我们得到结果,对最终的学习率不在这个间隔的边缘进行双重验证,这点非常重要。否则,你可能就会错失获得哪些超过间隔的更优参数设置。【这句话看得真是不明不白。参考了知乎翻译,这个意思是这样的,其实这个跟调参没有关系了,这就是数学可能性,当你得到的最优解是在区间的边缘上的时候,你要猜测有没有可能在边缘之外可能有更好的解】
5.6 搜索要从粗调到精调
在实践中,首先从一个比较大的范围进行快速搜索,之后在根据最有可能在哪儿出现最优值缩小这个范围。粗略搜索只需要在第一个训练周期进行,甚至第一个训练周期都不用执行完。因为大多数参数的设置根本不起作用,要不然就是这些参数根本就没法让模型学习,要不然就是让损失函数直接爆炸。在第二个阶段在较小的范围里搜索5个周期。在最后的阶段里,在最小的那个搜索范围里进行深度细致的搜索,这个过程要重复很多的周期。
5.7 贝叶斯超参数优化
贝叶斯超参数优化是一个完整的研究领域,这个领域的主要目的就是提出一些算法来更有效的在超参数空间进行搜索。它的核心理念是——在不同的超参数设置下查看其性能的时候,平衡探索-利用。基于这个模型,有很多库被开发出来了。比较有名的有 Spearmint, SMAC以及 Hyperopt。然而,在卷积神经网络的实际使用中,比起上面介绍的先认真挑选的一个范围,然后在该范围内随机搜索的方法,这个方法还是差一些。更多与实战相关的讨论在这里。
六、评价
模型集成
在实践中,为了能将神经网络的性能提高几个百分点,一种比较可靠的方式是训练多个独立的模型,然后在测试阶段综合它们的预测结果。集成模型的性能会随着模型的个数单调的增加(尽管收益率递减)。还有,模型的多样性程度越高,模型改进效果越明显。下面是一些构造集成模型的方法。
- 模型相同,初始化参数不一样 : 利用交叉验证决定最好的超参数,之后在不同的随机初始化条件下训练多个最优超参数组合。这个方法的危险性在于,多样性仅取决于初始化。
- 交叉验证中发现的最好模型 :利用交叉验证决定最好的超参数,选最好的前10个构成集成模型。这个方法改进了多样性,但是不足的地方是包含了次优模型。在实践中,这个比较容易实现因为在交叉验证之后不需要额外的训练模型了。
- 单一模型,不同的检查点 :当训练非常耗时时,将单个神经网络在训练中生成的若干记录点拿出来做成一个集成模型,这种方法多少也起点作用。
虽然缺乏多样性,但是在实践中工作的还行。这种方法的特点是构造起来比较容易。 - 在训练的过程中运行参数平均值:这种方法跟上一个提到的方法有点关联。也是一种很廉价的方法,但是几乎总是可以提高一到两个百分点的性能。它的方法是保存一份网络参数副本到内存中。更准确的说是对训练过程中产生的参数,对它们进行指数衰减求和(是一个累加的过程),并把结果保存在内存中【这句话是参考后面的文本根据自己的理解写出来的,而且我觉得知乎专栏写的不对】。用这种方法你其实把网络最后几次迭代的状态进行了平均。这种把最后几步权重进行平滑的方法往往能有更好的验证误差。对这种现象一种直观的解释是:咱们目标函数是一个碗状空间,我们的网络在这个空间上跳跃,我们对网络参数做平均有助于这个网络跳到中心去。
集成模型的缺点之一在测试集上获取评价指标需要花很长时间。感兴趣的读者可以看一下 Geoff Hinton最近的工作,他在“Dark Konwledge”中提出一种想法,他的想法是把集成模型通过一种“蒸馏”的方法萃取成一种单一的模型,其方法是把集成模型的对数似然概率合并到改进的目标函数里。
七、总结
训练一个神经网络:
- 利用小批量的数据对你的代码实现进行梯度检查,注意一些坑(pitfall)
- 进行合理性检查。为此你要做到:保证训练一开始的时候你的损失值是合理的。保证你的模型在小份数据上的训练精度能达到100%。
- 在训练的过程中监控这些值:损失(随时间的变化)、训练精度/验证精度。如果你想来点花哨的,可以把跟参数有关的更新变化也监控起来(不管是什么参数的更新,这种更新的变化应该都在1e-3)。如果你在训练卷积网络,第一层权重也要监控。
- 推荐两种更新方式: SGD+Nesterov , Adma
- 设计一种机制令学习率随着训练进程的深入逐步的减少。比方说每隔几轮就调低一点学习率,或者当验证精度达到最高不再增长的时候再减少一点。
- 利用随机搜索的方法来搜索最优超参数(不要用网格搜索)。从粗调到精调
- 利用集成的方法提高性能。
八、更多参考
- Leon Bottou 写得有关SGD的技巧
- Yann LeCun 写得Efficient BackProp
- Yoshua Bengio 所著的Practical Recommendations for Gradient-Based Training of Deep Architectures