前言
发展卷积神经网络的初衷是进行图像分类。图像主要有如下3个特性
- 多层次结构:如一张人脸由鼻子、嘴巴、眼睛等组成
- 特征局部性:如眼睛在一张图片中就局限在一个小区域内
- 平移不变性:如不管眼睛出现在图片的哪个位置,特征提取器都可以找出来
虽然卷积网络是为图像分类而发展起来的,但现在已经被用在各种任务中,如语音识别和机器翻译等。只要信号满足多层次结构、特征局部性和平移不变性3个特性,都可以使用卷积网络。
卷积网络的特性:
- 深度网络
- 参数共享
- 局部连接
在卷积神经网络中,主要包含3种基本模块:卷积层、池化层和全连接层。下面介绍卷积层。
- 局部连接和参数共享
局部连接和参数共享是卷积网络的核心概念。这显著区别于常规神经网络,因为常规神经网络采用全连接并且参数各不相同。输入和输出3D特征图中的每个元素称为神经元。你会发现:输出神经元只观察输入神经元中的一小部分,即空间尺度上只观察卷积核内的神经元,这就是局部连接,卷积核的空间大小也叫感受野。同一特征图的所有神经元使用相同的卷积核扫描输入3D特征图,即参数共享。
局部连接
例子1: CIFAR-10图像的输入特征图的尺寸为32×32×3,如果卷积核尺寸是5×5,那么卷积层中每个神经元连接输入特征图中5×5×3的局部区域,共5×5×3=75个权重。
例子2: 输入特征图的尺寸是14×14×128,如果卷积核尺寸是1×1,那么卷积层中每个神经元和输入特征图有1×1×128=128个连接。
例子3: 输入特征图的尺寸是7×7×256,卷积核尺寸是7×7,那么卷积层中每个神经元和输入特征图有7×7×256=12544个连接。
从上面三个例子可以发现,局部连接有时候也可以变成全连接。换句话说,全连接是特殊的局部连接。
对于例子2而言,卷积核尺寸为1,对于二维信号,1×1卷积没有任何意义,卷积网络特征图是三维,虽然空间维度上1×1卷积没有意义,但深度方向卷积有意义。比如,输入是彩色图像32×32×3,那么1×1卷积就是进行三维点积。这个卷积在图像处理中有重要应用,比如彩色图像转换为灰度图像就是1×1卷积。
换一个角度看,也可以把局部连接看成全连接,只是把卷积核窗口外的权重参数都设置为0。
参数共享
同一特征图的所有神经元使用相同的卷积核扫描输入3D特征图,即参数共享,能极大减小参数的数量。
CIFAR-10图像的输入特征图尺寸为32×32×3,如果卷积核尺寸是5×5,那么卷积层中每个神经元连接输入特征图中5×5×3的局部区域,共5×5×3=75个权重。如果采用步长S=1,则输出有32×32=1024个神经元,如果每个神经元参数都不同,则总共需要75×1024=76800个权重,仅一个特征图就需要这么多参数,而输出会有多个特征图,因此参数的数目是非常大的。
优点:参数共享使特征图上的每个神经元都使用相同的权重。在上面的例子中,一个特征图只需75个权重,极大地减小了参数的数量。由于采用参数共享原则,卷积网络获得了良好的平移不变性,即图像中如果有人脸,不管人脸在图像的什么位置,卷积网络都能一致地识别出来。
缺点:相邻神经元信息高度冗余,因为它们所观察的输入局部窗口是相邻的。图像有个特点,那就是相邻像素的值比较接近,特别是在平滑区域,所以这些局部窗口的像素很相似而神经元采用相同的权重,内积计算出的激活值几乎相等,这样相邻神经元信息几乎相同,只能提供十分有限的额外信息。神经元信息冗余对于网络前几层特别明显,后面层由于经过多次非线性激活,冗余不太明显。
卷积层
卷积网络采用卷积层来实现上述的局部连接和参数共享,所以卷积层是卷积网络的核心,而卷积层的核心是卷积运算。
卷积运算
卷积网络的卷积运算和信号处理中的卷积运算不太一样,把它理解为向量内积更合适(神经元的工作机制)。在图像处理中,边缘检测算法就是利用卷积运算实现的。卷积运算是线性滤波,对于图像中的每个像素,计算以该像素为中心的局部窗口内的像素和卷积核的内积,并将其作为该像素的新值。遍历图像中的每个像素,进行上述内积操作,就完成了一次滤波,得到一个和原图像尺寸一样的“新图像”。局部窗口和卷积核的大小一样,卷积核是一个小矩阵(3×3或5×5),卷积运算公式为:
其中 (i, j) 是中心像素的坐标, g是“新图像”, h是卷积核(这里卷积核的大小是3× 3)。
- 问题
当对图像边界进行卷积时,卷积核的一部分位于图像外面,无像素与之相乘,该如何?
- 舍弃图像边缘
- 像素填充
- 0填充 :常用
- 边缘复制
卷积运算过程——二维
输入特征图的尺寸为4×4,采用0填充后尺寸为6×6,卷积核大小为3×3,步长为1,则输出特征图的尺寸为4×4,与输入特征图尺寸一致。
在卷积运算中,卷积核的取值是核心,取值不同时,“新图像”的效果差别很大。通过卷积运算可以获得原图像的边缘、模糊图像和锐化图像等。
卷积运算代码实现——二维
数据使用上图的输入特征图,可对比结果看看是否正确。
import numpy as np
def cov2D(input_2Ddata,kern):
h,w = input_2Ddata.shape #输入数据的高度、宽度
kern_h,kern_w = kern.shape #卷积核大小
padding_h = (kern_h-1) // 2 #对原数据进行0填充,需要扩充的高度
padding_w = (kern_w-1) // 2 #对原数据进行0填充,需要扩充的宽度
padding = np.zeros((h+2*padding_h,w+2*padding_w))
padding[padding_h:-padding_h,padding_w:-padding_w] = input_2Ddata
output_2Ddata = np.zeros((h,w)) #输出数据大小和原数据大小一样
for i in range(h):
for j in range(w):
window = padding[i:i+kern_h,j:j+kern_w] #局部窗口
output_2Ddata[i,j] = np.sum(np.multiply(window,kern))
return output_2Ddata
input_2Ddata = np.array([[2,0,1,1],[1,2,0,3],[0,3,1,2],[2,2,0,1]]) # 4x4
kern = np.array([[-0.2,0.1,-0.1],[-0.1,-0.1,0.2],[0.3,0.2,0.1]]) # 3x3
res = cov2D(input_2Ddata,kern)
np.set_printoptions(formatter={'float': '{: 0.2f}'.format})
print(res)
[[ 0.20 0.70 1.00 0.40]
[ 0.80 -0.10 1.70 0.30]
[ 1.10 0.90 0.00 0.20]
[-0.10 -0.20 -0.70 -0.10]]
卷积层及代码实现——三维
- 3D特征图
一张正常的图像,基本上都是三维矩阵表示在数据上,即 [H×W×D],其中H是高度,W是宽度,D是深度。3D特征图可以看作D个2D数据,每个2D数据的尺寸均是 [H×W],称为特征图,3D特征图总共有 D个特征图。
- 大致卷积过程
- 每个特征图都分别与一个卷积核进行卷积运算,这样就得到D个特征图;
- D个特征图先进行矩阵相加,得到一个特征图;
- 再给该特征图的每个元素再加一个相同的偏置,最终得到一个新的特征图;
- 重复步骤1、2、3
代码实现——3D
卷积层操作用数学公式表示为:
其中步长为S,卷积核尺寸F,d1是原数据深度
h = 32 #输入数据的高度
w = 48 #输入数据的宽度度
d = 12 #输入数据的深度
out_d = 24 #输出数据的深度
kern_h = 3 #卷积核高度
kern_w = 3 #卷积核宽度
input_3Ddata = np.random.randn(h,w,d) #输入数据 32x48x12
output_3Ddata = np.zeros(h,w,out_d) #输出数据 32x48x24
kerns = np.random.randn(out_d,kern_h,kern_w,d) #卷积核组 24x3x3x12 表示一共有24个卷积核,因为生成的数据深度为24,每个卷积核由12个3x3的小卷积核组成
bias = np.random.randn(out_d) # 偏置
for m in range(out_d):
for n in range(d):
input_2Ddata = input_3Ddata[:,:,n] # 将原3D数据拆成d个2D数据
kern = kerns[m,:,:,n]
output_3Ddata[:,:,m] += cov2D(input_2Ddata,kern)
output_3Ddata[:,:,m] += bias[m]
卷积层运算需要的参数量如下:
- 卷积核四维矩阵:out_d×kern_h×kern_w×in_d
- 偏置向量:out_d
增加步长的卷积运算
1、假设原数据2D尺寸为 h1 Xw1
2、步长设为 S
3、卷积核尺寸为 F
由上述三个数据可算到下面数据:
1、填充尺寸 P = (F - 1 ) // 2
2、输出数据高度 h2 = (h1 - F + 2P)// S + 1
3、输出数据宽度 w2 = (w1 - F + 2P)// S + 1
def cov2D(input_2Ddata,kern,in_size,out_size,kern_size = 3,stride = 1):
h1,w1 = in_size
h2,w2 = out_size
out_2Ddata = np.zeros(out_size)
for i2,i1 in zip(range(h2),range(0,h1,stride)):
for j2.j1 in zip(range(w2),range(0,w1,stride)):
window = input_2Ddata[i1:i1+kern_size,j1:j1+kern_size]
out_2Ddata[i2,j2] = np.sum(np.multiply(window,kern))
return out_2Ddata
h = 32 #输入数据的高度
w = 48 #输入数据的宽度度
d = 12 #输入数据的深度
input_3Ddata = np.random.randn(h,w,d) #输入数据 32x48x12
# 超参数
S = 2 #步长
F = 3 #卷积核尺寸
d2 = 24 #输出数据的深度
# 计算填充尺寸、输出数据的尺寸
P = (F-1) // 2
h2 = (h - F + 2*P)// S + 1
w2 = (w - F + 2*P)// S + 1
####
padding = np.zeros((h+2*P,w+2*P,d))
padding[P:-P,P:-P,:] = input_3Ddata
output_3Ddata = np.zeros((h2,w2,d2))
kerns = np.random.randn(d2,F,F,d) # 4D卷积核
bias = np.random.randn(d2) # 偏置
for m in range(d2):
for n in range(d):
input_2Ddata = input_3Ddata[:,:,n] # 将原3D数据拆成d个2D数据
kern = kerns[m,:,:,n]
output_3Ddata[:,:,m] += cov2D(input_2Ddata,kern,(h,w),(h2,w2),kern_size = F,stride = S)
output_3Ddata[:,:,m] += bias[m]