参考与评述
参考书目《Deep Learning》Lan Goodfellow.
经典的深度学习框架是以计算图&梯度下降方法实现对前馈网络的有监督学习。
这里复现了前馈计算图的梯度计算实现。
一、前馈计算图实现
1. 前向与梯度计算
- 结果数组 (保存输入节点与计算节点的输出值,能够反映节点在计算方向的拓扑排序)
- 梯度数组 (保存输入节点与计算节点的梯度,能够反映节点在计算方向的拓扑排序)
- 连接图 (反映每个节点的父节点)
- 输出函数集合 (反映每个计算节点如何根据其输入得到输出)
- 梯度函数集合 (反映每个计算节点如何根据输入和它的梯度计算对其任意父节点的梯度)
即可组成一个完整前馈计算图。
我们以下图所示全连接神经网络为例构建计算图(我们将每个神经元看作一个节点)。
(1)\---/(3)\---/(5)\
X X (7)--(8)-->
(2)/---\(4)/---\(6)/
其中(1)(2)为输入点,(3)(4)(5)(6)为隐层,(7)为输出层,(8)为计算Loss的输出。这样,除了(1)(2)外其它都是计算节点。我们可以为该网络定义一个包含8个元素的数组,以及计算关系。
数组及连接图如下:
//结果数组与梯度数组,我们把输入节点放到计算节点之前
list=[1] [2] [3] [4] [5] [6] [7] [8]
grad=[1] [2] [3] [4] [5] [6] [7] [8]
//父节点集合
Par[1]=null Par[2]=null
Par[3]=1,2 Par[4]=1,2 //注意由于是前馈网络,Pa(i)=j则i>j
Par[5]=3,4 Par[6]=3,4
Par[7]=5,6 Par[8]=7
计算节点输出函数集合如下:(以下输出均为标量,输入 x x 为向量)
计算节点梯度函数集合如下:
注: dcFun(i,x,p) ,即求节点i在输入x下的输出对其父节点p的输出的偏导数。
先解释下下面所用到的一些方法。
Par[i]->get_output_array(); //返回节点i的所有父节点的输出列表,即i节点输入向量
Par[i]->get_index_array(); //返回节点i的所有父节点的索引列表
L.has_element(); //列表中还有值存在
L.get_next_element(); //返回列表下一个值
put_input_in(list,i,j); //向list的i到j索引处输入值
前馈实现:
//前馈计算
put_input_in(list,1,2);
for(i=3;i<=8;i++)
list[i]=Fun(i,Par[i]->get_output_array());
前馈传播之后,为了最小化最终输出list[8] ,即由(8)定义的损失函数输出,我们需要计算每一节点的梯度。
反馈实现:
//反馈梯度计算
for(i=1;i<8;i++) grad[i]=0; //清空梯度数组
grad[8]=1;
for(i=8;i>=3;i--) //迭代每个计算节点,累加其各个父节点梯度
{
input=Par[i]->get_output_array();
par_array=Par[i]->get_index_array();
while(par_array.has_element()) //迭代本节点的所有父节点
{
par_index=par_array.get_next_element();
grad[par_index]+=grad[i]*dcFun(i,input,par_index);
//这里dcFun(c,x,p)即求节点c在输入x下的输出对其父节点p的输出的偏导数
}
}
以计算(4)节点的输出梯度为例。我们先得到其子节点(5)和(6)的梯度。则
grad[4]=grad[5]∗(5对4输出的偏导)+grad[6]∗(6对4输出的偏导) g r a d [ 4 ] = g r a d [ 5 ] ∗ ( 5 对 4 输 出 的 偏 导 ) + g r a d [ 6 ] ∗ ( 6 对 4 输 出 的 偏 导 )
如何求5对4输出的偏导?我们由公式推导。
out5=sigmoid(out3∗w51+out4∗w52+b5) o u t 5 = s i g m o i d ( o u t 3 ∗ w 51 + o u t 4 ∗ w 52 + b 5 )
我们令a5=out3∗w51+out4∗w52+b5 我 们 令 a 5 = o u t 3 ∗ w 51 + o u t 4 ∗ w 52 + b 5
∂out5∂out4=sigmoid′(a5)∗w52 ∂ o u t 5 ∂ o u t 4 = s i g m o i d ′ ( a 5 ) ∗ w 52
这样我们可以得到grad[4]的表达式。
grad[4]=grad[5]∗(sigmoid′(a5)∗w52)+grad[6]∗(sigmoid′(a6)∗w62) g r a d [ 4 ] = g r a d [ 5 ] ∗ ( s i g m o i d ′ ( a 5 ) ∗ w 52 ) + g r a d [ 6 ] ∗ ( s i g m o i d ′ ( a 6 ) ∗ w 62 )
在反馈计算迭代到(5)(6)节点时,都会累加grad[4]这个值。
我们根据任意节点输出的梯度,以及其输入,就能调整这个节点的一些参数 。
2. 参数更新
在上一步中,我们得到了每个节点的输出,以及Loss对每个节点输出的梯度。
参数可以放在如上所示节点的内部,也可以单独作为一个节点的输出。
如果参数在节点内部:
由于A的输出对其参数W的导数只由节点A决定,因此这些操作可以在计算所有梯度后并行执行。
如果参数由单个节点定义:
X --A--->
/ //参数放在W节点输出中,这样A就只是一个计算形式,需要计算材料X与W
W //这种形式在如Tensorflow框架中出现
这种形式中,我们将W当作一个输入节点,这样,在梯度计算时我们的A将会直接算出Loss对于W输出的梯度。
二、全连接神经网络
1. MLP前向计算
一个全连接神经网络(MLP)可以当作一个整体作为计算图中的一个计算节点,它有它的依赖,输出方法,以及求父节点梯度的计算方法,权值更新方法。为了方便易用,也可以每一层当作一个计算节点。(PyTorch)
我们还可以将权值放到某个输入节点中,为了区分它和输入,把它定义成变量节点。(Tensorflow)
Require: 网络深度l
Require: Wi , i∈1,...,l(Wi的每一列表示i层某个神经元的全部权值) W i , i ∈ 1 , . . . , l ( W i 的 每 一 列 表 示 i 层 某 个 神 经 元 的 全 部 权 值 )
Require: bi , i∈1,...,l(bi表示i层各个神经元的偏置) b i , i ∈ 1 , . . . , l ( b i 表 示 i 层 各 个 神 经 元 的 偏 置 )
Require: X,程序输入 (X每一行为一个输入样本,行数为多少批样本,应用SGD)
Require: Y,目标输出 (Y每一行为一个样本对应标签,行数为多少批样本标签)
H_{0}=X
for k=1:l do
end for
E=Hl E = H l
L=Loss(E,Y)+λΩ(θ)(λΩ(θ)为正则项,用于优化学习) L = L o s s ( E , Y ) + λ Ω ( θ ) ( λ Ω ( θ ) 为 正 则 项 , 用 于 优 化 学 习 )
2. 对向量偏导的定义:
3. 线性单元梯度计算:
已知 AB=C A B = C , ∂L∂C=G ∂ L ∂ C = G ,求 ∂L∂A与∂L∂B ∂ L ∂ A 与 ∂ L ∂ B :( L L 对某个矩阵 的偏导 G G 的形式与 一模一样)