符号说明

$$ \begin{align*} X &: 卷积输入,\text{shape} 为[b,h,w,c_{in}]\\ W &: 卷积核,\text{shape}为[a,a,c_{in},c_{out}]\\ s &: 步长\\ f &: 卷积结果,\text{shape}为[b,(h-k)/s+1,(w-k)/s+1,c_{out}]\\ loss &: 损失函数,loss = g(f) \end{align*} $$

约定,所有张量下标从 0 开始。

卷积运算

对于结果矩阵中 f[i,j,k,l],其卷积的范围(感受野)为:

$$ X[i,js:js+a,ks:ks+a,:] $$

那么卷积运算就可以表示为:

$$ \begin{align*} f[i,j,k,l] &= \sum_{m=0}^{a-1} \sum_{n=0}^{a-1} \sum_{p=0}^{c_{in}-1}(X[i,m+js,n+ks,p]\cdot w[m,n,p,l])\\ &=\vec{x_{vec}}^T \vec{w_{vec}} \end{align*} $$

通过 im2col 技术,可以将卷积运算转换为向量内积。

损失函数对 W 的梯度

前式中,f[i,j,k,l] 对于 w[m,n,p,l] 的梯度贡献只有一项 x[i,m+js,n+ks,p]。我们需要确保 x 的索引有效,因此有如下约束条件:

$$ \begin{cases} 0\leq i < b-1\\ 0\leq m+js < h\\ 0\leq n+ks < w \\ 0\leq p <c_{in} \end{cases} $$

化简得到符合条件的 ijkl 的约束为:

$$ \begin{cases} 0\leq i < b-1\\ j<(h-m)/s\\ k<(w-n)/s \end{cases} $$

根据链式法则,有:

$$ \begin{align*} \frac{\partial loss}{\partial w[m,n,p,l]} &= \sum_{i=0}^{b-1}\sum_{j=0}^{\lfloor{(h-m)/s-1\rfloor}}\sum_{k=0}^{\lfloor{(w-n)/s-1\rfloor}} \frac{\partial loss}{\partial f[i,j,k,l]}\frac{\partial f[i,j,k,l]}{\partial w[m,n,p,l]}\\ &=\sum_{i=0}^{b-1}\sum_{j=0}^{\lfloor{(h-m)/s-1\rfloor}}\sum_{k=0}^{\lfloor{(w-n)/s-1\rfloor}} \frac{\partial loss}{\partial f[i,j,k,l]} X[i, m+js, n+ks, p] \end{align*} $$

其中 $\partial{loss} /\partial f$ 在反向传播时已经得到了,且 $\partial{loss} /(\partial {f[i,j,k,l]})$ 等于 $(\partial{loss} /\partial {f})[i,j,k,l]$,将 $\partial{loss} /\partial f$ 记为 outgrad。

观察上式,其和我们之前推导的卷积表达式非常像:后两个求和项的索引为 j,k 与结果索引无关,说明其在这两个维度上进行了卷积操作,第一个索引 l 与结果索引有关,说明这是一个向量内积。具体来,这个表达式可以视为卷积操作,卷积核为 loss 对 w 的导数,被卷积对象为 X,batch 的维度在最后一个,做内积的维度在第一个。

对比二式,卷积核为 autograd,卷积的单个感受野内部存在空洞,长宽方向上两个像素之间均隔了 s-1 个长度。这是一种空洞卷积,如下图所示,红色为卷积位置。

$$ \left[ \begin{matrix} {\color[RGB]{240, 0, 0} 1}& 2& {\color[RGB]{240, 0, 0} 3}& 4\\ 5& 6& 7& 8\\ {\color[RGB]{240, 0, 0} 9}& 10& {\color[RGB]{240, 0, 0} 11}& 12\\ 13& 14& 15& 16\\ \end{matrix} \right] $$

怎么实现这个空洞卷积呢?我们可以扩张我们的卷积核 outgrad,即在每一行没一列上都 dilate 填充 s-1 个元素,将 2×2 的的卷积核心扩展成 4×4 的卷积和,按照步长为 1 进行卷积:

$$ \left[ \begin{matrix} w_1& w_2\\ w_3& w_4\\ \end{matrix} \right] \,\,\Longrightarrow \left[ \begin{matrix} w_1& 0& w_2& 0\\ 0& 0& 0& 0\\ w_3& 0& w_4& 0\\ 0& 0& 0& 0\\ \end{matrix} \right] $$

到这里,我们的损失函数对权重的梯度表达式就可以写出来了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
X # 输入 [b, h, w, c_in]
W # 卷积核 [a, a, w_in, w_out]
outgrad # loss对输出的梯度
stride # 卷积步长

outgrad_dilated = dilate(outgrad, axis=(1, 2), stride-1) # [b, *, *, c_out]
outgrad_dilated_permuted = permute(outgrad_dilated, (1, 2, 0, 3)) # [*, *, b, cout]
X_permuted = permute(X, (3, 1, 2, 0)) # [c_in, h, w, b]
W_grad_ = conv(X_permuted, outgrad_dilated_permuted) #[c_in, h, w, c_out]
W_grad = permute(W_grad_, (1, 2, 0, 3))

