Download Source Code
Forward Pass
- Hidden Layer得到上一层的输出a’作为本层的输入,或Input Layer得到整个网络的输入。
- 当前层得到输入后首先经过线性计算(如Convolution Layer的卷积运算或Fully Connected Layer),得到Z。
- 线性运算的结果Z经过Activation Function(激活函数如Sigmoid、ReLU、tanh等),得到a。
- 激活函数的结果a最终作为本层输出并作为下一层的输入。
Backward Pass
总目标:求得Loss函数对本层Unknown Parameter的梯度(所有未知参数偏导数组成的向量), ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L、 ∂ L ∂ b \frac{\partial L}{\partial b} ∂b∂L,然后对参数更新。如上图所示,W和b在线性运算中。我们首先想到,可以像做高数题那样,先通过网络计算出一个以W和b为未知数的Loss函数,然后分别对每一个Unknown Parameter求偏导,但这不是计算思维,带有大量未知参数的表达式计算机无法存储也难以计算偏导,于是出现了Backpropagation,它包括Forward Pass(先)和Backward Pass(后),那我们下面来看为求 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L、 ∂ L ∂ b \frac{\partial L}{\partial b} ∂b∂L,Backward Pass都做了哪些工作。
-
对于任意一层,我们首先假设, ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L是已知的。为什么可以作这样的假设?如果我们是在Output Layer,a就是整个网络的输出y,而y和Label可以得到Loss函数的表达式,用该表达式对y求偏导,再代入Forward Pass中求得的y和已知的label,就可以求得最后一层Loss对a的偏导,Backward Pass也正是从这里开始的。我们会逐步得到任意一层的 ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L, ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L也叫作Backward Error。
-
有了 ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L,我们再求当前层的 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L,根据链式法则,显然我们需要 ∂ a ∂ Z \frac{\partial a}{\partial Z} ∂Z∂a,这实际上就是Activation Function的导数,值得注意的是,求 ∂ a ∂ Z \frac{\partial a}{\partial Z} ∂Z∂a需要Forward Pass中求得的当前层的Z的值。 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L也叫做Layer Error或Delta。
[ ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L= ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L * ∂ a ∂ Z \frac{\partial a}{\partial Z} ∂Z∂a]
-
有了 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L,离我们的目标 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L、 ∂ L ∂ b \frac{\partial L}{\partial b} ∂b∂L更近了,显然我们需要 ∂ Z ∂ W \frac{\partial Z}{\partial W} ∂W∂Z和 ∂ Z ∂ b \frac{\partial Z}{\partial b} ∂b∂Z。我们先忽略 ∂ Z ∂ b \frac{\partial Z}{\partial b} ∂b∂Z的计算,关注 ∂ Z ∂ W \frac{\partial Z}{\partial W} ∂W∂Z。在全连接层中,Z = W * a;在卷积层中,因为Receptive Field和Parameter Sharing,W的某些部分与a的某些部分相乘得到Z,总之,W和a通过简单的乘加运算得到Z,因此 ∂ Z ∂ W \frac{\partial Z}{\partial W} ∂W∂Z的结果就来自a。有了 ∂ Z ∂ W \frac{\partial Z}{\partial W} ∂W∂Z,我们就可以更新W,W‘ = W - Learning Rate * ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L。 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L、 ∂ L ∂ b \frac{\partial L}{\partial b} ∂b∂L也叫dW、db。
[ ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L= ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L * ∂ Z ∂ W \frac{\partial Z}{\partial W} ∂W∂Z]
-
既然得到了本层的 ∂ Z ∂ W \frac{\partial Z}{\partial W} ∂W∂Z,那Backward Pass是否可以结束了?显然不可以,如果结束了,那该层的上一层怎么办呢?于是我们希望再次得到 ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L,上一层再继续重复1,2,3步骤,因为2中我们得到了 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L,显然我们需要 ∂ Z ∂ a ′ \frac{\partial Z}{\partial a'} ∂a′∂Z,与3类似, ∂ Z ∂ a ′ \frac{\partial Z}{\partial a'} ∂a′∂Z就来自于W, ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L也叫作Backward Error,至此我们可以对上一层继续Backward Pass。
[ ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L= ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L * ∂ Z ∂ a ′ \frac{\partial Z}{\partial a'} ∂a′∂Z]
值得注意的是:
- Backward Pass过程中,需要Forward Pass中计算的Z和a,使用Z计算 ∂ a ∂ Z \frac{\partial a}{\partial Z} ∂Z∂a进而[ ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L= ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L * ∂ a ∂ Z \frac{\partial a}{\partial Z} ∂Z∂a]得到Layer Error(Delta),使用上一层的a计算 ∂ Z ∂ a ′ \frac{\partial Z}{\partial a'} ∂a′∂Z进而[ ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L= ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L * ∂ Z ∂ a ′ \frac{\partial Z}{\partial a'} ∂a′∂Z]得到Backward Error。因此Z和a需要在Forward Pass中保存下来。
- 首先有本层的Backward Error,然后结合本层的Z和Activation Funtion获得本层的Layer Error,使用上一层的a和本层的Layer Error进行update,使用本层的Unknown Parameter和本层的Layer Error获得上一层的Backward Error(Maxpooling特殊)。
- 无论是怎样的Layer,在当前层的Backward Error已知的情况下,求得Layer Error的过程都是根据激活函数,都是类似的[ ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L= ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L * ∂ a ∂ Z \frac{\partial a}{\partial Z} ∂Z∂a]。
- 但不同的Layer得到 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L和 ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L是有区别的,原因是 ∂ Z ∂ W \frac{\partial Z}{\partial W} ∂W∂Z和 ∂ Z ∂ a ′ \frac{\partial Z}{\partial a'} ∂a′∂Z得到的方式不同,下面开始逐个介绍。
Convolution Layer Backward Pass
在卷积层中,上一层的输入Input a’是一个矩阵X(我们以单通道即1 channel为例),待更新的参数W叫做卷积核(Kernel或Filter,假设只有一个卷积核,卷积核的厚度应当等于输入通道数 = 1),输出是一个单通道矩阵O,我们通过上述的1,2操作,可以得到
∂
L
∂
Z
\frac{\partial L}{\partial Z}
∂Z∂L,也就是
∂
L
∂
O
\frac{\partial L}{\partial O}
∂O∂L。有下面公式:
∂
L
∂
F
=
C
o
n
v
o
l
u
t
i
o
n
(
I
n
p
u
t
X
,
L
a
y
e
r
E
r
r
o
r
∂
L
∂
O
)
\frac{\partial L}{\partial F} = Convolution(Input\ X,\ Layer\ Error\ \frac{\partial L}{\partial O})
∂F∂L=Convolution(Input X, Layer Error ∂O∂L)
∂ L ∂ X = F u l l C o n v o l u t i o n ( 180 ° r o t a t e d F , L a y e r E r r o r ∂ L ∂ O ) \frac{\partial L}{\partial X} = Full\ Convolution(180°\ rotated\ F,\ Layer\ Error\ \frac{\partial L}{\partial O}) ∂X∂L=Full Convolution(180° rotated F, Layer Error ∂O∂L)
具体可见:How does Backpropagation work in a CNN?
在我的代码中并没有在卷积层的反向传播过程中使用卷积操作,因为没能准确地把握多通道,多核的情况,网上的例子大都是单通道和单核的,后续捋清楚了再更新代码。
这份代码中用的多层循环,因为都是加乘操作,O对X的偏导都来自Filter,O对Filter的偏导都来自X,通过循环找到待求导变量的系数累加,效率较低。
Pooling Layer Backward Pass
- 池化层没有Activation Funtion,因此Z = a, ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L = ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L,即Backward Error = Layer Error。
- 池化层没有Unknown Parameters,因此不需要求 ∂ L ∂ W \frac{\partial L}{\partial W} ∂W∂L。
- 但池化层必须完成 ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L,求出上一层的Backward Error进行反向传播。
- 对于最大池化,得到 ∂ L ∂ Z \frac{\partial L}{\partial Z} ∂Z∂L 即 ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L后,求 ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L,a’的尺寸大于a,因此 ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L的尺寸要大于 ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L , ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L中,在a’被采样的位置的导数值与 ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L相等,其余位置为0。
- 对于平均池化,将 ∂ L ∂ a \frac{\partial L}{\partial a} ∂a∂L的值平均到池化窗口中再填回到 ∂ L ∂ a ′ \frac{\partial L}{\partial a'} ∂a′∂L中即可。
具体可见:序号3
Fully Connected Layer Backward Pass
About Padding in Backward Pass
- Padding出现在Convolution Layer,当然Pooling Layer也可以Padding,但我的代码中没有考虑。
- 如果Forward Pass时有Padding,在求上一层的Backward Error的时候,求得的矩阵中是包含对Padding的Zero求导的,要先把这些Zero的偏导数求出来然后再去掉送给上一层。
- 如果Forward Pass时有Padding,在求dW(即对Filter求偏导)时,要把a’(即X)先Padding再与本层的Layer Error作相关操作。
the Neural Network Structure for CIFAR10
代码中在Convolution Layer和Fully Connected Layer后都加了一个Activation Funtion,卷积层加了ReLU,全连接层加了Sigmoid。
Process CIFAR10 Dataset For C++
CIFAR10的数据集文件是二进制文件,C++直接读取比较麻烦,我先用Pytoch的DataLoader将图片转化为Tensor直接存到文本文件中,C++只需要从文本文件中读RGB对应的数值即可。相关程序在CIFAR10_for_C++.py中。
Code structure
- Array2d用于全连接层,具有行、列属性,用一个vector存储全部值,将二维索引映射到一维;其中实现了向量的一些基本操作,如行点积,列点积。
- Array3d用于卷积层和池化层,具有宽、高、通道属性,用一个vector存储全部值,将三维索引映射到一维;其中实现了一些矩阵基本操作,加乘等。
- 三种层,Convolution Layer、Fully Connected Layer、Maxpooling Layer,每一层Compute函数和activate函数进行Forward Pass,通过gradient_L_to_Z函数来获取Layer Error,通过Backward函数计算上一层的Backward Error,通过Update函数更新Unknown Parameter(除了Maxpooling)。
- ReLU和Sigmoid不单独作为层,分别内置在Convolution Layer和Fully Connected Layer中。
- MSE和Cross Entropy是两个Loss Funtion,其中的start_backward函数计算Output Layer的Backward Error,由此开始反向传播过程。
- CNN负责将main声明的网络模型中的层衔接起来。train函数负责在训练集上训练,包括了正向和反向传播;Predict函数和test_accuracy函数共同在测试集上计算正确率,仅有正向传播过程。
- db_handler处理数据集,读取CIFAR10_for_C++.py处理后的文件。