上一节课我们得到:
∂ s ∂ W = δ ∂ z ∂ W \frac { \partial s } { \partial \boldsymbol { W } } = \boldsymbol { \delta }\frac{\partial z}{\partial W} ∂W∂s=δ∂W∂z
假设 z z z长度为n, x x x长度为m,那么我们现在来看看 ∂ z ∂ W \frac{\partial z}{\partial W} ∂W∂z, 由于W是一个n × m的矩阵, 我们单独看一个元素 W i j W_{ij} Wij。由于 W i j W_ij Wij只对 z i z_i zi有影响,因此:
∂ z i ∂ W i j = ∂ ∂ W i j ∑ k = 1 m W i k x k = x j ( 即 j = k 时 的 系 数 ) \frac{\partial z_i}{\partial W_{ij}} = \frac{\partial }{\partial W_{ij}} \sum_{k = 1}^{m}W_{ik}x_k = x_j (即j=k时的系数) ∂Wij∂zi=∂Wij∂k=1∑mWikxk=xj(即j=k时的系数)
所以有:
∂ z i ∂ W = x T \frac{\partial z_i}{\partial W} = x^T ∂W∂zi=xT
那么 ∂ z ∂ W \frac{\partial z}{\partial W} ∂W∂z 就应该是一个n × m的矩阵, 而 ∂ s ∂ W i j = δ i x j \frac{\partial s}{\partial W_{ij}} = \delta_ix_j ∂Wij∂s=δixj , 所以有:
∂ s ∂ W = δ x T [ n × m ] [ n × 1 ] [ 1 × m ] \begin{aligned} & \frac { \partial s } { \partial \boldsymbol { W } } = &\boldsymbol { \delta }&\boldsymbol { x } ^ { T } \\ &[ n \times m ] &[ n \times 1 ]& [ 1 \times m ] \end{aligned} ∂W∂s=[n×m]δ[n×1]xT[1×m]
对于x的偏导,输入阶段我们将窗口内的单词拼接成了一个向量,对于反向传播的误差,也可以直接拆分到各个单词向量进行更新。
∇ x J = W T δ \nabla_xJ = W^T\delta ∇xJ=WTδ
如果一个单词在同一个窗口中出现了两次,那么我们就对这个单词进行两次更新。
假如预训练词向量中有电视、电影、节目三个词义相近,词向量相似的词语,但训练集只有电视、电影两个词出现,而测试集中出现了节目这个词。那么如果我们在训练阶段更新了词向量,就会导致三个词向量之间相似度降低。
解决方案: 如果下游任务数据集较小,就不要更新词向量。如果下游任务数据集很大,那么可以在训练阶段更新词向量(相当于fine-tune)
这之前我们要了解图计算(graph computing) 和 **计算图(computation graph)**是两个不同的概念。本节课的计算图是用图的方式来表示计算流程的神经网络计算框架,而图计算是利用图论方法研究任意数据的内在联系的一类算法统称。
注意这里的max函数,需要跟情况讨论输入参数的梯度,并根据运行反向传播的时刻的输入值来决定使用哪一个梯度(相当于激活的神经元或抑制的神经元)。下图中蓝色部分为反向传播的梯度值,在更新时需要与学习率 α \alpha α相乘。
如果一个节点输出被下一层的多个节点使用,那么反向更新时这个节点就会接受到两个误差信号(如图),我们只需要将这两个输入信号相加(事实上就是多元复合函数求偏导的公式呀)。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200223122953606.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2dlZWtfaGNo,size_16,color_FFFFFF,t_70) ## 2.4 反向传播运算规律 观察2.2节中的计算过程,可以得出以下规律: + +运算将上游梯度分发复制到下游每个分支 + max运算将上游梯度路由到下游中最大值的那一个分支 + *运算将下游的值交换后作为梯度发送给下游分支。但这只是针对上游分支只有两个的情况。如果有多个呢?(当然就是一个分支的梯度等于其他分支的前向传播值的乘积)
对于如下计算图,如果我们直接计算 ∂ s ∂ b \frac{\partial s}{\partial \boldsymbol{b}} ∂b∂s,这是极其低效的,因为这样的话我们计算 ∂ s ∂ W \frac{\partial s}{\partial \boldsymbol{W}} ∂W∂s时又会重复计算上游的梯度。
高效的计算方式是Top-Down,从loss反向一层层的传播,每一次计算都通过链式法则,利用上一层的梯度信息。
对于以下计算图,有了前面的知识,我们可以轻易地进行Fprop和Bprop,如果方法正确,Fprop和Bprop的计算复杂度应该相同。根据2.3节:
∂ z ∂ x = ∑ i = 1 n ∂ z ∂ y i ∂ y i ∂ x \frac{\partial z}{\partial \boldsymbol x}=\sum_{i=1}^{n} \frac{\partial z}{\partial y_{i}} \frac{\partial y_{i}}{\partial x} ∂x∂z=i=1∑n∂yi∂z∂x∂yi
对于一些规范的图(大多数神经网络),我们都可以利用矩阵和雅克比来简化计算
对于每个节点,如果满足下面的条件,那么由这些节点构成的网络就能够自动求导,反向传播。
+ 定义节点的输入与输出
+ 定义该节点输出对每个输入的偏导
现代神经网络框架正式利用这个原理,通过程序员定义每一层/节点的手写梯度计算,来实现整个深度神经网络的自动求导。求偏导公式是一个符号计算过程,tf, torch等框架只提供数值计算,所以需要程序员给出所有可能的计算公式(前向、反向)
这是pytorch框架计算图的定义,可以很清晰地看到前向传播、反向传播的逻辑:
对于单个单元,这里以简单的乘法运算为例,实现代码如下,这里要注意forward和backword的接口参数
(注意区分梯度的数值计算和符号计算)
根据偏导的定义,我们可以通过以下公式计算梯度的近似值,但由于计算中h(1e-4)不是无穷小的数,所以并不是真实的梯度值。
f ′ ( x ) ≈ f ( x + h ) − f ( x − h ) 2 h f ^ { \prime } ( x ) \approx \frac { f ( x + h ) - f ( x - h ) } { 2 h } f′(x)≈2hf(x+h)−f(x−h)
理论上我们可以用这个方法实现完全自动的求导,但我们可以看到这个方法要求每次计算梯度时都要重新计算f(x+h)、f(x-h),并且每次更新对每个参数都要计算一次,性能较低。通常使用这个方法来检查我们的代码实现。
当网络参数量很大的时候,为了防止过拟合,我们需要在一般的loss后面添加一个L2正则项来限制参数。(通过惩罚偏离0较远的参数值,限制模型的拟合能力)
J ( θ ) = 1 N ∑ i = 1 N − log ( e f y i ∑ c = 1 C e f c ) + λ ∑ k θ k 2 J ( \theta ) = \frac { 1 } { N } \sum _ { i = 1 } ^ { N } - \log \left( \frac { e ^ { f _ { y _ { i } } } } { \sum _ { c = 1 } ^ { C } e ^ { f _ { c } } } \right) + \lambda \sum _ { k } \theta _ { k } ^ { 2 } J(θ)=N1i=1∑N−log(∑c=1Cefcefyi)+λk∑θk2
向量化是指,尽可能的将循环计算转换为更高维的向量计算,因为计算框架对向量运算有优化,比一般的循环快很多。比如下面两行,计算输入和输出一样,但时间却相差了40多倍,如果使用GPU, 差距会远大得多。
早起使用的非线性激活函数如下,其中tanh(z) = 2*sigmod(z) - 1, sigmod 和 tanh现在仍然会在某些特殊情况下被用到,但不再是默认的激活函数:
通常需要将权重初始化为较小的值
一般来说,简单地SGD就会很有效。但一些复杂的网络可能需要适应性更强的优化器,这些优化器可能通过给每个变量设置不同的学习率并记录,来提高其优化效率(Adagrad、RMSprp、Adam(推荐作为默认使用)、sparseAdam)
一般默认起始学习率0.001, 可以以10倍减小,太大会导致不收敛,太小则优化过程很慢。
最好允许学习率在学习过程中逐渐减小,比如每k个epoch减半或使用如下公式:
l r = l r 0 e − k t ( t 为 e p o c h ) lr = lr_0e^{-kt}(t为epoch) lr=lr0e−kt(t为epoch)