Download Source Code
CNN for CIFAR10 in C++
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处理后的文件。
|