文章目录
手撕卷积层(numpy)
卷积层的操作和定义
基础运算
卷积层的核心就是卷积运算,其实就是矩阵运算,大概过程如下:
( 4 , 4 ) (4, 4) (4,4)和 ( 3 , 3 ) (3, 3) (3,3),最后输出结果为 ( 2 , 2 ) (2, 2) (2,2)
偏置 bias
但是在Pytorch的CNN中,除了基础卷积操作以外,还存在偏置
,如下所示:
填充 padding
CNN中,在进行卷积处理之前,有时需要对周围元素填入数据,称之为填充(padding)
,如下所示
- p a d d i n g = 1 padding = 1 padding=1: 图像大小经过卷积后不变
步幅 Stride
卷积核的移动步长称之为 步幅(Stride)
,上述例子均为
S
t
r
i
d
e
=
1
Stride=1
Stride=1,
关系计算公式
假设输入大小为 ( H , W ) (H, W) (H,W),卷积核大小为 ( F H , F W ) (FH, FW) (FH,FW),输出大小为 ( O H , O W (OH, OW (OH,OW, p a d d i n g = P padding=P padding=P, S t r i d e = S Stride=S Stride=S。此时输出大小的计算公式为:
O
H
=
H
+
2
P
−
F
H
S
+
1
OH=\frac{H+2P-FH}{S}+1
OH=SH+2P−FH+1
O
W
=
W
+
2
P
−
F
W
S
+
1
OW=\frac{W+2P-FW}{S}+1
OW=SW+2P−FW+1
卷积层的实现
初始数据生成
x = np.random.rand(10, 1, 28, 28)
x.shape
# (10, 1, 28, 28)
生成四维数组,访问每一个数据,用x[0]或者x[1]即可;访问第一个数据第一个通道的二维数据可以用x[0,0]或者是x[0][0]
im2col & col2im
针对多层的卷积,全部使用for循环会变慢,numpy有这个特点,所以这里使用im2col(image to column)这个函数来实现:
对3维或者4维数据应用,应用im2col后,变成二维矩阵,以适应滤波器。将应用滤波器的区域(3维方块)横向展开为1列。im2col会在所有应用滤波器的地方进行这个展开处理。
步幅比较小的时候,展开后的个数会多于原方块的元素个数,会消耗更多的内存
def im2col(self, input_data, conv_h, conv_w, stride=1, padding=0):
"""
:param input_data: N x C x H x W
:param conv_h: 卷积核的高
:param conv_w: 卷积核的长
:param stride: 步幅
:param padding: 填充
:return: col 2维数据
"""
N, C, H, W = input_data.shape
out_h = (H + 2 * padding - conv_h) // stride + 1
out_w = (W + 2 * padding - conv_w) // stride + 1
img = np.pad(input_data, [(0, 0), (0, 0), (padding, padding), (padding, padding)], "constant")
col = np.zeros(N, C, conv_h, conv_w, out_h, out_w)
for y in range(conv_h):
y_max = y + stride * out_h
for x in range(conv_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(self, col, input_shape, conv_h, conv_w, stride=1, padding=0):
"""
:param col:
:param input_shape: 输入数据形式 例如:(10, 1, 28, 28)
:param conv_h:
:param conv_w:
:param stride:
:param padding:
:return:
"""
N, C, H, W = input_shape
out_h = (H + 2 * padding - conv_h) // stride + 1
out_w = (W + 2 * padding - conv_w) // stride + 1
col = np.zeros(N, out_h, out_w, C, conv_h, conv_w).transpose(0, 3, 4, 5, 1, 2)
img = np.zeros((N, C, H + 2 * padding + stride - 1, W + 2 * padding + stride - 1))
for y in range(conv_h):
y_max = y + stride * out_h
for x in range(conv_w):
x_max = x + stride * out_h
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
return img[:, :, padding:H + padding, padding:W + padding]
基础卷积层实现
class Convolution:
def __init__(self, W, b, stride=1, padding=0):
"""
:param W: 权重
:param b: 偏置
:param stride: 步幅
:param padding: 填充
"""
self.W = W
self.b = b
self.stride = stride
self.padding = padding
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2 * self.padding - FH) / self.stride)
out_w = int(1 + (W + 2 * self.padding - FW) / self.stride)
col = self.im2col(x, FH, FW, self.stride, self.padding)
col_W = self.W.reshape(FN, -1).T # conv2d 展开
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
return out
reshape将各个滤波器的方块纵向展开为1列。这里通过reshape(FN,-1)将参数指定为-1,这是reshape的一个便利的功能。通过在reshape时指定为-1,reshape函数会自动计算-1维度上的元素个数,以使多维数组的元素个数前后一致
(10, 3, 5, 5)形状的数组的元素个数共有750个,指定reshape(10,-1)后,就 会转换成(10, 75)形状的数组。
Forward中最后将输出的大小转换成合适的形状,使用了numpy中的transpose函数。transpose会更改多维数组的轴的顺序。如图7-20 所示,通过指定从0开始的索引(编号)序列,就可以更改轴的顺序。
进行卷积层的反向传播时,要进行逆处理,比如说使用col2im函数
最终卷积层实现
import os, sys
import collections
import numpy as np
class Convolution:
def __init__(self, W, b, stride=1, padding=0):
"""
:param W: 权重
:param b: 偏置
:param stride: 步幅
:param padding: 填充
"""
self.W = W
self.b = b
self.stride = stride
self.padding = padding
# 中间数据,backward时会用到
self.x = None
self.col = None
self.col_W = None
# 权重和偏置参数的梯度
self.dW = None
self.db = None
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2 * self.padding - FH) / self.stride)
out_w = int(1 + (W + 2 * self.padding - FW) / self.stride)
col = self.im2col(x, FH, FW, self.stride, self.padding)
col_W = self.W.reshape(FN, -1).T # conv2d 展开
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
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 = self.col2im(dcol, self.x.shape, FH, FW, self.stride, self.padding)
return dx
def im2col(self, input_data, conv_h, conv_w, stride=1, padding=0):
"""
:param input_data: N x C x H x W
:param conv_h: 卷积核的高
:param conv_w: 卷积核的长
:param stride: 步幅
:param padding: 填充
:return: col 2维数据
"""
N, C, H, W = input_data.shape
out_h = (H + 2 * padding - conv_h) // stride + 1
out_w = (W + 2 * padding - conv_w) // stride + 1
img = np.pad(input_data, [(0, 0), (0, 0), (padding, padding), (padding, padding)], "constant")
col = np.zeros(N, C, conv_h, conv_w, out_h, out_w)
for y in range(conv_h):
y_max = y + stride * out_h
for x in range(conv_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(self, col, input_shape, conv_h, conv_w, stride=1, padding=0):
"""
:param col:
:param input_shape: 输入数据形式 例如:(10, 1, 28, 28)
:param conv_h:
:param conv_w:
:param stride:
:param padding:
:return:
"""
N, C, H, W = input_shape
out_h = (H + 2 * padding - conv_h) // stride + 1
out_w = (W + 2 * padding - conv_w) // stride + 1
col = np.zeros(N, out_h, out_w, C, conv_h, conv_w).transpose(0, 3, 4, 5, 1, 2)
img = np.zeros((N, C, H + 2 * padding + stride - 1, W + 2 * padding + stride - 1))
for y in range(conv_h):
y_max = y + stride * out_h
for x in range(conv_w):
x_max = x + stride * out_h
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
return img[:, :, padding:H + padding, padding:W + padding]