CS224N课程笔记:神经网络与反向传播
NewBeeNLP原创出品
公众号专栏作者@Ryan
知乎 | 机器学习课程笔记
CS224N课程笔记系列,持续更新中
课程主页:http://web.stanford.edu/class/cs224n/
1、Neural Networks: Foundations
在前面的讨论中认为,因为大部分数据是线性不可分的所以需要非线性分类器,不然的话线性分类器在这些数据上的表现是有限的。神经网络就是如下图所示的一类具有非线性决策分界的分类器。现在我们知道神经网络创建的决策边界,让我们看看这是如何创建的。
1.1 Neural
一个神经元是用 个输入和生成单个输出的通用的计算单元。不同的神经元根据它们不同的参数(一般认为是神经元的权值)会有不同的输出。对神经元来说一个常见的选择是“ ”,或者称为“二元逻辑回归”单元。这种神经元以 维的向量作为输入,然后计算出一个激活标量(输出) 。这种神经元也和一个 维的权值向量 和一个偏置标量 相关联。然后这个神经元的输出是:
我们也可以把上面公式中的权值和偏置项结合在一起:
这个公式可以以下图的形式可视化:
1.2 A Single Layer of Neurons
考虑如下图所示将输入 作为多个神经元的输入的情况,我们可以把上述的概念扩展到多个神经元上,如下图所示。
如果我们定义不同的神经元的权值为、偏置为和相对应的激活输出为:
....
让我们定义简化公式以便于更好地表达复杂的网络:
我们现在可以将缩放和偏差的输出写成:
激活函数 可以变为如下形式:
那么这些激活的作用是什么呢?我们可以把这些激活看作是一些特征加权组合的存在的指标。然后,我们可以使用这些激活的组合来执行分类任务。
1.3 Feed-forward Computation
到目前为止我们知道一个输入向量如何经过一层 单元的变换得到激活输出 。但是这么做的意思是什么呢?让我们考虑一个 中的命名实体识别问题作为例子:
这里我们想判断中心词 是不是以命名实体。在这样的情况中,我们不仅仅想要捕获在单词向量的窗口中的单词的出现,更是要为了分类而去了解单词之间的交互信息。例如,可能只有是第一个单词和是第二个单词的时候, 才是命名实体。这种非线性决策通常是不能把输入直接放到 函数中,而是要将输入经过如 中讨论中间层的非线性变换(例如 函数),再将这些结果放到函数中。我们因此能够使用另一个矩阵与激活输出计算得到可微归一化的得分用于分类任务:
其中是激活函数(例如函数)。「维度分析:」 如果我们使用一个4维的词向量来表示单个单词和使用一个5个词的窗口,然后输入是 。如果我们在隐藏层使用8个 单元和从激活函数中生成一个分数输出,其中,,。
1.4 Maximum Margin Objective Function
类似很多的机器学习模型,神经网络需要一个优化目标函数,一个我们想要最小化或最大化的误差。这里我们讨论一个常用的误差度量方法:最大间隔目标函数。使用这个目标函数的背后的思想是保证对“真”标签数据的计算得分要比“假”标签数据的计算得分要高。
回到前面的例子,如果我们令“真”标签窗口 的计算得分为 ,令“假”标签窗口 的计算得分为 (下标表示这个这个窗口 )。
然后,我们对目标函数最大化 或者最小化 。然而,我们修改目标函数来保证误差仅在 才进行计算。这么做的原因是我们仅关注“真”数据要比“假”数据的分数要高,才能满足最大间隔。因此,我们想要如果 误差为,否则为0 。因此,我们的优化的目标函数现在为:
然而,上面的优化目标函数是有风险的,因为它不能创造一个安全的间隔。我们希望“真”数据要比“假”数据的得分大于某个正的间隔。换而言之,我们想要误差在 就开始计算,而不是当时才计算。因此,我们修改优化目标函数为:
我们可以把这个间隔缩放使得,让其他参数在优化过程中自动进行调整,并且不会影响模型的表现。如果想更多地了解这方面,可以去读一下中的函数间隔和几何间隔中的相关内容。最后,我们定义在所有训练窗口上的优化目标函数为:
按照上面的公式有, 和 。
在这部分我们讨论损失函数为正时,模型中不同参数时如何训练的。如果损失为 0时,那么不需要再更新参数。我们一般使用梯度下降(或者像这样的变体)来更新参数,所以要知道在更新公式中需要的任意参数的梯度信息:
反向传播是一种利用微分链式法则来计算模型上任意参数的损失梯度的方法。为了更进一步理解反向传播,我们先看下图中的一个简单的网络
这里我们使用只有单个隐藏层和单个输出单元的神经网络。现在让我们先建立一些符号定义:
是神经网络的输入。 是神经网络的输出。 每层(包括输入和输出层)的神经元都接收一个输入和生成一个输出。第层的第个神经元接收标量输入和生成一个标量激活输出 。 我们把反向传播误差在的计算定义为。 第1层认为是输入层而不是第1个隐藏层。对输入层,。 是将第k层的输出映射到第k+1层的输入的转移矩阵,因此将这个新的符号用在中的例子和。
现在我们开始反向传播:假设损失函数为正值,我们想更新参数,我们看到只参与了 和的计算。这点对于理解反向传播是非常重要的-参数的反向传播梯度只和参与了前向计算中的参数的值有关系,在随后的前向计算中和相乘计算得分。我们可以从最大间隔损失看到:
为了简化我们只分析。所以,
我们可以看到梯度计算最后可以简化为 ,其中本质上是第2层中第个神经元反向传播的误差。与相乘的结果,输入第2层中第个神经元中。
我们以下图为例,让我们从“误差共享/分配”的来阐释一下反向传播,现在我们要更新:
如果要控制列表的层级,则需要在符号-
前使用空格。如下:
我们从 的1的误差信号开始反向传播。 然后我们把误差与把映射到 的神经元的局部梯度相乘。在这个例子中梯度正好等于1 ,则误差仍然为1。所以有 。 这里误差信号1已经到达。我们现在需要分配误差信号使得误差的“误差共享”到达。 现在在 的误差为(在的误差信号为)。因此在 的误差为 。 与第2步的做法相同,我们把在 的误差 映射到 的神经元的局部梯度相乘。在这里局部梯度为 。 因此在的误差是,我们将其定义为。 最后,我们通过将上面的误差与参与前向计算的相乘,把误差的“误差共享”分配到。 所以,对于的梯度损失可以计算为。注意我们使用这个方法得到的结果是和之前微分的方法的结果是完全一样的。因此,计算网络中的相应参数的梯度误差既可以使用链式法则也可以使用误差共享和分配的方法-这两个方法能得到相同结果。
「偏置更新」 偏置项和其他权值在数学形式是等价的,只是在计算下一层神经元输入时相乘的值是常量1。因此在第层的第个神经元的偏置的梯度时。例如在上面的例子中,我们更新的是而不是,那么这个梯度为。
「从到从反向传播的一般步骤:」
我们有从向后传播的误差,如下图所示
我们通过把与路径上的权值相乘,将这个误差反向传播到。
因此在接收的误差是。
然而,在前向计算可能出下图的情况,会参与下一层中的多个神经元的计算。那么第层的第个神经元的误差也要使用上一步方法将误差反向传播到 上。
因此现在在接收的误差是。
实际上,我们可以把上面误差和简化为。
现在我们有在正确的误差,然后将其与局部梯度相乘,把误差信息反向传到第层的第个神经元上。
因此到达的误差为。
1.6 Training with Backpropagation – Vectorized
到目前为止,我们讨论了对模型中的给定参数计算梯度的方法。这里会一般泛化上面的方法,让我们可以直接一次过更新权值矩阵和偏置向量。注意这只是对上面模型的简单地扩展,这将有助于更好理解在矩阵-向量级别上进行误差反向传播的方法。
对更定的参数,我们知道它的误差梯度是。其中是将映射到的矩阵。因此我们可以确定整个矩阵的梯度误差为:
因此我们可以将整个矩阵形式的梯度写为在矩阵的中反向传播的误差向量和前向激活输出的外积。
现在我们来看看如何能够计算误差向量。我们从上面的例子中有,。这可以简单地改写为矩阵的形式:
在上面的公式中运算符是表示向量之间对应元素的相乘()。
「计算效率:」 在探索了 element-wise 的更新和 vector-wise 的更新之后,必须认识到在科学计算环境中,如 MATLAB 或 Python(使用 Numpy / Scipy 库),向量化运算的计算效率是非常高的。因此在实际中应该使用向量化运算。此外,我们也要减少反向传播中的多余的计算-例如,注意到是直接依赖在上。所以我们要保证使用更新时,要保存用于后面的计算-然后计算层的时候重复上述的步骤。这样的递归过程是使得反向传播成为计算上可负担的过程。
2、 Neural Networks: Tips and Tricks
2.1 Gradient Check
在上一部分中,我们详细地讨论了如何用基于微积分的方法计算神经网络中的参数的误差梯度/更新。这里我们介绍一种用数值近似这些梯度的方法-虽然在计算上的低效不能直接用于训练神经网络,这种方法可以非常准确地估计任何参数的导数;因此,它可以作为对导数的正确性的有用的检查。给定一个模型的参数向量和损失函数,围绕的数值梯度由 得出:
其中是一个很小的值(一般约为 )。当我们使用扰动参数的第个元素时,就可以在前向传播上计算误差。相似地,当我们使用扰动参数的第个元素时,就可以在前向传播上计算误差。因此,计算两次前向传播,我们可以估计在模型中任意给定参数的梯度。我们注意到数值梯度的定义和导数的定义很相似,其中,在标量的情况下:
当然,还是有一点不同-上面的定义仅仅在正向扰动计算梯度。虽然是可以用这种方式定义数值梯度,但在实际中使用 常常可以更准确和更稳定,因为我们在两个方向都对参数扰动。为了更好地逼近一个点附近的导数/斜率,我们需要在该点的左边和右边检查函数的行为。也可以使用泰勒定理来表示 有比例误差,这相当小,而导数定义更容易出错。
现在你可能会产生疑问,如果这个方法这么准确,为什么我们不用它而不是用反向传播来计算神经网络的梯度?这是因为效率的问题-每当我们想计算一个元素的梯度,需要在网络中做两次前向传播,这样是很耗费计算资源的。再者,很多大规模的神经网络含有几百万的参数,对每个参数都计算两次明显不是一个好的选择。同时在例如SGD这样的优化技术中,我们需要通过数千次的迭代来计算梯度,使用这样的方法很快会变得难以应付。这种低效性是我们只使用梯度检验来验证我们的分析梯度的正确性的原因。梯度检验的实现如下所示:
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 '''
f(x) = 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) # restore to previous value (very important!) x[ix] = old_value
# compute the partial derivative # the slope grad[ix] = (fxh_left - fxh_right) / (2 * h) it.iternext() # step to next dimension return grad
2.2 Regularization
和很多机器学习的模型一样,神经网络很容易过拟合,这令到模型在训练集上能获得近乎完美的表现,但是却不能泛化到测试集上。一个常见的用于解决过拟合(“高方差问题”)的方法是使用正则化。我们只需要在损失函数上增加一个正则项,现在的损失函数如下:
在上面的公式中,是矩阵在神经网络中的第个权值矩阵)的范数和是超参数控制损失函数中的权值的大小。当我们尝试去最小化,正则化本质上就是当优化损失函数的时候,惩罚数值太大的权值(让权值的数值分配更加均衡,防止出现部分权值特别大的情况)。
由于范数的二次的性质(计算矩阵的元素的平方和),正则项有效地降低了模型的灵活性和因此减少出现过拟合的可能性。增加这样一个约束可以使用贝叶斯派的思想解释,这个正则项是对模型的参数加上一个先验分布,优化权值使其接近于0-有多接近是取决于的值。
选择一个合适的值是很重要的,并且需要通过超参数调整来选择。的值太大会令很多权值都接近于0,则模型就不能在训练集上学习到有意义的东西,经常在训练、验证和测试集上的表现都非常差。的值太小,会让模型仍旧出现过拟合的现象。需要注意的是,偏置项不会被正则化,和不会计算入损失项中-尝试去思考一下为什么(译者注:我认为偏置项在模型中仅仅是偏移的关系,使用少量的数据就能拟合到这项,而且从经验上来说,偏置值的大小对模型表现没有很显著的影响,因此不需要正则化偏置项)。
有时候我们会用到其他类型的正则项,例如正则项,它将参数元素的绝对值全部加起来-然而,在实际中很少会用正则项,因为会令权值参数变得稀疏。在下一部分,我们讨论,这是另外一种有效的正则化方法,通过在前向传播过程随机将神经元设为0(译者注:实际上是通过在每次迭代中忽略它们的权值来实现“冻结”部分。这些“冻结”的不是把它们设为0,而是对于该迭代,网络假定它们为0。“冻结”的 不会为此次迭代更新)。
2.3 Dropout
是一个非常强大的正则化技术,在论文 《Dropout: A Simple Way to Prevent Neural Networks from Overfitting》中首次提出,下图展示了如何应用在神经网络上。
这个想法是简单而有效的-在训练过程中,在每次的前向/反向传播中我们按照一定概率(1-p)随机地“”一些神经元子集(或者等价的,我们保持一定概率的神经元是激活的)。然后,在测试阶段,我们将使用全部的神经元来进行预测。使用神经网络一般能从数据中学到更多有意义的信息,更少出现过拟合和通常在现今的任务上获得更高的整体表现。这种技术应该如此有效的一个直观原因是,本质上作的是一次以指数形式训练许多较小的网络,并对其预测进行平均。
在实际中,在实践中,我们使用的方式是我们取每个神经元层的输出,并保持概率的神经元是激活的,否则将神经元设置为0。然后,在反向传播中我们仅对在前向传播中激活的神经元回传梯度。最后,在测试过程,我们使用神经网络中全部的神经元进行前向传播计算。然而,有一个关键的微妙之处,为了使有效地工作,测试阶段的神经元的预期输出应与训练阶段大致相同-否则输出的大小可能会有很大的不同,网络的表现已经不再明确了。因此,我们通常必须在测试阶段将每个神经元的输出除以某个值——这留给读者作为练习来确定这个值应该是多少,以便在训练和测试期间的预期输出相等。
2.4 Neuron Units
到目前为止,我们讨论了含有 sigmoidal neurons 的非线性分类的神经网络。但是在很多应用中,使用其他激活函数可以设计更好的神经网络。下面列出一些常见的激活函数和激活函数的梯度定义,它们可以和前面讨论过的 sigmoidal 函数互相替换。
Sigmoid:这是我们讨论过的常用选择,激活函数为:
其中。
的梯度为
「:」 函数是函数之外的另一个选择,在实际中它能更快地收敛。 和的主要不同在于的输出范围在0到1,而 的输出范围在-1到1。
其中。
的梯度为:
「:」 有时候函数有时比函数的选择更为优先,因为它的计算量更小。然而当的值大于 1时,函数的数值会饱和(如下图所示会恒等于1)。激活函数为:
这个函数的微分也可以用分段函数的形式表示:
「:」 函数是另外一种非线性激活函数,它可以是的另外一种选择,因为它hard clipped functions那样过早地饱和:
softsign
函数的微分表达式为:
其中 是符号函数,根据的符号返回1或者-1。
「:」函数是激活函数中的一个常见的选择,当的值特别大的时候它也不会饱和。在计算机视觉应用中取得了很大的成功:
函数的微分是一个分段函数:
「:」 传统的单元当的值小于0时,是不会反向传播误差,改善了这一点,当的值小于0时,仍然会有一个很小的误差反向传播回去。
其中 。
函数的微分是一个分段函数:
2.5 Data Preprocessing
与机器学习模型的情况一样,要想模型在当前任务上获得合理的表现的关键步骤是对数据进行合理的预处理。
「Mean Subtraction:」 给定一组输入数据,一般把中的值减去的平均特征向量来使数据零中心化。在实践中很重要的一点是,只计算训练集的平均值,而且这个在训练集,验证集和测试集都是减去这个平均值。
「Normalization:」 另外一个常见的技术(虽然没有Mean Subtraction常用)是将每个输入特征维度缩小,让每个输入特征维度具有相似的幅度范围。这是很有用的,因此不同的输入特征是用不同“单位”度量,但是最初的时候我们经常认为所有的特征同样重要。实现方法是将特征除以它们各自在训练集中计算的标准差。
「Whitening:」 相比上述的两个方法,Whitening没有那么常用,它本质上是数据经过转换后,特征之间相关性较低,所有特征具有相同的方差(协方差阵为1)。首先对数据进行Mean Subtraction处理,得到。然后我们对进行奇异值分解得到矩阵,, ,计算将投影到由的列定义的基上。我们最后将结果的每个维度除以中的相应奇异值,从而适当地缩放我们的数据(如果其中有奇异值为0,我们就除以一个很小的值代替)。
2.6 Parameter Initialization
让神经网络实现最佳性能的关键一步是以合理的方式初始化参数。一个好的起始方法是将权值初始化为通常分布在0附近的很小的随机数-在实践中效果还不错。在论文《Understanding the difficulty of training deep feedforward neural networks (2010)》研究不同权值和偏置初始化方案对训练动力(training dynamics)的影响。实验结果表明,对于sigmoid和tanh激活单元,当一个权值矩阵以如下的均匀分布的方式随机初始化,能够实现更快的收敛和得到更低的误差:
其中是(fan-in)的输入单元数, 是 (fan-out)的输出单元数。在这个参数初始化方案中,偏置单元是初始化为 0。这种方法是尝试保持跨层之间的激活方差以及反向传播梯度方差。如果没有这样的初始化,梯度方差(当中含有纠正信息)通常随着跨层的反向传播而衰减。
2.7 Learning Strategies
训练期间模型参数更新的速率/幅度可以使用学习率进行控制。在最简单的梯度下降公式中,是学习率:
你可能会认为如果要更快地收敛,我们应该对 取一个较大的值-然而,在更快的收敛速度下并不能保证更快的收敛。实际上,如果学习率非常高,我们可能会遇到损失函数难以收敛的情况,因为参数更新幅度过大,会导致模型越过凸优化的极小值点,如下图所示。在非凸模型中(我们很多时候遇到的模型都是非凸),高学习率的结果是难以预测的,但是损失函数难以收敛的可能性是非常高的。
避免损失函数难以收敛的一个简答的解决方法是使用一个很小的学习率,让模型谨慎地在参数空间中迭代-当然,如果我们使用了一个太小的学习率,损失函数可能不会在合理的时间内收敛,或者会困在局部最优点。因此,与任何其他超参数一样,学习率必须有效地调整。
深度学习系统中最消耗计算资源的是训练阶段,一些研究已在尝试提升设置学习率的新方法。例如, 通过取的神经元的平方根的倒数来缩放权值(其中)的学习率。
还有其他已经被证明有效的技术-这个方法叫 annealing,在多次迭代之后,学习率以以下方式降低:保证以一个高的的学习率开始训练和快速逼近最小值;当越来越接近最小值时,开始降低学习率,让我们可以在更细微的范围内找到最优值。一个常见的实现 annealing 的方法是在 [公式] 每次的迭代学习后,通过一个因子来降低学习率。指数衰减也是另一个常见的方法,在次迭代后学习旅变为,其中是初始的学习率和是超参数。还有另外一种方法是允许学习率随着时间减少:
在上述的方案中,是一个可调的参数,代表起始的学习率。也是一个可调参数,表示学习率应该在该时间点开始减少。在实际中,这个方法是很有效的。在下一部分我们讨论另外一种不需要手动设定学习率的自适应梯度下降的方法。
2.8 Momentum Updates
动量方法,灵感来自于物理学中的对动力学的研究,是梯度下降方法的一种变体,尝试使用更新的“速度”的一种更有效的更新方案。动量更新的伪代码如下所示:
# Computes a standard momentum update
# on parameters x
v = mu * v - alpha * grad_x
x += v
2.9 Adaptive Optimization Methods
是标准的随机梯度下降(SGD)的一种实现,但是有一点关键的不同:对每个参数学习率是不同的。每个参数的学习率取决于每个参数梯度更新的历史,参数的更新历史越稀少,就使用更大的学习率加快更新。换句话说,之前很少被更新的参数就用比现在更大的学习率更新。
其中。
在这个技术中,我们看到如果梯度的历史的很低,那么学习率会非常高。这个技术的一个简单的实现如下所示:
# Assume the gradient dx and parameter vector xcache += dx ** 2x += -learning_rate * dx / np.sqrt(cache + 1e-8)
其他常见的自适应方法有和,其更新规则如下所示:
# Update rule for RMS prop
cache = decay_rate * cache + (1 - decay_rate) * dx ** 2
x += -learning_rate * dx / (np.sqrt(cache) + eps)
# Update rule for Adam
m = beta * m + (1 - beta1) * dx
v = beta * v + (1 - beta2) * (dx ** 2)
x += -learning_rate * m / (np.sqrt(v) + eps)
是利用平方梯度的移动平局值,是的一个变体-实际上,和不一样,它的更新不会单调变小。更新规则又是 的一个变体,但是加上了动量更新。
2.10 More reference
如果希望了解以上的梯度优化算法的具体细节,可以阅读这篇文章:An overview of gradient descent optimization algorithms。