第1关:实现卷积层的前向传播
任务描述
本关任务:实现卷积层的前向传播。
相关知识
为了完成本关任务,你需要掌握:
- 全连接层存在的问题;
- 什么是卷积;
- 什么是卷积层。
本实训内容可参考《深度学习入门——基于 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可以通过以下公式计算:
Ho=(H−Kh+2P)/S+1Wo=(W−Kw+2P)/S+1
一种最常用的卷积层的配置是卷积核大小为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^=im2col(F)×W^T
此时,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):
'''
卷积层的前向传播
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 ##########
FN,C,FH,FW = self.W.shape
N,C,H,W = x.shape
out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (W + 2*self.pad - FW) / self.stride)
col = im2col(x,FH,FW,self.stride,self.pad)
col_W = self.W.reshape(FN,-1).T #滤波器展开为2维数组
out = np.dot(col,col_W) + self.b
out = out.reshape(N,out_h,out_w,-1).transpose(0,3,1,2)
return out
########## End ##########
第2关:实现池化层的前向传播
任务描述
本关任务:实现池化层的前向传播。
相关知识
为了完成本关任务,你需要掌握:
- 感受野与下采样;
- 池化层的定义。
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第7章的内容。
感受野与下采样
在上一个实训中,我们对卷积层进行了学习,也了解了使用卷积层对图像进行处理的基本原理。在本实训中,我们更进一步,来思考卷积神经网络中的感受野和下采样。在卷积神经网络中,通过卷积层的堆叠,特征图中的每个像素会受到输入图像中一个特定区域的影响。这个区域的大小就叫做感受野。感受野的大小,决定了网络提取到的特征的范围大小。试想,如果一个网络模型的感受野非常小,你又如何能够期望这个网络模型可以识别出比其感受野更大的目标呢?因此,在很多视觉任务中,如图像分类、语义分割、目标检测等,感受野的大小对网络模型的性能有非常大的影响。
通过卷积层的堆叠,网络模型的感受野实际上是在不断增大的,但是这个增大的速度非常慢。为了加快感受野的扩大,可以采用将卷积的步长设为2的方式,对特征图进行下采样。通过下采样,可以将网络模型的感受野扩大2倍。然而,尽管卷积计算比全连接层快了很多,其仍然有很大的计算开销。那么有没有一种快速的方法可以实现下采样和扩大感受野呢?这个答案就是池化层。
池化层
池化层与卷积层相似,通过在输入特征图上进行滑窗,并将窗口内的特征值通过一个归结函数计算得到一个输出值。使用 max 作为归结函数的池化层叫做最大值池化,使用平均值作为归结函数的池化层叫做平均值池化,二者都非常常用。通常,池化层会设置窗口大小Kh×Kw以及步长和填充。一种常用的池化层的配置是Kh=Kw=2,步长为2,填充为0,这种池化层可以将特征图的长和宽都缩小一半。下图展示了最大值池化的计算方式。
图1 最大值池化
在池化层中,输入特征图和输出特征图一一对应,池化操作只在每个通道内进行,而没有通道间的交叉,因此池化层的计算非常快。另外,因为池化层无差别的对待池化窗口内的特征值,所以相比于卷积层计算加权和的方式,池化层能提供更好的平移和旋转不变性。
池化层的实现
在实现池化层时,与实现卷积层相似,为了充分利用矩阵计算的便利性,通常会先将输入特征图通过一个im2col
操作转化成一个大矩阵,这个矩阵的每一行对应池化时的一个窗口。这样,卷积操作就变成了逐行进行的 max 或者平均值操作。具体的分析可以参考教材第7.3和7.5节中的介绍。
在本实训中,你只需要实现最大值池化。实训已经预先定义了一个MaxPool
类,在该类的构造函数中,其接受对应的池化窗口大小pool_h
和pool_w
,步长stride
和填充pad
。
在本实训中,你需要实现前向传播函数forward()
。forward()
函数的输入x
是一个维度等于4的numpy.ndarray
,形状为(B,C,H,W),其中B是 batch size。首先,你需要对x
进行im2col
操作,实训已经提供了一个im2col
操作,其将输入特征图转化成一个(B×Ho×Wo,Kh×Kw×C)的矩阵,每一行代表一个卷积窗口。
编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,实现上述全连接层的前向传播。
测试说明
平台会对你编写的代码进行测试,测试方法为:平台会随机产生输入x
,然后根据你的实现代码,创建一个MaxPool
类的实例,然后利用该实例进行前向传播计算,并与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过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]]]]
pool_h: 2
pool_w: 2
stride: 2
pad: 0
则对应的输出神经元为:
[[[[0.7 0.87]
[0.92 0.7]]
[[0.78 0.91]
[0.79 0.84]]]
[[[0.94 0.98]
[0.49 0.89]]
[[0.98 0.86]
[0.94 0.96]]]]
import numpy as np
from utils import im2col
class MaxPool:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
r'''
池化层的初始化
Parameter:
- pool_h: int
- pool_h: int
- stride: int
- pad: int
'''
self.pool_h = pool_h
self.pool_w = pool_w
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 ##########
N,C,H,W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
#展开
col = im2col(x,self.pool_h,self.pool_w,self.stride,self.pad)
col = col.reshape(-1,self.pool_h*self.pool_w)
#最大值
out = np.max(col,axis=1)
#转换
out = out.reshape(N,out_h,out_w,C).transpose(0,3,1,2)
return out
########## End ##########