对于 padding 不为 1 的情况,我们直接从 shape 来考虑。在正向过程中,可以直接假定 padding 为 0,输入为 pad 后新的输入。根据这一等价转换,conv(X_permuted, outgrad_dilated_permuted) 这一步得到中 X_permuted 是根据真实的 X 得到,而 outgrad 是等价的 X 得到的,作为卷积核的 outgrad 其偏大了 2padding,因此在卷积这一步中要指定 padding=2padding:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
X # 输入 [b, h, w, c_in]
W # 卷积核 [a, a, w_in, w_out]
outgrad # loss对输出的梯度
stride # 卷积步长
padding # 

outgrad_dilated = dilate(outgrad, axis=(1, 2), stride-1) # [b, *, *, c_out]
outgrad_dilated_permuted = permute(outgrad_dilated, (1, 2, 0, 3)) # [*, *, b, cout]
X_permuted = permute(X, (3, 1, 2, 0)) # [c_in, h, w, b]
W_grad_ = conv(X_permuted, outgrad_dilated_permuted, padding=2*padding) #[c_in, h, w, c_out]
W_grad = permute(W_grad_, (1, 2, 0, 3))

损失函数对 X 的梯度

有了上面的基础,我们来讨论 loss 对 X 的梯度。首先来讨论一点,对于 X[i,j,k,l],如果其与 w[m,n,l,p] 相乘了,那么其应该在计算卷积 f[i,(j-m)/s,(k-n)/s,p] 的结果,即:

$$ f[i,(j-m)/s,(k-n)/s,p] = \sum_{p=0}^{c_{out}-1}w[m,n,l,p]\cdot X[i,j,k,l] $$

那么 loss 对于 X[i,j,k,l] 的梯度,只有 f[i,(j-m)/s,(k-n)/s,p] 对其有贡献,且贡献为 w[m,n,l,p]。

接下来可以推导 loss 对于 X[i,j,k,l] 的表达式:

$$ \begin{align*} \frac{\partial loss}{\partial X\left[ i,j,k,l \right]} &=\sum_{m=0}^{a-1}{\sum_{n=0}^{a-1}{\sum_{p=0}^{c_{out}}{\frac{\partial loss}{\partial f[i,(j-m)/s,(k-n)/s,p]}\cdot \frac{\partial f[i,(j-m)/s,(k-n)/s,p]}{\partial X\left[ i,j,k,l \right]}}}} \\ &=\sum_{m=0}^{a-1}{\sum_{n=0}^{a-1}{\sum_{p=0}^{c_{out}}{\frac{\partial loss}{\partial f[i,(j-m)/s,(k-n)/s,p]}w\left[ m,n,l,p \right]}}} \end{align*} $$

又是似曾相识的一幕,有了上面的经验,这次分析就游刃有余得多:卷积核是 W,被卷积对象是 autograd,在 autograd 的最后一个维度上进行线性变换,将其从 c_out 映射到 c_in 上。batch 的维度是 W 的第一个维度。在长宽两个维度上,感受野内部每次的步长是 -1,也就是说卷积核第一个元素将与最后一个元素相乘。我们将卷积核 flip 一下即可。聪明的你肯定注意到了,感受野内部不是连续的,两个元素之间间隔了 s-1 个元素,因此也需要将 outgrad 使用 dilate 填充 s-1 个 0 元素。

可达鸭眉头一皱,事情没有这么简单。理论上,这个梯度的 shape 应当与 X 相等,但 outgrad 本来就比 X 小,经过卷积之后应该更小了。怎会如此?我们直接观察 j=0、k=0 的状态,代入上式,会发现我们对 outgrad 的索引为负值了。这时候就需要将 outgrad 周围填充 a-1 个元素。

到这里,我们的损失函数对输入的梯度表达式就可以写出来了:

1
2
3
4
5
6
7
8
9
X # 输入 [b, h, w, c_in]
W # 卷积核 [a, a, w_in, w_out]
outgrad # loss对输出的梯度
stride # 卷积步长

W_flipped = flip(W, axis=(0,1)) # 在前两个维度上翻转stride
W_flipped_permuted = permute(W_flipped, axis=(0,1,3,2)) # [a, a, w_out, w_in]
outgrad_dilated = dilate(outgrad, axis=(1, 2), stride-1) # dilate填充stride-1个0
W_grad = conv(outgrad_dilated, W_flipped_permuted, padding=a-1) # [b, h, w, c_in]

对于 padding 不为 1 的情况,我们一样从 shape 来考虑。conv(outgrad_dilated, W_flipped_permuted, padding=a-1) 这一句中 outgrad 偏大 2padding,W 无偏,因此 padding 数要少一倍的 padding:

1
2
3
4
5
6
7
8
9
X # 输入 [b, h, w, c_in]
W # 卷积核 [a, a, w_in, w_out]
outgrad # loss对输出的梯度
stride # 卷积步长

W_flipped = flip(W, axis=(0,1)) # 在前两个维度上翻转stride
W_flipped_permuted = permute(W_flipped, axis=(0,1,3,2)) # [a, a, w_out, w_in]
outgrad_dilated = dilate(outgrad, axis=(1, 2), stride-1) # dilate填充stride-1个0
W_grad = conv(outgrad_dilated, W_flipped_permuted, padding=a-1-padding) # [b, h, w, c_in]

参考文档

Backpropagation through a Conv Layer