任务描述
本关任务:实现卷积层的反向传播。
相关知识
为了完成本关任务,你需要掌握:卷积层的反向传播。
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第5章的内容。
卷积层的反向传播
在之前的实训中,我们学习了卷积层的前向传播。我们知道,卷积层的前向传播通常先通过 im2col 操作将输入特征图转化成一个矩阵,其中矩阵的每一行对应于输入特征图在一个卷积窗口的所有通道内的数据,这样,卷积操作就被转化成了矩阵乘法。忽略掉偏置的计算(因为偏置的计算非常简单),卷积计算可以用下面的公式表示:
y^=x^×W^
这里,x^是输入x经过 im2col 计算后的结果。因为卷积层的权重W的形状通常是(C′,C,Kh,Kw),为了匹配上面矩阵乘法的形式,需要通过 reshape 和 transpose 操作将W转化为(C⋅Kh⋅Kw,C′)的形状,这里记这个矩阵为W^。得到的结果y^的形状是(B⋅H′⋅W′,C′),通过 reshape 和 transpose 操作将其变换成(B,C′,H′,W′)就是我们想要的结果了。
对于这个形式,你是否感觉有一点熟悉呢?没错,这与我们之前学习的全连接层的计算形式如出一辙,套用全连接层的反向传播公式:
∂W^∂l∂x^∂l=∂y^∂l⋅∂W^∂y^=x^×(∂y^∂l)T=∂y^∂l⋅∂x^∂y^=W^×∂y^∂l
最后,我们需要做的就是把∂l/∂x^变换为∂l/∂x,同时把把∂l/∂W^变换为∂l/∂W。对于后者,只要通过 transpose 和 reshape 操作还原为原来的形状即可。对于前者,我们需要通过 im2col 的逆运算——col2im——来降其转换为∂l/∂x。
卷积层反向传播的实现
实训拓展了在之前的实训定义的Convolution
类,实训已经给出了forward(x)
的实现,并针对反向传播的需要对其进行了一定的修改。你需要实现该类的反向传播函数backward(dout)
,dout
是损失函数相对卷积层输出的梯度,即之前公式中的∂y∂l,是一个形状为(B,C′,H′,W′)的numpy.ndarray
。实训已经提供了一个col2im
的实现,其可以将一个(B×H′×W′,C×Kh×Kw)的矩阵转化成(B,C,H,W)的特征图。
实训提供的col2im
函数的定义如下:col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0)
,对应参数的含义分别为:
col
:变换后的矩阵;input_shape
:输入特征图的形状;filter_h
和filter_w
:池化窗口的宽和高;stride
:池化的步长;pad
:池化的填充。编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,实现上述卷积层的前向传播。
测试说明
平台会对你编写的代码进行测试,测试方法为:平台会随机产生输入x
、权重W
、偏置b
和输出梯度dout
,然后根据你的实现代码,创建一个Convolution
类的实例,然后利用该实例先进行前向传播计算,再进行反向传播计算。你的答案将并与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过10−5即可。
样例输入:
x:
[[[[0.11 0.65 0.08 0.28 0.77]
[0.12 0.29 0.77 0.58 0.11]
[0.44 0.43 0.4 0.28 0.87]
[0.52 0.78 0.99 0.58 0.13]
[0.46 0.31 0.9 0.19 0.3 ]]
[[0.12 0.65 0.58 0.44 0.9 ]
[0.23 0.34 0.96 0.94 0.37]
[0.63 0.17 0.61 0.22 0.98]
[0.91 0.32 0.78 0.61 0.42]
[0.21 0.01 0.9 0.06 0.2 ]]]
[[[0.79 0.36 0.39 0.6 0.12]
[0.84 0.62 0.32 0.84 0.69]
[0.34 0.21 0.03 0.17 0.22]
[0.6 0.19 0.4 1. 0.93]
[0.46 0.54 0.62 0.8 0.81]]
[[0.12 0.7 0.88 0.53 0.79]
[0.36 0.14 0.87 0.99 0.27]
[0.35 0.87 0.25 0.57 0.3 ]
[0.92 0.23 0.84 0.72 0.9 ]
[0.05 0.91 0.61 0.23 0.56]]]]
W:
[[[[-0.77 0.09 -0.46]
[ 0.42 0.89 0.97]
[ 0.41 0.52 2.57]]
[[-0.75 0.75 -0.38]
[-0.78 -1.72 0.99]
[ 1.24 -0.68 -1.2 ]]]
[[[-0.44 0.3 -0.01]
[-0.03 -0.75 1.33]
[-0.13 1.63 0.02]]
[[-1.11 -1.18 -1.46]
[ 0.76 -1.01 -1.38]
[ 0.88 0.43 -1.41]]]]
b:
[0.94 1.15]
dout:
[[[[0.12 0.09 0.12 0.87 0.02]
[0.21 0.26 0.51 0.61 0.34]
[0.04 0.59 0.69 0.45 0.06]
[0.14 0.56 0.18 0.17 0.47]
[0.69 0.18 0.73 0.4 0.79]]
[[0.8 0.62 0.72 0.45 0.96]
[0.34 0.7 0.15 0.44 0.39]
[0.2 0.97 0.8 0.36 0.93]
[0.93 0.3 0.63 0.13 0.12]
[0.94 0.77 0.18 0.78 0.15]]]
[[[0.35 0.72 0.24 0.56 0.09]
[0.3 0.35 0.56 0.99 0.73]
[0.98 0.96 0.23 0.9 0.11]
[0.13 0.84 0.74 0.83 0.78]
[0.12 0.89 0.44 0.1 0.05]]
[[0.43 0.14 0.4 0.3 0.26]
[0.3 0.19 0.48 0.93 0.53]
[0.06 0.7 0.6 0.6 0.45]
[0.45 0.34 0.53 0.78 0.98]
[0.09 0.4 0.66 0.48 0.97]]]]
stride: 1
pad: 1
则对应的梯度为:
dW:
[[[[ 7.13 8.73 7.06]
[ 8.88 10.84 9.14]
[ 7.69 9.52 7.76]]
[[ 9.1 10.96 9.22]
[10.08 12.4 10.84]
[ 8.37 9.58 8.4 ]]]
[[[ 7.85 9.8 7.89]
[10.09 13.15 10.29]
[ 8.24 10.3 8.54]]
[[ 9.9 12.03 9.94]
[11.2 14.32 10.46]
[ 9.18 10.2 8.73]]]]
db:
[22.28 25.81]
dx:
[[[[-0.86 0.5 0.13 1.01 0.6 ]
[ 0.53 1.35 3.06 1.8 5.08]
[ 0.51 2.03 3.2 3.85 2.66]
[-0.02 3.16 3.41 3.03 3.3 ]
[ 1.74 2.95 4.48 2. 2.9 ]]
[[-1.83 -3.07 -3.9 -3.27 -1.84]
[-1.18 -4.4 -3.96 -4.45 -4.01]
[-0.8 -4.18 -4.8 -2.68 -2.55]
[-1.33 -3.98 -4.19 -4.52 -2.5 ]
[-0.43 -3.09 -2.66 -1.98 -2.85]]]
[[[ 0.05 0.84 -0.12 0.38 0.59]
[ 0.4 2.04 2.73 2.93 4.09]
[ 1.32 2.18 3.09 4.05 5.21]
[ 0.21 5.2 5.26 3.39 5.16]
[ 1.5 2.46 4.97 4.83 4.2 ]]
[[-2.09 -2.98 -2.84 -3.86 -2.09]
[-0.82 -3.19 -4.79 -5.26 -4.81]
[-2.88 -2.64 -2.45 -5.77 -5.37]
[-1.04 -4.3 -3.87 -5.76 -6.24]
[ 0.76 -1.61 -0.85 -0.47 -3.83]]]]
上述结果有四舍五入的误差,你可以忽略。
import numpy as np
from utils import im2col, col2im
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
r'''
卷积层的初始化
Parameter:
- W: numpy.array, (C_out, C_in, K_h, K_w)
- b: numpy.array, (C_out)
- stride: int
- pad: int
'''
self.W = W
self.b = b
self.stride = stride
self.pad = pad
self.x = None
self.col = None
self.col_W = None
self.dW = None
self.db = None
def forward(self, x):
r'''
卷积层的前向传播
Parameter:
- x: numpy.array, (B, C, H, W)
Return:
- y: numpy.array, (B, C', H', W')
H' = (H - Kh + 2P) / S + 1
W' = (W - Kw + 2P) / S + 1
'''
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = 1 + int((H + 2 * self.pad - FH) / self.stride)
out_w = 1 + int((W + 2 * self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
r'''
卷积层的反向传播
Parameter:
- dout: numpy.array, (B, C', H', W')
Return:
- dx: numpy.array, (B, C, H, W)
另外,还需计算以下结果:
- self.dW: numpy.array, (C', C, Kh, Kw) 与self.W形状相同
- self.db: numpy.array, (C',) 与self.b形状相同
'''
########## Begin ##########
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)
self.db = np.sum(dout, axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
return dx
########## End ##########
任务描述
本关任务:实现池化层的反向传播。
相关知识
为了完成本关任务,你需要掌握:
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第7章的内容。
池化层的反向传播
在之前的实训中,我们学习了池化层的前向传播,也知道无论在原理上还是在实现上池化层的前向传播与卷积层的前向传播都有很强的相似性。二者在反向传播上依然如此,所有的计算都是以窗口为处理单位。
对于平均值池化,反向传播非常简单,只要把对应输出位置的梯度平均放回窗口内的每个输入位置即可。对于最大值池化,如我们之前学习的 max 函数存在不可导点,因此我们按照之前 ReLU 的方法,使用次梯度,即:在每个窗口中,将输出位置的梯度放回前向传播时最大值对应的输入位置。下图展示了最大值池化反向传播的过程。
图1 最大值池化的前向和反向传播
池化层反向传播的实现
在实现池化层的前向传播时,我们采用了与实现卷积层时相似的技巧,即先用 im2col 操作,将输入特征图转化成一个矩阵,每个窗口内的数据都对应到矩阵的每一行,这样,窗口内的平均值操作或者最大值操作就变成矩阵的行操作,从而能够利用 numpy 进行高效的操作。因此,在进行反向传播时,也与卷积层相似,再对变换后的矩阵在平均值或最大值反向传播之后,再用 col2im 操作还原为与前向传播时输入特征图相同的形状。
实训拓展了在之前的实训定义的MaxPool
类,实训已经给出了forward(x)
的实现,并针对反向传播的需要对其进行了一定的修改。你需要实现该类的反向传播函数backward(dout)
,dout
是损失函数相对于全连接层输出的梯度,是一个形状为(B,C,H,W)的numpy.ndarray
。在前向传播时,输入特征图和最大值对应的索引分别记录在了self.x
和self.arg_max
中。实训已经提供了一个 col2im 的实现,请参考上一关的介绍。
编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,实现上述池化层的反向传播。
测试说明
平台会对你编写的代码进行测试,测试方法为:平台会随机产生输入x
和输出梯度dout
,然后根据你的实现代码,创建一个MaxPool
类的实例,然后利用该实例先进行前向传播计算,再进行反向传播计算。你的答案将并与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过10−5即可。
样例输入:
x:
[[[[0.79 0.36 0.41 0.16]
[0.94 0.56 0.26 0.89]
[0.82 0.5 0.81 0.47]
[0.95 0.99 0.38 0.94]]
[[0.46 0.74 0.32 0.22]
[0.18 0.05 0.98 0.39]
[0.81 0.9 0.5 0.57]
[0.16 0.18 0.73 0.06]]]
[[[0.51 0.48 0.8 0.57]
[0.64 0.05 0.64 0.28]
[0.01 0.65 0.03 0.21]
[0.24 0.3 0.72 0.89]]
[[0.14 0.43 0.98 0.6 ]
[0.79 0.42 0.42 0.39]
[0.04 0.29 0.55 0.32]
[0.38 0.26 0.39 0.97]]]]
dout:
[[[[0.41 0.71]
[0.28 0.74]]
[[0.39 0.43]
[0.58 0.25]]]
[[[0.07 0.55]
[0.4 0.15]]
[[0.42 0.83]
[0.59 0.38]]]]
则对应的输入特征图的梯度为:
dx:
[[[[0. 0. 0. 0. ]
[0.41 0. 0. 0.71]
[0. 0. 0. 0. ]
[0. 0.28 0. 0.74]]
[[0. 0.39 0. 0. ]
[0. 0. 0.43 0. ]
[0. 0.58 0. 0. ]
[0. 0. 0.25 0. ]]]
[[[0. 0. 0.55 0. ]
[0.07 0. 0. 0. ]
[0. 0.4 0. 0. ]
[0. 0. 0. 0.15]]
[[0. 0. 0.83 0. ]
[0.42 0. 0. 0. ]
[0. 0. 0. 0. ]
[0.59 0. 0. 0.38]]]]
import numpy as np
from utils import im2col, col2im
class MaxPool:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
r'''
池化层的初始化
Parameter:
- pool_h: int
- pool_h: int
- stride: int
- pad: int
'''
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.x = None
self.arg_max = None
def forward(self, x):
r'''
池化层的前向传播
Parameter:
- x: numpy.array, (B, C, H, W)
Return:
- y: numpy.array, (B, C, H', W')
H' = (H - Kh + 2P) / S + 1
W' = (W - Kw + 2P) / S + 1
'''
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h + 2 * self.pad) / self.stride)
out_w = int(1 + (W - self.pool_w + 2 * self.pad) / self.stride)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h * self.pool_w)
arg_max = np.argmax(col, axis=1)
out = np.max(col, axis=1)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
self.x = x
self.arg_max = arg_max
return out
def backward(self, dout):
r'''
池化层的反向传播
Parameter:
- dout: numpy.array, (B, C', H', W')
Return:
- dx: numpy.array, (B, C, H, W)
'''
########## Begin ##########
dout = dout.transpose(0, 2, 3, 1)
pool_size = self.pool_h * self.pool_w
dmax = np.zeros((dout.size, pool_size))
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
dmax = dmax.reshape(dout.shape +(pool_size,))
dcol = dmax.reshape(dmax.shape[0] *dmax.shape[1] * dmax.shape[2],-1)
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride , self.pad)
return dx
########## End ##########