卷积神经网络(Convolutional Neural Network, CNN)是一种利用物体边缘的图像识别技术,被广泛应用于图像识别领域,在图像识别比赛中几乎全部的深度学习方法都基于CNN来实现。
整体结构
基于全连接的神经网络
在以往的学习中,我们认识到全连接的神经网络,通常是由Affine(仿射函数,线性层)+ReLU(激活函数,也可替换为Sigmoid,Tanh的等)构成,Affine为模型建立了线性的表达能力,而ReLU则在此基础上增加非线性表达因素,增强了整个模型的表达能力,可以拟合更加复杂的曲线。实际生活中的场景大多是非线性的,因此,非线性特征越多,才能训练出符合各种特征的曲线与模型,提高模型的泛化能力。
最终由Softmax进行输出的正则化处理,得到每个分类的概率。
图:基于全连接层(Affine层)的网络
基于CNN的网络
CNN在此基础上,增加了卷积层(Convolution)和池化层(Pooling),将原始的Affine-ReLU替换成Conv-ReLU-Pooling,在下图中靠近输出的层依旧使用了Affine-ReLU,更前面一层则是Conv-ReLU,没有使用Pooling。
图:基于CNN的网络
卷积层
全连接层存在的问题
以图像识别为例,全连接层将三通道(1,28,28)拉平成一通道,虽然简化了数据形式,但这会导致丢失数据在不同维度上的信息。全连接层忽视了数据形状,将全部输入数据作为相同神经元处理,无法利用与形状相关信息。
卷积层可以保持形状不变,在不同层之间以三维数据形式进行传递,有效利用形状信息。
卷积层的输入输出数据被称为“特征图(featuer map)”。
卷积运算
卷积运算相当于图像处理中的滤波器运算,卷积运算的符号是⊗,其含义是,通过滑动过滤器窗口,将输入元素与滤波器相乘后相加,得到新的位置上的元素。因此卷积运算也可以称作乘积累加运算。
卷积运算也可以包含偏置,偏置通常是1*1的大小,被分别加到全部元素中。
图:卷积运算的计算顺序,
4*4的矩阵经过卷积得到2*2的输出
图:偏置,所有应用了滤波器的元素被加上某个固定值
填充
由上图可知,44的矩阵在卷积后变小(22)了,如果应用多个卷积层,最终会导致输出变成1*1的矩阵,这还怎么玩?因此提出了“填充(padding)”的概念,填充过程有2个参数可以配置,分别是填充大小(指外围增加的行、列数)与填充数值(0、1、2等)。
图:对输入数据进行大小为
1、值为0的填充后,经过卷积运算,能得到与输入维度4*4相同的输出矩阵
步幅
应用滤波器的位置间隔被称为“步幅(stride)”,步幅决定了输出(尺寸)大小,步幅越大,输出越小。
输入、滤波器、输出、填充、步幅 这五个元素之间的关系可以用下式表示:
3维数据的卷积
在三维数据中,第三维被称为“通道数”,输入数据和滤波器的通道数要设置为相同的值,且所有滤波器的大小要设置为相同的值。
运算过程是对每一个通道计算完成后,进行相加,最后得到单通道的数据。
图:对三维数据进行卷积运算
运算得到三维数据
前面讲到,卷积会将三维数据运算后累加,得到二维数据,那么如何保证运算后的数据仍然是三维呢?这里需要用到多个滤波器,假设滤波器的个数是N,则运算后会得到(N, W, H)大小的新数据,如下图所示:
图:应用多个滤波器,输出三维数据,通道数=滤波器个数
池化层
卷积运算中,通过设置Padding(填充)可以扩大输出的尺寸,设置Stride(步幅)则可以缩小输出尺寸。池化层也有类似的作用,可以通过设置池化层大小,来对池内的数据进行最大值、平均值运算。
- Max池化:对池内数据求最大值,作为输出元素,图像识别领域通常使用Max池化
- Average池化:对池内数据求平均值后,作为输出元素
Max池化就是对池内元素求最大值的操作,池的大小通常设置成跟Stride(步幅)相同。
图:Max池化处理顺序
池化层有如下特征:
- 没有要学习的参数:只是求Max/Average,没有参数要学习
- 通道数不发生变化:按通道独立计算,不会增加或减少通道数
- 对微小的位置变化具有鲁棒性:即使输入数据发生微小偏差,池化结果仍然可以保持不变,即池化会吸收输入数据的偏差
根据池化层的特征,可以总结出它有如下作用:
- 降低数据大小:减少计算量
- 平移不变性:对特征相应一致
- 鲁棒性:从而让模型具备泛化能力
卷积层和池化层实现
im2col
im2col是image to column的简称,该函数能够将4维的输入数据降低成2维矩阵。具体地说,是将应用滤波器的区域(3维方块)横向展开为1列。
图:im2col将滤波器应用区域展开为一列
在实际的卷积运算中,步幅较小,滤波器的应用区域都是重叠的,这会导致展开后的元素个数多于原始输入数据中的元素,占据的内存上升。消耗内存是im2col的一个缺点,但这带来了运算速度的大幅提升——在线性代数库中对于矩阵计算已经实现了高度最优化。因此这是一种空间换时间的方式。
经过im2col处理,输入数据被横向展开为1列。同时,将滤波器纵向展开为1列,并且计算这两个矩阵的乘积,就可以得到卷积层的输出结果。
图:卷积运算的滤波器处理
im2col的函数签名如下,参数名都很好理解。
im2col(input_data, filter_h, filter_w, stride=1, pad=0)
以下用通道数为3的7*7数据举例,两组数据的批大小分别是1和10。
import sys, os
sys.path.append(os.pardir)
from common.util import im2col
x1 = np.random.rand(1, 3, 7, 7) # 批大小为1
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape) # (9, 75) # 第二维75个元素(3*5*5,滤波器元素个数总和)
x2 = np.random.rand(10, 3, 7, 7) # 10个数据
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape) # (90, 75)
卷积层实现
具备forward函数的卷积层实现如下:
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
sellf.W = W
self.b = b
self.stride = strinde
self.pad = pad
def forward(self, x):
FN, C, FH, FW = self.W.shape # 初始化滤波器参数
N, C, H, W = x.shape # 输入,C=channel通道数
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 = sef.W.reshape(FN, -1).T # 展开滤波器
out = np.dot(col, col_W) +self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) # transpose调整多维数组轴的顺序
return out
图:transpose按照新顺序对轴进行排序
池化层的实现
MAX池化的实现相对简单,对每一行求最大值即可。
图:对输入数据展开池化
# 池化层实现
class Pooling:
def__init__(self, pool_h, pool_w, stride=1, pad=0):
sef.pool_h = pool_h
self.pooll_w = pool_w
self.stride = stride
self.pad = pad
def forward(self, x):
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)
# 1 展开
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h * self.pool_w)
# 2 最大值
out = np.max(col, axis=1) # 对指定轴求max
# 3 转换
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out
CNN实现
以一个名为SimpleConvNet的简单卷积网络为例,它的组成是Conv-ReLU-Pooling--Affine-ReLU--Affine-Softmax。
类的初始化部分,保存卷积的各项参数。
class SimpleConvNet:
def __init__(self, input_dim=(1, 28, 28), conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
hidden_size=100, output_size=10, weight_init_std=0.01):
filter_num = conv_param['filter_num']
filter_size = conv_param['filter_size']
filter_pad = conv_param['pad']
filter_stride = conv_param['stride']
input_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))
第二步,根据权重标准差以及各层的大小,进行权重参数初始化,所有权重都保存在params变量中
self.params = {}
# 卷积层
self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
# 全连接1
self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
# 全连接2
self.params['W3'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
最后生成必要的层
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'], conv_param['stride'], conv_param['pad'])
self.layers['Relu1'] = Relu()
self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
self.layers['Relu2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])
self.last_year = softmaxwithloss()
推理和计算损失函数
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x)
return self.lastLayer.forward(y, t)
以上是正向传播以及计算损失的实现方法,最后来关注下反向传播的求梯度逻辑
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse() # 反序
grads = {}
grads['W1'] = self.layers['Conv1'].dW
grads['b1'] = self.layers['Conv1'].db
grads['W2'] = self.layers['Affine1'].dW
grads['b2'] = self.layers['Affine1'].db
grads['W3'] = self.layers['Affine2'].dW
grads['b3'] = self.layers['Affine2'].db
return grads
CNN可视化
经过学习,滤波器掌握到了图像中的物体边缘信息,反映到滤波器上就是横向&纵向的滤波器。
随着层次加深,卷积层所提取到的信息越来越抽象。
CNN经典模型:LeNet和AlexNet
LeNet
1998年被提出,用于手写数字识别,和当代CNN相比,LeNet有如下不同点:
- 使用
sigmoid作为激活函数 - 使用子采样(
subsampling)缩小中间数据的大小
AlexNet
它具有如下特点:
- 激活函数使用
ReLU - 使用进行局部正规化的
LRN(Local Response Normalization)层 - 使用
Dropout
数据规模的增大以及GPU运算速度提升,已经成为深度学习发展的两项巨大动力。