关键词:神经网络 Neural networks、正向计算 Forward computation、反向传播 Backward propagation、神经元 Neuron Units、最大边际损失 Max-margin Loss、梯度检查 Gradient checks、Xavier参数初始化 、学习率 Learning rates、Adagrad。
讲义:神经网络、反向传播 (Lecture Notes: Neural Networks, Backpropagation)
课程讲师:Christopher Manning、Richard Socher
讲义作者:Rohit Mundra、Amani Peddada、Richard Socher、Qiaojing Yan
本文上一部分请见:CS224n: Natural Language Processing with Deep Learning 笔记、文献及知识点整理(六)神经网络、反向传播(二)_放肆荒原的博客-CSDN博客
2 神经网络:提示和技巧(Neural Networks: Tips and Tricks)
有了神经网络的数学基础,现在我们分享一些在实践中使用神经网络时常用的技巧。
2.1 梯度检查
在上一节中,我们详细讨论了如何通过基于微积分(解析)的方法计算神经网络模型中参数的误差梯度/更新。这里我们现在介绍一种数值近似这些梯度的技术——尽管计算效率太低,无法直接用于训练网络,但这种方法可以非常精确地估计任何参数的导数;因此,它可以作为一个对分析导数的正确性的健全检查。给定一个具有参数向量θ和损失函数J的模型,周围的数值梯度可通过中心差分公式简单给出:
式中ε是一个小数字(通常约为). 术语是在给定输入下,当我们将参数θ的第i个元素扰动+ε时,向前传递计算的误差。类似地,术语是当我们对参数θ的第i个元素扰动−ε时,向前传递计算的误差。因此,使用两个前向过程,我们可以近似模型中任何给定参数元素的梯度。我们注意到,数值梯度的定义非常自然地遵循导数的定义,在标量情况下:
【注:】梯度检查是比较分析梯度和数值梯度的好方法。 分析梯度应该很接近,并且可以使用以下方法计算数值梯度:
和可以使用两个前向传递进行评估。 在 Snippet 2.1 中可以看到它的一个实现。
当然,有一点不同——上面的定义只是在正方向扰动 x 来计算梯度。 虽然以这种方式定义数值梯度是完全可以接受的,但实际上使用中心差分公式通常更精确和稳定,我们可以在两个方向上扰动参数。 直觉上,为了更好地近似一个点周围的导数/斜率,我们需要检查函数 f 在该点左侧和右侧的行为。 还可以使用泰勒定理(Taylor)证明,中心差分公式的误差与成正比,并且非常小,而导数定义更容易出错。
现在,你可能会问一个自然的问题,如果这个方法如此精确,为什么我们使用它来计算所有的网络梯度,而不是应用反向传播?正如前面所暗示的,简单的答案是效率低下——回想一下,每次我们想要计算一个元素的梯度时,我们都需要在网络中进行两次前向传递,这在计算上非常昂贵。此外,许多大型神经网络可以包含数百万个参数,每个参数计算两次传递显然不是最优的。而且,由于在优化技术(如SGD)中,我们必须在数千次迭代中每次迭代计算一次梯度,很明显,这种方法很快变得难以处理。这种低效率就是为什么我们只使用梯度检查来验证我们的分析梯度的正确性,这要快得多。梯度检查的标准实现如下所示:
#Snippet 2.1
def eval_numerical_gradient(f, x):
"""
a naive implementation of numerical gradient of f at x
- f should be a function that takes a single argument
- x is the point (numpy array) to evaluate the gradient
at
"""
fx = f(x) # evaluate function value at original point
grad = np.zeros(x.shape)
h = 0.00001
# iterate over all indexes in x
it = np.nditer(x, flags=[’multi_index’], op_flags=[’readwrite’])
while not it.finished:
# evaluate function at x+h
ix = it.multi_index
old_value = x[ix]
x[ix] = old_value + h # increment by h
fxh_left = f(x) # evaluate f(x + h)
x[ix] = old_value - h # decrement by h
fxh_right = f(x) # evaluate f(x - h)
x[ix] = old_value # restore to previous value (very important!)
# compute the partial derivative
grad[ix] = (fxh_left - fxh_right) / (2*h) # the slope
it.iternext() # step to next dimension
return grad
2.2 正则化(Regularization)
与许多机器学习模型一样,神经网络很容易过度拟合,模型能够在训练数据集上获得近乎完美的性能,但失去了对看不见的数据进行泛化的能力。 用于解决过度拟合(也称为“高方差问题”)的常用技术是结合 正则化惩罚。 我们的想法是,我们简单地在损失函数 J 上附加一个额外的项,这样总成本现在计算为:
【注:】矩阵 U 的 Frobenius 范数定义如下:
在上述公式中,是矩阵(网络中的第i个权重矩阵)的Frobenius范数,λ是控制正则化项相对于原始代价函数的权重的超参数。由于我们试图最小化,正则化的本质是惩罚权重过大,同时优化原始成本函数。由于Frobenius范数的二次性(计算矩阵的平方元素之和),正则化有效地降低了模型的灵活性,从而减少了过拟合现象。
施加这样的约束也可以解释为先验贝叶斯信念,即最优权重接近于零——接近程度取决于λ的值。选择正确的λ值至关重要,必须通过超参数调整来选择。λ的值太高会导致大多数权重设置得太接近0,并且模型无法从训练数据中学习到任何有意义的东西,通常在训练集、验证集和测试集上获得较差的准确性。值太低,我们再次陷入过度拟合的领域。必须注意的是,偏差项没有被规范化,也不会对上述成本项产生影响——试着想想为什么会出现这种情况!
确实有时会使用其他类型的正则化,例如 L1 正则化,它对参数元素的绝对值(而不是平方)求和 —— 然而,这在实践中不太常用,因为它会导致参数权重的稀疏性。 在下一节中,我们将讨论 dropout,它通过在前向传播中随机丢弃(即设置为零)神经元来有效地充当另一种形式的正则化。
2.3 Dropout
Dropout是一种强大的正则化技术,由Srivastava等人在《Dropout:防止神经网络过度拟合的简单方法》一文中首先提出。这个想法简单而有效——在训练过程中,我们将以某种概率(1-p)在每次向前/向后传递过程中随机“丢弃”一个子集神经元(或者等效地,我们将以概率p保持每个神经元的存活)。然后,在测试期间,我们将使用完整的网络来计算我们的预测。结果是,网络通常从数据中学习更多有意义的信息,不太可能过度拟合,并且通常总能在手头的任务中获得更高的总体性能。这项技术之所以如此有效,一个直观的原因是dropout所做的基本上是一次性以指数形式训练许多较小的网络,并对其预测进行平均。
【注:】
Dropout应用于人工神经网络。图片来源于Srivastava等人。
在实践中,我们引入 dropout 的方式是,我们取每一层神经元的输出 h,并以概率 p 保持每个神经元,否则将其设置为 0。然后,在反向传播期间,我们只通过梯度通过神经元 在向前传球期间保持活着。 最后,在测试期间,我们使用网络中的所有神经元计算前向传递。 然而,一个关键的微妙之处在于,为了使 dropout 有效地工作,测试期间神经元的预期输出应该与训练期间大致相同——否则输出的幅度可能会完全不同,并且神经元的行为 网络不再定义良好。 因此,我们通常必须将测试期间每个神经元的输出除以某个值——留给读者作为练习来确定该值应该是多少,以便训练和测试期间的预期输出相等。
2.4 神经元单元
到目前为止,我们已经讨论了包含 sigmoidal 神经元来引入非线性的神经网络; 然而,在许多应用中,可以使用其他激活函数来设计更好的网络。 这里列出了一些常见的选择以及它们的函数和梯度定义,这些可以用上面讨论的 sigmoidal 函数代替。
Sigmoid:这是我们讨论过的默认选择; 激活函数 σ 由下式给出:
σ(z) 的梯度为:
图 9:sigmoid 非线性响应
Tanh:tanh 函数是 sigmoid 函数的替代方法,通常发现它在实践中收敛速度更快。 tanh 和 sigmoid 的主要区别在于 tanh 的输出范围为 -1 到 1,而 sigmoid 的输出范围为 0 到 1。
tanh(z) 的梯度为:
图 10:tanh 非线性的响应
Hard tanh:hard tanh 函数有时比 tanh 函数更受欢迎,因为它的计算成本更低。 然而,当 z 大于 1 时,它确实会饱和。hard tanh 的激活是:
导数也可以用分段函数形式表示:
图 11:hard tanh 非线性的响应
Soft Sign:Soft sign函数是另一种非线性,可被视为tanh的替代品,因为它也不像hard tanh函数那样容易饱和:
导数表示为:
其中sgn是signum函数,根据z的符号返回±1
图 12:Soft Sign非线性的响应
ReLU:ReLU(Rectified Linear Unit)函数是一种流行的激活选择,因为即使对于较大的 z 值它也不会饱和,并且在计算机视觉应用中取得了很大成功:
rect(z) = max(z, 0)
导数就是分段函数:
图 13:ReLU 非线性的响应
Leaky ReLU:传统的 ReLU 单元在设计上不会为非正 z 传播任何误差——leaky ReLU 对此进行了修改,使得即使 z 为负数也允许小误差向后传播:
leaky(z) = max(z, k · z)
where 0 < k < 1
这样,导数可以表示为:
图 14:Leaky ReLU 非线性的响应
2.5 数据预处理
与机器学习模型的通常情况一样,确保模型在手头的任务中获得合理性能的关键步骤是对数据执行基本预处理。下面概述了一些常用技术。
平均减法(Mean Subtraction)
给定一组输入数据 X,习惯上通过从 X 中减去 X 的平均特征向量来对数据进行零中心化。重要的一点是,在实践中,仅在整个训练集上计算均值,这个均值是 从训练、验证和测试集中减去。
归一化(Normalization)
另一种常用的技术(虽然可能不如平均减法)是缩放每个输入特征维度以具有相似的幅度范围。 这很有用,因为输入特征通常以不同的“单位”衡量,但我们通常希望最初将所有特征视为同等重要。 我们实现这一点的方法是简单地将特征除以它们各自在训练集中计算的标准差。
白化(Whitening)
不像均值减法 + 归一化那样常用,白化本质上将数据转换为具有恒等协方差矩阵 - 也就是说,特征变得不相关并且方差为 1。这是通过首先对数据进行均值减法来完成的,像往常一样 , 得到 X'。 然后我们可以对 X' 进行奇异值分解 (SVD) 得到矩阵 U、S、V。然后我们计算 UX' 以将 X' 投影到由 U 的列定义的基中。我们通过 S 中相应的奇异值来适当地缩放我们的数据最后划分结果的每个维度(如果奇异值为零,我们可以只除以一个小数)。
2.6 参数初始化
使用神经网络实现最佳性能的关键步骤是以合理的方式初始化参数。一个好的开始策略是将权重初始化为正态分布在0附近的小随机数——实际上,这通常是可以接受的。然而,在《理解训练深度前馈神经网络(2010)的差异》一文中,Xavier等人研究了不同权重和偏差初始化方案对训练动态的影响。经验结果表明,对于sigmoid和tanh激活单元,当矩阵W的权重以均匀分布随机初始化时,收敛速度更快,错误率更低,如下所示:
其中是W(fan-in)的输入单元数,是W(fan-out)的输出单元数。在该参数初始化方案中,偏置单位被初始化为0。这种方法试图保持激活方差以及跨层的反向传播梯度方差。如果没有这样的初始化,梯度方差(作为信息的代理)通常会随着层间的反向传播而减小。
2.7 学习策略
训练期间模型参数更新的速率/幅度可以使用学习率来控制。 在以下简单的梯度下降公式中,α 是学习率:
您可能会认为,对于快速收敛速度,我们应该将 α 设置为较大的值——但是较大的收敛速度并不能保证更快的收敛。 事实上,在学习率非常大的情况下,我们可能会遇到损失函数实际上发散的情况,因为参数更新导致模型超出凸极小值,如图 15 所示。在非凸模型(我们使用的大多数模型)中, 大学习率的结果是不可预测的,但是损失函数发散的可能性非常高。
图 15:这里我们看到用大的学习率更新参数 会导致误差的发散。
避免发散损失的简单解决方案是使用非常小的学习率,以便我们仔细扫描参数空间——当然,如果我们使用太小的学习率,我们可能无法在合理的时间内收敛,或者可能陷入局部最小值。 因此,与任何其他超参数一样,必须有效地调整学习率。
由于训练是深度学习系统中最昂贵的阶段,一些研究试图改进这种设置学习学习率的简单方法。 例如,Ronan Collobert 通过扇入神经元 () 的平方根的倒数来缩放权重 (其中) 的学习率。
还有其他几种技术也被证明是有效的——其中一种方法是退火,在几次迭代之后,学习率会以某种方式降低——这种方法确保我们从高学习率开始并接近 快速最小化; 当我们接近最小值时,我们开始降低学习率,以便我们可以在更细粒度的范围内找到最优值。 执行退火的一种常见方法是在每 n 次学习迭代后将学习率 α 降低 x 倍。 指数衰减也很常见,其中,迭代 t 处的学习率 α 由 给出,其中 α0 是初始学习率,k 是超参数。 另一种方法是让学习率随着时间的推移而降低,如下:
在上述方案中,α0 是一个可调参数,表示起始学习率。 τ 也是一个可调参数,表示学习率应该开始降低的时间。 在实践中,这种方法被发现效果很好。 在下一节中,我们将讨论另一种不需要手动设置学习率的自适应梯度下降方法。
2.8 动量更新(Momentum Updates)
动量方法是梯度下降的一种变体,受物理学中动力学和运动研究的启发,试图使用更新的“速度”作为更有效的更新方案。 动量更新的伪代码如下所示:
Snippet 2.2
# Computes a standard momentum update
# on parameters x
v = mu*v - alpha*grad_x
x += v
2.9 自适应优化方法
AdaGrad是标准随机梯度下降(SGD)的一种实现,它有一个关键区别:每个参数的学习率都可能不同。每个参数的学习速率取决于该参数的梯度更新历史,其方式是使用更大的学习速率更快地更新具有稀少更新历史的参数。换句话说,过去没有太多更新的参数现在更有可能有更高的学习率。看公式:
在这种技术中,我们看到如果梯度历史的 RMS 极低,则学习率非常高。 这种技术的一个简单实现是:
Snippet 2.3
# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / np.sqrt(cache + 1e-8)
其他常见的自适应方法是 RMSProp 和 Adam,它们的更新规则如下所示(由 Andrej Karpathy 提供):
Snippet 2.4
# Update rule for RMS prop
cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)
Snippet 2.5
# Update rule for Adam
m = beta1*m + (1-beta1)*dx
v = beta2*v + (1-beta2)*(dx**2)
x += - learning_rate * m / (np.sqrt(v) + eps)
RMSProp 是 AdaGrad 的一种变体,它利用平方梯度的移动平均值——特别是,与 AdaGrad 不同的是,它的更新不会变得单调变小。 Adam 更新规则又是 RMSProp 的变体,但增加了类似动量的更新。 我们推荐读者去寻找这些方法的各自来源,以更详细地分析并掌握。
<未完,待续,下一篇《Dependency Parsing》>