跳转至

17.3 卷积的反向传播原理

17.3 卷积层的训练⚓︎

同全连接层一样,卷积层的训练也需要从上一层回传的误差矩阵,然后计算:

  1. 本层的权重矩阵的误差项
  2. 本层的需要回传到下一层的误差矩阵

在下面的描述中,我们假设已经得到了从上一层回传的误差矩阵,并且已经经过了激活函数的反向传导。

17.3.1 计算反向传播的梯度矩阵⚓︎

正向公式:

(0)Z=WA+b

其中,W是卷积核,*表示卷积(互相关)计算,A为当前层的输入项,b是偏移(未在图中画出),Z为当前层的输出项,但尚未经过激活函数处理。

我们举一个具体的例子便于分析。图17-21是正向计算过程。

图17-21 卷积正向运算

分解到每一项就是下列公式:

Multiple \tag

求损失函数Ja11的梯度:

(5)Ja11=Jz11z11a11=δz11w11

上式中,δz11是从网络后端回传到本层的z11单元的梯度。

Ja12的梯度时,先看正向公式,发现a12z11z12都有贡献,因此需要二者的偏导数相加:

(6)Ja12=Jz11z11a12+Jz12z12a12=δz11w12+δz12w11

最复杂的是求a22的梯度,因为从正向公式看,所有的输出都有a22的贡献,所以:

(7)Ja22=Jz11z11a22+Jz12z12a22+Jz21z21a22+Jz22z22a22$$$$=δz11w22+δz12w21+δz21w12+δz22w11

同理可得所有a的梯度。

观察公式7中的w的顺序,貌似是把原始的卷积核旋转了180度,再与传入误差项做卷积操作,即可得到所有元素的误差项。而公式5和公式6并不完备,是因为二者处于角落,这和卷积正向计算中的padding是相同的现象。因此,我们把传入的误差矩阵Delta-In做一个zero padding,再乘以旋转180度的卷积核,就是要传出的误差矩阵Delta-Out,如图17-22所示。

图17-22 卷积运算中的误差反向传播

最后可以统一成为一个简洁的公式:

(8)δout=δinWrot180

这个误差矩阵可以继续回传到下一层。

  • 当Weights是3×3时,δin需要padding=2,即加2圈0,才能和Weights卷积后,得到正确尺寸的δout
  • 当Weights是5×5时,δin需要padding=4,即加4圈0,才能和Weights卷积后,得到正确尺寸的δout
  • 以此类推:当Weights是N×N时,δin需要padding=N-1,即加N-1圈0

举例:

正向时stride=1:A(10×8)W(5×5)=Z(6×4)

反向时,δz(6×4)+4padding=δz(14×12)

然后:δz(14×12)Wrot180(5×5)=δa(10×8)

17.3.2 步长不为1时的梯度矩阵还原⚓︎

我们先观察一下stride=1和2时,卷积结果的差异如图17-23。

图17-23 步长为1和步长为2的卷积结果的比较

二者的差别就是中间那个结果图的灰色部分。如果反向传播时,传入的误差矩阵是stride=2时的2x2的形状,那么我们只需要把它补上一个十字,变成3x3的误差矩阵,就可以用步长为1的算法了。

以此类推,如果步长为3时,需要补一个双线的十字。所以,当知道当前的卷积层步长为S(S>1)时:

  1. 得到从上层回传的误差矩阵形状,假设为M×N
  2. 初始化一个(MS)×(NS)的零矩阵
  3. 把传入的误差矩阵的第一行值放到零矩阵第0行的0,S,2S,3S...位置
  4. 然后把误差矩阵的第二行的值放到零矩阵第S行的0,S,2S,3S...位置
  5. ......

步长为2时,用实例表示就是这样:

[δ110δ120δ1300000δ210δ220δ23]

步长为3时,用实例表示就是这样:

[δ1100δ1200δ1300000000000000δ2100δ2200δ23]

17.3.3 有多个卷积核时的梯度计算⚓︎

有多个卷积核也就意味着有多个输出通道。

也就是14.1中的升维卷积,如图17-24。

图17-24 升维卷积

正向公式:

z111=w111a11+w112a12+w121a21+w122a22$$$$z112=w111a12+w112a13+w121a22+w122a23$$$$z121=w111a21+w112a22+w121a31+w122a32$$$$z122=w111a22+w112a23+w121a32+w122a33
z211=w211a11+w212a12+w221a21+w222a22$$$$z212=w211a12+w212a13+w221a22+w222a23$$$$z221=w211a21+w212a22+w221a31+w222a32$$$$z222=w211a22+w212a23+w221a32+w222a33

Ja22的梯度:

Ja22=JZ1Z1a22+JZ2Z2a22=Jz111z111a22+Jz112z112a22+Jz121z121a22+Jz122z122a22+Jz211z211a22+Jz212z212a22+Jz221z221a22+Jz222z222a22=(δz111w122+δz112w121+δz121w112+δz122w111)+(δz211w222+δz212w221+δz221w212+δz222w211)=δz1W1rot180+δz2W2rot180

因此和公式8相似,先在δin外面加padding,然后和对应的旋转后的卷积核相乘,再把几个结果相加,就得到了需要前传的梯度矩阵:

(9)δout=mδin_mWmrot180

17.3.4 有多个输入时的梯度计算⚓︎

当输入层是多个图层时,每个图层必须对应一个卷积核,如图17-25。

图17-25 多个图层的卷积必须有一一对应的卷积核

所以有前向公式:

Multiple \tag

