任务描述
本关任务:实现卷积层的前向传播。
相关知识
为了完成本关任务,你需要掌握:
- 全连接层存在的问题;
- 什么是卷积;
- 什么是卷积层。
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第 7 章的内容。
全连接层存在的问题
在上一个实训中,我们对全连接层进行了学习。在全连接层中,所有的输入神经元和所有的输出神经元之间都两两相连,这样可以更加充分地提取输入和输出之间的关系。但是,这样的做法也会带来一些问题。
首先,全连接层忽略了数据本身的结构和形状,无法充分利用数据的局部性。例如,图像是一种常用的数据形式,也是深度学习领域的一大热点。在图像中,同一个物体往往会占据一块连续的区域,如果我们的目标是识别这个物体,那么只要对对应的区域进行特征提取,显然是个更合理的选择。而全连接层只能对整张图片进行学习,这不是个很好的方式。
另一方面,全连接层的计算和存储开销非常巨大。还是以图像为例,对于一个224×224×3的图像(宽和高是 224 像素,有 RGB 三个颜色通道),如果我们希望把通道数变成 64,此时输出是一个224×224×64的特征图。如果使用全连接层,那么一共需要224×224×3×224×224×64=483385147392个参数和乘加运算,单纯存储这些参数就需要1.7TB的空间,这明显是无法承受的。而如果使用我们后面介绍的3×3卷积层,则只需要64×3×3×3=1728个参数和224×224×64×3×3×3=86704128次乘加运算,整体的存储和计算开销有了明显的下降。
卷积运算和卷积层
卷积运算是图像处理领域一种非常常用的操作,实际上就是一种滤波。对于H×W的输入图像,卷积运算将一个Kh×Kw的卷积核在输入图像上进行滑窗,输入图像在窗口内的输入与卷积核对应位置的元素对应相乘再相加,就得到了最终的结果。下图展示了卷积运算的步骤。
图1 卷积操作
为了控制卷积运算滑窗的方式和输出特征图的形状,还引入了步长(stride)和填充(pad)两个概念,对于输入为H×W,卷积核为Kh×Kw,步长为S,填充为P的卷积操作,其输出图像的尺寸Ho×Wo可以通过以下公式计算:
一种最常用的卷积层的配置是卷积核大小为3×3,步长为1,填充为1,此时输入输出特征图的大小相同。更加详细的对卷积运算的介绍可以参考教材对应章节和这篇文章。
卷积层就是把卷积操作应用在神经网络中得到的网络层。在上述卷积操作的基础上,卷积层引入了通道的概念,每个输出通道和每个输入通道之间都对应了一组卷积参数,这些卷积参数叫做卷积核。每个输出通道是所有输入通道和对应卷积参数的卷积运算的结果求和得到的。下图展示了卷积层的计算方式。
图2 卷积层的计算
卷积层是卷积神经网络(CNN)的核心组成部分。通常来说,卷积神经网络是由一系列的卷积层堆叠、组合而成,卷积层与池化层、批归一化层(batch normalization)、激活层的不同组合,在计算机视觉任务上展现了强大的拟合能力。如今,卷积神经网络已经成为解决二维计算机视觉任务中最重要的方法,并且在三维计算机视觉任务中也发挥了很大的作用。
卷积层的实现
在实现卷积层时,为了充分利用矩阵计算的便利性,通常会先将输入特征图通过一个im2col
操作转化成一个大矩阵,这个矩阵的每一行对应卷积时的一个窗口。这样,卷积操作就变成了矩阵乘法。具体的分析可以参考教材第7.4节中的介绍。
实训已经预先定义了一个Convolution
类,在该类的构造函数中,其接收对应的权重W
,偏置b
,步长stride
和填充pad
。权重W
是一个N×C×Kh×Kw的numpy.ndarray
,偏置b
是一个长度为M的numpy.ndarray
,其中N是输出通道数,C是输入通道数,Kh和Kw是卷积核的尺寸。
在本实训中,你需要实现前向传播函数forward()
。forward()
函数的输入x
是一个维度等于4的numpy.ndarray
,形状为(B,C,H,W),其中B是 batch size。首先,你需要对x
进行im2col
操作,实训已经提供了一个im2col
操作,其将输入特征图转化成一个(B×Ho×Wo,Kh×Kw×C)的矩阵,每一行代表一个卷积窗口。值得注意的是,因为转化后的特征图矩阵是(B×Ho×Wo,Kh×Kw×C)的,而权重W是Co,C,Kh,Kw的,因此,我们在实现时需要先将W做一次 reshape,变成Co,C×Kh×Kw,再做一次转置,再进行矩阵乘法:
此时,Fo^是(B×Ho×Wo,Co)的,此时,通过一次 reshape 和 transpose 操作就可以将其转化成(B,Co,Ho,Wo)的Fo。
实训提供的im2col
函数的定义如下:im2col(input_data, filter_h, filter_w, stride=1, pad=0)
,对应参数的含义分别为:
input_data
:输入特征图;filter_h
和filter_w
:池化窗口的高和宽;stride
:池化的步长;pad
:池化的填充。
编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,实现上述卷积层的前向传播。
测试说明
平台会对你编写的代码进行测试,测试方法为:平台会随机产生输入x
、权重W
和偏置b
,然后根据你的实现代码,创建一个Convolution
类的实例,然后利用该实例进行前向传播计算,并与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过10−5即可。
样例输入:
x:
[[[[0.7 0.25 0.87 0.76]
[0.13 0.87 0.02 0.29]
[0.81 0.92 0.7 0.13]
[0.67 0.01 0.69 0.46]]
[[0.41 0.78 0.91 0.3 ]
[0.56 0.73 0.88 0.2 ]
[0.47 0.06 0.41 0.24]
[0.79 0.2 0.84 0.2 ]]]
[[[0.94 0.19 0.83 0.79]
[0.93 0.65 0.68 0.98]
[0.16 0.9 0.15 0.71]
[0.49 0.21 0.89 0.33]]
[[0.56 0.82 0.6 0.11]
[0.98 0.73 0.86 0.29]
[0.48 0.31 0.96 0.73]
[0.86 0.94 0.78 0.57]]]]
W:
[[[[ 0.73 0.61 0.31]
[-1.07 -1.13 -0.19]
[-0.24 0.9 -1.68]]
[[-0.59 -0.84 -0.37]
[-0.74 -0.83 -0.23]
[ 0.26 0.44 -0.42]]]
[[[-0.4 0.42 1.27]
[ 0.94 -0.58 1.41]
[-1.91 -1.18 1.93]]
[[-0.03 -1.14 0.72]
[-2.05 -0.45 -0.6 ]
[-0.84 0.1 -0. ]]]]
b:
[1.62 1.12]
stride: 1
pad: 1
则对应的输出特征图为:
[[[[-1.14 0.1 -1.35 -0.52]
[-0.08 -1.59 -0.81 0.93]
[ 0.59 -2.91 -0.92 0.28]
[ 0.51 0.7 0.52 -0.28]]
[[ 2. -0.53 -1.93 -1.6 ]
[ 3.15 -2.23 -1.62 -3.13]
[ 2.07 0.59 0.42 -1.93]
[ 1.29 1.66 0.27 -0.83]]]
[[[-0.26 -1.56 -1.55 0.36]
[-1.97 -1.57 -2.57 0.73]
[ 0.55 -1.75 -0.36 0.22]
[-0.04 -1.09 -1.43 -1.38]]
[[ 0.35 -0.79 -0.78 -3. ]
[ 2.82 -1.5 0.63 -2.71]
[ 2.43 -0.31 0.33 -4.35]
[ 1.08 0.88 -1.33 -0.7 ]]]]
上述结果有四舍五入的误差,你可以忽略。
开始你的任务吧,祝你成功!
import numpy as np
from utils import im2col
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
r'''
卷积层的初始化
Parameter:
- W: numpy.array, (C_out, C_in, K_h, K_w)
- b: numpy.array, (C_out)
- stride: int
- pad: int
'''
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):
r'''
卷积层的前向传播
Parameter:
- x: numpy.array, (B, C, H, W)
Return:
- y: numpy.array, (B, C', H', W')
H' = (H - Kh + 2P) / S + 1
W' = (W - Kw + 2P) / S + 1
'''
########## Begin ##########
K_n, C, K_h, K_w =self.W.shape
B, C, H, W = x.shape
out_h = int((H - K_h + 2*self.pad) / self.stride + 1)
out_w = int((W - K_w + 2*self.pad) / self.stride + 1)
col = im2col(x, K_h, K_w, self.stride, self.pad)
col_w = self.W.reshape(K_n, -1).T
out = np.dot(col, col_w) + self.b
out = out.reshape(B, out_h, out_w, -1).transpose(0, 3, 1, 2)
return out
########## End ##########
反问:
- 代码中col_w和out中的-1是起什么作用;
- 代码out中transpose(0, 3, 1, 2)是什么意思。