在卷积神经网络(CNN)中,各层之间传递的数据通常为4维张量。例如,一个形状为(10, 1, 28, 28)的数据表示包含10个样本,每个样本高度为28、宽度为28、通道数为1的图像数据。
为了高效地进行卷积运算,常使用一种称为im2col的技术对输入数据进行变换。该方法将原本多维的输入结构转换成二维矩阵形式,从而能够利用高效的矩阵乘法完成滤波器的滑动窗口操作。具体而言,im2col会将应用于输入的局部区域按顺序从上到下、从左到右地展开成列向量:
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
N, C, H, W = input_data.shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
return col
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
N, C, H, W = input_shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
return img[:, :, pad:H + pad, pad:W + pad]
经过im2col处理后,原本复杂的局部计算被转化为标准的线性运算。此时,只需将卷积核的权重参数同样以纵向方式拉直为列向量,并与展开后的输入矩阵相乘,即可完成整个卷积过程。
基于这一原理,可以将卷积层封装为一个名为Convolution的类,通过前向传播中的矩阵乘积和反向传播中的梯度计算实现完整的功能流程:
class Convolution:
# 初始化方法将滤波器(权重)、偏置、步幅、填充作为参数接收
def __init__(self, W, b, stride=1, pad=0):
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):
# 滤波器是 (FN, C, FH, FW)的 4 维形状
# FN、C、FH、FW分别是 Filter Number(滤波器数量)、Channel、Filter Height、Filter Width的缩写
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
# transpose会更改多维数组的轴的顺序
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):
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
对于池化层的实现,也可以采用im2col的方式对输入数据进行重构。尽管其处理逻辑与卷积层类似,但关键区别在于:池化操作在各个通道上独立进行,不涉及跨通道的参数学习。
当输入数据被im2col展开为二维矩阵后,仅需对每一行分别求取最大值(以最大池化为例),再将结果重新组织为所需的输出形状即可完成操作:
class Pooling:
def __init__(self, pool_h, pool_w, stride=2, pad=0):
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):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / 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):
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

雷达卡


京公网安备 11010802022788号







