# 17.2 卷积前向计算代码实现

## 17.2 卷积前向计算代码实现⚓︎

### 17.2.1 卷积核的实现⚓︎

class ConvWeightsBias(WeightsBias_2_1):
def __init__(self, output_c, input_c, filter_h, filter_w, init_method, optimizer_name, eta):
self.FilterCount = output_c
self.KernalCount = input_c
self.KernalHeight = filter_h
self.KernalWidth = filter_w
...

def Initialize(self, folder, name, create_new):
self.WBShape = (self.FilterCount, self.KernalCount, self.KernalHeight, self.KernalWidth)
...


• FilterCount=2，第一维，过滤器数量，对应输出通道数。
• KernalCount=3，第二维，卷积核数量，对应输入通道数。两个Filter里面的Kernal数必须相同。
• KernalHeight=5，KernalWidth=5，卷积核的尺寸，第三维和第四维。同一组WeightsBias里的卷积核尺寸必须相同。

### 17.2.2 卷积前向运算的实现 - 方法1⚓︎

class ConvLayer(CLayer):
def forward(self, x, train=True):
self.x = x
self.batch_size = self.x.shape[0]
else:
#end if
self.z = conv_4d(...)
return self.z


def conv_4d(x, weights, bias, out_h, out_w, stride=1):
batch_size = x.shape[0]
input_channel = x.shape[1]
output_channel = weights.shape[0]
filter_height = weights.shape[2]
filter_width = weights.shape[3]
rs = np.zeros((batch_size, num_output_channel, out_h, out_w))

for bs in range(batch_size):
for oc in range(output_channel):
rs[bs,oc] += bias[oc]
for ic in range(input_channel):
for i in range(out_h):
for j in range(out_w):
ii = i * stride
jj = j * stride
for fh in range(filter_height):
for fw in range(filter_width):
rs[bs,oc,i,j] += x[bs,ic,fh+ii,fw+jj] * weights[oc,ic,fh,fw]


1. 批量数据循环（第一维）：bs in batch_size，对每个样本进行计算；
2. 输出通道循环（第二维）：oc in output_channel。这里先把bias加上了，后加也可以；
3. 输入通道循环：ic in input_channel;
4. 输出图像纵坐标循环：i in out h
5. 输出图像横坐标循环：j in out_w。循环4和5完成对输出图像的每个点的遍历，在下面的子循环中计算并填充值；
6. 卷积核纵向循环（第三维）：fh in filter_height
7. 卷积核横向循环（第四维）：fw in filter_width。循环6和7完成卷积核与输入图像的卷积计算，并保存到循环4和5指定的输出图像的点上。

Time used for Python: 38.057225465774536


### 17.2.3 卷积前向运算的实现 - 方法2⚓︎

pip install numba


@nb.jit(nopython=True)
def jit_conv_4d(x, weights, bias, out_h, out_w, stride=1):
...


Time used for Numba: 0.0727994441986084


    print("correctness:", np.allclose(output1, output2, atol=1e-7))


correctness: True


np.allclose方法逐元素检查两种方法的返回值的差异，如果绝对误差在1e-7之内，说明两个返回的四维数组相似度极高，运算结果可信。

### 17.2.4 卷积前向运算的实现 - 方法3⚓︎

    def forward_img2col(self, x, train=True):
self.x = x
self.batch_size = self.x.shape[0]
assert(self.x.shape == (self.batch_size, self.InC, self.InH, self.InW))
self.col_x = img2col(x, self.FH, self.FW, self.stride, self.padding)
self.col_w = self.WB.W.reshape(self.OutC, -1).T
self.col_b = self.WB.B.reshape(-1, self.OutC)
out1 = np.dot(self.col_x, self.col_w) + self.col_b
out2 = out1.reshape(batch_size, self.OutH, self.OutW, -1)
self.z = np.transpose(out2, axes=(0, 3, 1, 2))
return self.z


#### 四维数组的展开⚓︎

x =
(样本1)                 [样本2]
(通道1)                 (通道1)
[[[[ 0  1  2]           [[[27 28 29]
[ 3  4  5]              [30 31 32]
[ 6  7  8]]             [33 34 35]]
(通道2)                 (通道2)
[[ 9 10 11]             [[36 37 38]
[12 13 14]              [39 40 41]
[15 16 17]]             [42 43 44]]
(通道3)                 (通道3)
[[18 19 20]             [[45 46 47]
[21 22 23]              [48 49 50]
[24 25 26]]]            [51 52 53]]]]
------------------------------------------
col_x =
[[0.  1.  3.  4.|  9. 10. 12. 13.| 18. 19. 21. 22.]
[ 1.  2.  4.  5.| 10. 11. 13. 14.| 19. 20. 22. 23.]
[ 3.  4.  6.  7.| 12. 13. 15. 16.| 21. 22. 24. 25.]
[ 4.  5.  7.  8.| 13. 14. 16. 17.| 22. 23. 25. 26.]
----------------+----------------+----------------
[27. 28. 30. 31.| 36. 37. 39. 40.| 45. 46. 48. 49.]
[28. 29. 31. 32.| 37. 38. 40. 41.| 46. 47. 49. 50.]
[30. 31. 33. 34.| 39. 40. 42. 43.| 48. 49. 51. 52.]
[31. 32. 34. 35.| 40. 41. 43. 44.| 49. 50. 52. 53.]]