最复杂的情况,求Ja122的梯度:

Ja111=Jz11z11a122+Jz12z12a122+Jz21z21a122+Jz22z22a122=δz11w122+δz12w121+δz21w112+δz22w111

泛化以后得到:

(14)δout1=δinW1rot180

Ja222的梯度:

Ja211=Jz11z11a222+Jz12z12a222+Jz21z21a222+Jz22z22a222=δz11w222+δz12w221+δz21w212+δz22w211

泛化以后得到:

(15)δout2=δinW2rot180

17.3.5 权重(卷积核)梯度计算⚓︎

图17-26展示了我们已经熟悉的卷积正向运算。

图17-26 卷积正向计算

要求J对w11的梯度,从正向公式可以看到,w11对所有的z都有贡献,所以:

(9)Jw11=Jz11z11w11+Jz12z12w11+Jz21z21w11+Jz22z22w11=δz11a11+δz12a12+δz21a21+δz22a22

对W22也是一样的:

(10)Jw12=Jz11z11w12+Jz12z12w12+Jz21z21w12+Jz22z22w12=δz11a12+δz12a13+δz21a22+δz22a23

观察公式8和公式9,其实也是一个标准的卷积(互相关)操作过程,因此,可以把这个过程看成图17-27。

图17-27 卷积核的梯度计算

总结成一个公式:

(11)δw=Aδin

17.3.6 偏移的梯度计算⚓︎

根据前向计算公式1,2,3,4,可以得到:

(12)Jb=Jz11z11b+Jz12z12b+Jz21z21b+Jz22z22b=δz11+δz12+δz21+δz22

所以:

(13)δb=δin

每个卷积核W可能会有多个filter,或者叫子核,但是一个卷积核只有一个偏移,无论有多少子核。

17.3.7 计算卷积核梯度的实例说明⚓︎

下面我们会用一个简单的例子来说明卷积核的训练过程。我们先制作一张样本图片,然后使用“横边检测”算子做为卷积核对该样本进行卷积,得到对比如图17-28。

图17-28 原图和经过横边检测算子的卷积结果

左侧为原始图片(80x80的灰度图),右侧为经过3x3的卷积后的结果图片(78x78的灰度图)。由于算子是横边检测,所以只保留了原始图片中的横边。

卷积核矩阵:

w=(010020010)

现在我们转换一下问题:假设我们有一张原始图片(如左侧)和一张目标图片(如右侧),我们如何得到对应的卷积核呢?

我们在前面学习了线性拟合的解决方案,实际上这个问题是同一种性质的,只不过把直线拟合点阵的问题,变成了图像拟合图像的问题,如表17-3所示。

表17-3 直线拟合与图像拟合的比较

样本数据 标签数据 预测数据 公式 损失函数
直线拟合 样本点x 标签值y 预测直线z z=xw+b 均方差
图片拟合 原始图片x 目标图片y 预测图片z z=xw+b 均方差

直线拟合中的均方差,是计算预测值与样本点之间的距离;图片拟合中的均方差,可以直接计算两张图片对应的像素点之间的差值。

为了简化问题,我们令b=0,只求卷积核w的值,则前向公式为:

z=xw$$$$loss=12(zy)2

反向求解w的梯度公式(从公式11得到):

lossw=losszzw=x(zy)

即w的梯度为预测图片z减去目标图片y的结果,再与原始图片x做卷积,其中x为被卷积图片,z-y为卷积核。

训练部分的代码实现如下:

def train(x, w, b, y):
    output = create_zero_array(x, w)
    for i in range(10000):
        # forward
        jit_conv_2d(x, w, b, output)
        # loss
        t1 = (output - y)
        m = t1.shape[0]*t1.shape[1]
        LOSS = np.multiply(t1, t1)
        loss = np.sum(LOSS)/2/m
        print(i,loss)
        if loss < 1e-7:
            break
        # delta
        delta = output - y
        # backward
        dw = np.zeros(w.shape)
        jit_conv_2d(x, delta, b, dw)
        w = w - 0.5 * dw/m
    #end for
    return w

一共迭代10000次:

  1. 用jit_conv_2d(x,w...)做一次前向计算
  2. 计算loss值以便检测停止条件,当loss值小于1e-7时停止迭代
  3. 然后计算delta值
  4. 再用jit_conv_2d(x,delta)做一次反向计算,得到w的梯度
  5. 最后更新卷积核w的值

运行结果:

......
3458 1.0063169744079507e-07
3459 1.0031151142628902e-07
3460 9.999234418532805e-08
w_true:
 [[ 0 -1  0]
 [ 0  2  0]
 [ 0 -1  0]]
w_result:
 [[-1.86879237e-03 -9.97261724e-01 -1.01212359e-03]
 [ 2.58961697e-03  1.99494606e+00  2.74435794e-03]
 [-8.67754199e-04 -9.97404263e-01 -1.87580756e-03]]
w allclose: True
y allclose: True

当迭代到3460次的时候,loss值小于1e-7,迭代停止。比较w_true和w_result的值,两者非常接近。用numpy.allclose()方法比较真实卷积核和训练出来的卷积核的值,结果为True。比如-1.86879237e-03,接近于0;-9.97261724e-01,接近于-1。

再比较卷积结果,当然也会非常接近,误差很小,allclose结果为True。用图示方法显示卷积结果比较如图17-29。

图17-29 真实值和训练值的卷积结果区别

人眼是看不出什么差异来的。由此我们可以直观地理解到卷积核的训练过程并不复杂。

代码位置⚓︎

ch17, Level3