NumpyNN 系列导航
基础知识
卷积网络适合用来处理网格状数据,比如图像数据、时间序列数据等,一般由卷积层、汇聚层(池化层)和全连接层构成。
如果用全连接来处理这些网格状数据有以下缺点:
- 参数太多。对一张1001003的图片,第一层就需要30000个权重值,随着隐藏层的增多,参数的规模也会急剧增多,就会导致训练效率低,也很容易出现过拟合。
- 局部不变性特征。自然图像中的物体都具有局部不变性特征,比如尺度缩放、平移、旋转等操作不影响其语义信息.而全连接前馈网络很难提取这些局部不变性特征。
因此在处理网格状数据时用卷积层来代替全连接层,卷积层也是一种前馈神经网络。
卷积层的运算过程如下图,用一个卷积核扫完整张图片:
其中黄色的是每次卷积操作的卷积区域,对应生成右边特征图红色的值。
注:上图展示了基础的单通道卷积操作,且没用加偏置,即bias=False
代码实现
先来写一个基本框架,然后慢慢丰富卷积的细节
class Conv2d:
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, bias=True):
self.in_channels = in_channels
self.out_channels = out_channels
self.kenel_size = kernel_size
self.stride = stride
self.padding = padding
self.bias = bias
self.kernel = np.random.normal((self.out_channels, self.in_channels, *self.kernel_size))
self.b = np.zeros((self.out_channels)) if self.bias else None
def forward(self, X):
return self._conv2d(X, self.kernel, self.b, self.stride, self.padding)
def backward(self, pre_grad):
pass
def __call__(self, *arg):
return self.forward(*arg)
def _conv2d(self, X, K, b, stride, padding):
pass
接下来就是编写核心代码 _conv2d 方法了。
先写一个简单的单通道卷积:
def conv2d(self, X, K, b):
"""
X: 输入特征图, shape:[x_w, x_h]
K: 卷积核, shape:[k_w, k_h]
"""
x_w, x_h = X.shape
k_w, k_h = K.shape
# 输出特征图大小
y_w, y_h = x_w - k_w + 1, x_h-k_h+1
Y = np.zeros((y_w, y_h))
if b is None:
b = np.zeros()
# 计算输出特征图的每个值
for i in range(0, y_h):
for j in range(0, y_w):
conv_region = X[i:i+k_h, j:j+k_w]
Y[i, j] = np.sum(conv_region * K) + b
return Y
上述代码不仅仅是单样本单通道,且没有考虑步长,填充。接下来,推广到步长不为1,且可自定义设置填充的卷积操作:
def _conv2d(self, X, K, b, stride, padding):
"""
X: 输入特征图, shape:[x_w, x_h]
K: 卷积核, shape:[k_w, k_h]
"""
# 填充
if padding > 0:
X =np.pad(array=X,
pad_width=((padding, padding), (padding, padding)),
mode="constant",
condtant_values = 0)
x_h, x_w = X.shape
k_h, k_w = K.shape
y_h = (x_h - k_h) // stride + 1
y_w = (x_w - k_w) // stride + 1
Y = np.zeros((y_h, y_w))
if b is None:
b = np.zeros()
for i in range(0, x_h-y_w+1, stride):
for j in range(x_w-k_w+1, stride):
conv_region = X[i:i+k_h, j:j+k_w]
Y[i//stride][j//stride] = np.sum(conv_region * K) + b
return Y
注:加入padding之后,只需要在最开始按照设定值填充,之后就可以不用管padding参数了。
最后,考虑多通道多样本的卷积操作:
def _conv2d(self, X, K, b, stride, padding):
"""
param:
X: 输入特征图
type: np.array
shape: [batch_size, in_channels, x_h, x_w]
K: 卷积核
type: np.array
shape: [out_channels, in_channels, k_h, k_w]
stride: 卷积步长
padding: padding
"""
if padding > 0:
X =np.pad(array=X,
pad_width=((0, 0), (0, 0),(padding, padding), (padding, padding)),
mode="constant",
condtant_values = 0)
batch_size, in_channels_x, x_h, x_w = X.shape
out_channels, in_channels_k, k_h, k_w = K.shape
assert in_channels_x == in_channels_k, "输入特征图和卷积核维度不匹配"
y_h = (x_h - k_h) // stride + 1
y_w = (x_w - k_w) // stride + 1
if b is None:
b = np.zeros((out_channels))
Y = np.zeros((batch_size, out_channels, y_h, y_w))
for b_s in range(0, batch_size):
for o_c in range(0, out_channels):
Y[b_s, o_c] += b[o_c]
for i_c in range(0, in_channels_k):
for i in range(0,x_h-k_h+1 , stride):
for j in range(0,x_w-k_w+1 ,stride):
conv_region = X[b_s, i_c, i:i+k_h, j:j+k_w]
Y[b_s, o_c, i//stride, j//stride] = np.sum(conv_region * K[o_c, i_c])
return Y
可见,多通道多样本的卷积无非是多增加了三层循环,其余操作基本没变。
注:本文所写的卷积操作完全是按照定义来编写,虽然经过了测试,且能保证和深度学习框架中卷积操作计算结果一致,但在实际生产中并不适用,因为效率太低了。
小结
本文按照卷积定义写了多通道,多样本卷积操作。后续还有转置卷积,膨胀卷积,群组卷积,未完待续….