自己实现 Conv 类

在实现时,我们假定直接给出卷积核的参数

除此之外,步长、如何 padding 是两个很重要的参数,因此连同卷积核参数一起作为类的初始化参数。

class MyConv(object):
    def __init__(self, weight: List, stride: int, padding_mode='VALID') -> None:
        super().__init__()
        # 将 list 的数据变成numpy类型的数据
        self.weight = np.asarray(weight, np.float32)  
        self.s = (stride, stride)
        # 有三种 'VALID', 'SAME', 'FULL'
        self.padding_mode = padding_mode
        # self.kc: kernel的out_channels, self.k: kernel 尺寸   
        self.kc, self.k, _ =  self.weight.shape  

显然,数据 input_data 进来后,我们需要考虑如何 padding 这个数据,一般来说,我们认为有三种 padding 模式:'VALID', 'SAME', 'FULL' (可以参考这里

padding 时实际上只需要考虑在高、宽上单个方向上 padding 多少像素即可,然后统一进行 padding

    def padding(self, data):
        c, h, w = data.shape
        if self.padding_mode == 'VALID':
	        # pad_h 表示 h 上单个方向上需要 padding 的像素数
            pad_h = 0  
            pad_w = 0
        elif self.padding_mode == 'SAME':
	        # 求解 h = (h-f+2p)//s + 1 -> ph=?, pw 同理
            pad_h = ((h-1)*self.s[0]+self.k-h) // 2 # 
            pad_w = ((w-1)*self.s[1]+self.k-w) // 2 # 
        elif self.padding_mode == 'FULL':
            pad_h = self.k - 1
            pad_w = self.k - 1
        # 创建padding后的数据,并进行赋值
        new_data = np.zeros([c, h+2*pad_h, w+2*pad_w])
        for i in range(c):
            new_data[i, pad_h:pad_h+h, pad_w:w+pad_w] = data[i]
        return new_data

我们知道,卷积的过程是卷积核在输入数据上进行滑动的同时并进行计算,每次进行计算时需要找到对应的data上的数据,这里我们使用生成器来实现

 # 使用生成器获得每次的区域
    def iter_region(self, data):
	    # self.out_h 和 self.out_w 是卷积输出的特征图的长和宽
        for i in range(self.out_h):
            for j in range(self.out_w):
                cur_h = i * self.s[0]
                cur_w = j * self.s[1]
                # 当前滑动到的窗口(在input_data里面)
                roi = data[:, cur_h:cur_h+self.k, cur_w:cur_w+self.k]  
                yield roi, i, j

最后,我们只需要写一下前向传播过程 forward 即可

    def forward(self, data):
	    # 输入的数据是 list 格式,转成 numpy 后先padding
        data = self.padding(np.asarray(data))
        c, h, w = data.shape
        # 卷积核的通道数必需等于输入数据的通道数
        assert c == self.kc  
        
        # 输出特征图的长和宽:H' = (H-F+2P)//S+1
        # 这里我们因为已经padding了,所以此时P=0
        self.out_h = (h-self.k) // self.s[0] + 1
        self.out_w = (w-self.k) // self.s[1] + 1
        # 创建输出特征图
        out = np.zeros([self.out_h, self.out_w])
        for roi, i, j in self.iter_region(data):
            out[i][j] = np.sum(roi*self.weight)
        return out

官方对比代码

为了验证我们自己实现的代码,我们这里利用 pytorch 实现了一个卷积,用于对比结果。

这里,我们假定 out_channels = 1

class officalConv(torch.nn.Module):
    def __init__(self, weight, stride, padding=0) -> None:
        super().__init__()
        # 输入的 weight 是 List,尺寸为 [in_channels, K, K]
        # 将 weight 形状变为 [out_channels, in_channels, K, K],即认为 n_filters = 1
        weight = torch.FloatTensor(weight).unsqueeze(0)
        self.weight = torch.nn.Parameter(data=weight, requires_grad=False)
        self.stride = stride
        self.out_channels, self.in_channels, self.K , _ = self.weight.shape
        self.padding = padding
 
    def forward(self, input):
	    # 输入data的尺寸为 [C, H, W]
	    # 填充 B 维度,并假定为 1
        input = torch.FloatTensor(input).unsqueeze(0)
        B, C, H, W = input.shape
        assert C == self.in_channels
        out = F.conv2d(input, self.weight, stride=self.stride, padding=self.padding)
        return out.numpy()

主函数

if __name__ == '__main__':
    input_data = [
        [
            [1,0,1,2,1],
            [0,2,1,0,1],
            [1,1,0,2,0],
            [2,2,1,1,0],
            [2,0,1,2,0]
        ],
        [
            [2,0,2,1,1],
            [0,1,0,0,2],
            [1,0,0,2,1],
            [1,1,2,1,0],
            [1,0,1,1,1]
        ]
    ]  
 
    weight = [
        [
            [1,0,1],
            [-1,1,0],
            [0,-1,0]
        ],
        [
            [-1,0,1],
            [0,0,1],
            [1,1,0]
        ]
    ]
 
    conv_official = officalConv(weight, 1, 1)
    print(conv_official.forward(input_data))
    my_conv = MyConv(weight, 1, padding_mode='SAME')
    print(my_conv.forward(input_data))

参考资料: