Skip to content
BaiRuic
Go back

动手写NumpyNN CNN

目录
NumpyNN 系列导航

基础知识

卷积网络适合用来处理网格状数据,比如图像数据、时间序列数据等,一般由卷积层、汇聚层(池化层)和全连接层构成。

如果用全连接来处理这些网格状数据有以下缺点:

  • 参数太多。对一张1001003的图片,第一层就需要30000个权重值,随着隐藏层的增多,参数的规模也会急剧增多,就会导致训练效率低,也很容易出现过拟合。
  • 局部不变性特征。自然图像中的物体都具有局部不变性特征,比如尺度缩放、平移、旋转等操作不影响其语义信息.而全连接前馈网络很难提取这些局部不变性特征。

因此在处理网格状数据时用卷积层来代替全连接层,卷积层也是一种前馈神经网络。

卷积层的运算过程如下图,用一个卷积核扫完整张图片:

NumpyNN - 2019-06-19-juanji

其中黄色的是每次卷积操作的卷积区域,对应生成右边特征图红色的值。

注:上图展示了基础的单通道卷积操作,且没用加偏置,即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

可见,多通道多样本的卷积无非是多增加了三层循环,其余操作基本没变。

注:本文所写的卷积操作完全是按照定义来编写,虽然经过了测试,且能保证和深度学习框架中卷积操作计算结果一致,但在实际生产中并不适用,因为效率太低了。

小结

本文按照卷积定义写了多通道,多样本卷积操作。后续还有转置卷积,膨胀卷积,群组卷积,未完待续….