符号说明#
$$
\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