• 前4行是样本1的数据，后4行是样本2的数据
• 前4列是通道1的数据，中间4列是通道2的数据，后4列是通道3的数据

#### 权重数组的展开⚓︎

weights=
(过滤器1)               (过滤器2)
(卷积核1)               (卷积核1)
[[[[ 0  1]             [[[12 13]
[ 2  3]]               [14 15]]
(卷积核2)               (卷积核2)
[[ 4  5]               [[16 17]
[ 6  7]]               [18 19]]
(卷积核3)               (卷积核3)
[[ 8  9]               [[20 21]
[10 11]]]              [22 23]]]]
---------------------------------------
col_w=
[[ 0 12]
[ 1 13]
[ 2 14]
[ 3 15]
[ 4 16]
[ 5 17]
[ 6 18]
[ 7 19]
[ 8 20]
[ 9 21]
[10 22]
[11 23]]


#### 结果数据的处理⚓︎

[[1035.| 2619.]
[1101.| 2829.]
[1233.| 3249.]
[1299.| 3459.]
------+-------
[2817.| 8289.]
[2883.| 8499.]
[3015.| 8919.]
[3081.| 9129.]]


1. 两个样本的原始数组x展开后的矩阵col_x$8\times 12$，计算结果是$8\times 2$，如果原始数据只有一个样本，则展开矩阵col_x的形状是$4\times 12$，那么运算结果将会是$4\times 2$。所以，在上面这个$8\times 2$的矩阵中，前4行应该是第一个样本的卷积结果，后4行是第二个样本的卷积结果。
2. 如果输出通道只有一个，则权重矩阵w展开后的col_w只有一列，那么运算结果将会是$8\times 1$；两个输出通道的运算结果是$8\times 2$。所以第一列和第二列应该是两个通道的数据，而不是两个样本的数据。

• 第1列的前4行是第1个样本的第1个通道的输出
• 第2列的前4行是第1个样本的第2个通道的输出
• 第1列的后4行是第2个样本的第1个通道的输出
• 第2列的后4行是第2个样本的第2个通道的输出

1. 先把数据变成2个样本 * 输出高度 * 输出宽度的形状：
out2 = output.reshape(batch_size, output_height, output_width, -1)


out2=
[[[[1035. 2619.]
[1101. 2829.]]
[[1233. 3249.]
[1299. 3459.]]]
[[[2817. 8289.]
[2883. 8499.]]
[[3015. 8919.]
[3081. 9129.]]]]


1. 把第4维数据放到第2维（由于是0-base的，所以是把第3维移到第1维的位置）：
out3 = np.transpose(out2, axes=(0, 3, 1, 2))


conv result=
(样本1)                     (样本2)
(通道1)                     (通道1)
[[[[1035. 1101.]            [[[2817. 2883.]
[1233. 1299.]]              [3015. 3081.]]
(通道2)                     (通道2)
[[2619. 2829.]              [[8289. 8499.]
[3249. 3459.]]]             [8919. 9129.]]]]


#### 验证正确性⚓︎

def test_4d_im2col():
......
f1 = c1.forward_numba(x)
f2 = c1.forward_img2col(x)
print("correctness:", np.allclose(f1, f2, atol=1e-7))


correctness: True


#### 性能测试⚓︎

def test_performance():
...
print("compare correctness of method 1 and method 2:")
print("forward:", np.allclose(f1, f2, atol=1e-7))


method numba: 11.663846492767334
method img2col: 14.926148653030396
compare correctness of method 1 and method 2:
forward: True


numba方法会比im2col方法快3秒，目前看来numba方法稍占优势。但是如果没有numba的帮助，im2col方法会比方法1快几百倍。

### 代码位置⚓︎

ch17, Level2

• test_2d_conv，理解2维下im2col的工作原理
• understand_4d_im2col，理解4维下im2col的工作原理
• test_4d_im2col，比较两种方法的结果，从而验证正确性
• test_performance，比较两种方法的性能