CNN简介与实现
- 导语
- 整体结构
- 卷积层
- 卷积
- 填充
- 步幅
- 三维卷积
- 立体化
- 批处理
- 实现
- 池化层
- 特点
- 实现
- CNN实现
- 可视化
- 总结
- 参考文献
导语
CNN全称卷积神经网络,可谓声名远扬,被用于生活中的各个领域,也是最好理解的神经网络结构之一。
整体结构
相较于先前的神经网络,CNN出现了卷积层和池化层的概念,基本的组成模块是“卷积-ReLU-池化”,并且,在靠近输出或最后输出时时仍会采用“Affine-ReLU”、"Affine-ReLU"的组合,书上给出的示例图如下:
卷积层
在思考为什么要用卷积层之前,我们可以先来看看卷积层之前的全连接层有什么局限性,全连接层通常要求输入是一个一维的数组,即使原始数据是更高维的数据,如高、长、通道的三维图像,这个时候,使用全连接层,原始数据中的几何信息、点之间的相对位置等空间信息就都被清除了,这些信息其实很重要,因为点与点之间在高维空间的关联性是比一维更强的。
相比之下,卷积层就考虑到了这些空间信息,当输入为图像时,卷积层会以三维数据的形式接受输入数据,并且输出也是三维数据。
CNN中卷积层的输入输出数据被称作特征图,输入叫输入特征图,输出叫输出特征图。
卷积
卷积是卷积层的运算,类似与图像中的滤波器处理,具体做法如图(图源自网络,侵删):
此图省略了卷积核,只给出了输入和结果,以该图为例,输入是一个4×4的矩阵,在矩阵上存在一个3×3的滑动窗口,窗口每次移动一个单位,每次对窗口内的矩阵A进行一次权重累和,具体的权重为同等大小的卷积核矩阵,具体的例子如下, 36 = 1 × 1 + 2 × 1 + 0 × 3 + 4 × 0 + 5 × 2 + 6 × 0 + 7 × 1 + 8 × 2 + 1 × 1 36=1×1+2×1+0×3+4×0+5×2+6×0+7×1+8×2+1×1 36=1×1+2×1+0×3+4×0+5×2+6×0+7×1+8×2+1×1。
与全连接层一样,CNN中也存在偏置,对于算出的结果矩阵,对矩阵中的所有元素可以加上一个相同的偏置值。
填充
在进行卷积前,有时候要把数据拓宽,例如把4×4拓成6×6,如何拓宽呢很简单,把不够的部分都设置为同一个值就可以(一般是0或者1),具体操作如图(图源网络,侵删):
这种做法,就叫做填充,使用填充主要是为了调整输出大小,在使用卷积核运算的时候,如果不进行填充,卷积的结果势必会在整体上变小(如4×4变成2×2),多次使用后,最后的结果就可能只有一个1,因此使用填充来避免这种情况的发生。
步幅
步幅很容易理解,就是滑动窗口的每次的移动距离,像下面这张图,就是步幅为2时候的卷积(图源网络,侵删):
可以看到,增大步幅会使得输出变小,加上填充会变大,这个时候就可以根据两者关系列出卷积输出结果的公式了。
书上的描述如下(值除不尽四舍五入):
三维卷积
在现实使用中,CNN的输入并不是一个单纯的二维矩阵,输入的图像时一个带有高、宽、通道的具体的特征图,以RGB为例,RGB图像是三通道,如果对RGB图像进行卷积,那么就要对图像上的每一个通道都使用一个卷积核,通道方向有多个特征图时,需要按照通道方向进行输入数据和滤波器的卷积运算,并将结果累和,生成一个新的二维矩阵。
立体化
当我们把输入和输出推向更一般的适用情况,多通道输入数据使用对应的多通道核,最后输出一张单个图,书上的例子如下,其中C为通道数、H为高度、W为长度。
如果要再通道方向上也拥有多个卷积运算的输出,就需要使用多个滤波器(权重),书上的图如下:
如果再考虑上偏置,书上给出的图如下:
批处理
通常,为了加快效率,神经网络会将输入数据进行一批批的打包,一次性处理一堆数据,为了处理一批数据,需要在上一张图的基础上加上批次,书上给出的图如下:
数据作为4维数据在各层之间传递,批处理将N次处理汇总成了1次进行。
实现
如果直接实现卷积运算,利用for循环,效率其实是不高的,况且python给出了更好的选择:im2col函数。
im2col将输入数据展开来适合卷积核的计算,书上给出的图如下:
这里更详细的解释一下,输入的是一个三维的数据,把每一面(二维)从左到右,从上到下,拉成一个一维的数组,然后把每个通道的一维数组拼起来,形成一个二维的矩阵,如果是多批次,就把这些矩阵首尾相连,形成一个更大的二维矩阵即可。
实际的卷积运算中,卷积核的应用区域几乎彼此重叠,因此,在使用im2col之后,展开的元素个数会多于原来的输入元素个数,所以会消耗更多的内存。
书上给出了用im2col进行卷积的流程:
还需要明晰的一点是,im2col的使用并不会损失原数据在空间上的信息,它只是为了方便进行矩阵对数据进行了一些处理,并且在最后恢复了原来的数据模式。
书上给出了im2col和基于im2col实现的卷积层代码如下:
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
#输入,高,长,步幅,填充
N, C, H, W = input_data.shape
out_h = (H + 2*pad - filter_h)//stride + 1#根据步长和高度计算输出的长高
out_w = (W + 2*pad - filter_w)//stride + 1
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))#设置一个空的拉伸之后的二维数组
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
return col
Class Convolution:
def __init__(self,W,b,stride=1,pad=0):#初始化赋值
self.W=W
self.b=b
self.stride=stride
self.pad=pad
def forward(self,x):
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)#拉伸
#卷积层反向传播的时候,需要进行im2col的逆处理
col_W=self.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)
#更改轴的顺序,NHWC变成NCHW
return out
def backward(self, dout):
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0,2,3,1).reshape(-1, FN)
self.db = np.sum(dout, axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)#逆运算
return dx
池化层
简单来说,卷积是使用卷积核计算对应区域的乘积和,池化层是选取对应区域的最大值(也有其他的池化,比如平均值池化,指的是取对应区域的平均值作为输出),书上给出的例子如下:
特点
池化层的操作很简单,不需要像卷积层那样学习卷积核的参数,只需要提取最值或平均即可;其次,池化层的计算是按照通道独立进行的,输入和输出的通道数不会变化;最后,池化层对输入数据的微小偏差具有鲁棒性(例如目标区域的非最大值有变化,并不会影响池化层最后的输出)。
实现
池化层也是用im2col展开,但展开时在通道方向上是独立的,书上给的图示如下:
书上的实现代码如下:
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):#初始化
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.x = None
self.arg_max = None
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)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)#拉伸
col = col.reshape(-1, self.pool_h*self.pool_w)#变成二维矩阵
arg_max = np.argmax(col, axis=1)
out = np.max(col, axis=1)#取最值
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)#还原成数据
self.x = x
self.arg_max = arg_max
return out
def backward(self, dout):#反向传播
dout = dout.transpose(0, 2, 3, 1)
pool_size = self.pool_h * self.pool_w
dmax = np.zeros((dout.size, pool_size))
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
dmax = dmax.reshape(dout.shape + (pool_size,))
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
return dx
CNN实现
将已经实现的各个层进行组合,就可以实现一个简单的CNN,书上给出了一个简单CNN的具体代码实现,具体图如下:
书上加上注释的代码如下:
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))
# 初始化权重
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)
self.params['W2'] = weight_init_std * \
np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
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['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
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_layer = 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.last_layer.forward(y, t)
def accuracy(self, x, t, batch_size=100):#计算准确度
if t.ndim != 1 : t = np.argmax(t, axis=1)
acc = 0.0
for i in range(int(x.shape[0] / batch_size)):
tx = x[i*batch_size:(i+1)*batch_size]
tt = t[i*batch_size:(i+1)*batch_size]
y = self.predict(tx)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
def numerical_gradient(self, x, t):#求梯度,用数值微分方法
loss_w = lambda w: self.loss(x, t)
grads = {}
for idx in (1, 2, 3):
grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])
return grads
def gradient(self, x, t):#误差反向传播求梯度
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
训练所需要的时间相较于先前的方法比较久,但是得到的结果识别率更高,具体训练结果如下:
可视化
“卷积”是一种数学运算,逻辑上其实很难理解到它的用处,因此,书上给出了对卷积作用更加直接的展现方式,以上一部分学习前和学习后的卷积核为例,各个卷积核的权重图如下:
学习前:
学习后:
可以明显的看到,学习前杂乱无章的权重矩阵,在学习后变得有迹可循,明显有些区域的权重更深一些,那么,这些权重更大的部分对应的目标究竟是什么呢?
书上给出了答案:这些卷积核在学习边缘(颜色变化的分界线)和斑块(局部的块状区域),例如黑白分界线,可以根据手写数字识别的例子想象,手写的数字是黑色,背景是白色,那么卷积核的目标就是使得模型对黑色的部分更加敏感,权重更大。
上述的结果是只进行了一次卷积得到的,随着层次的加深,提取的信息也会越来越抽象,在深度学习中,最开始层会对简单的边缘有响应,接下来是对纹理,在接下来是对更复杂的性质,随着层次递增,模型的目标会从简单的形状进化到更高级的信息。
总结
本章详细介绍了CNN的构造,对卷积层、池化层进行了从零开始的实现,但是对反向传播的部分只给出了代码实现。最重要的还是对im2col的理解,明白了im2col的原理,卷积层、池化层乃至反向传播的实现,这些问题就迎刃而解了。
基于最基本的CNN,后续还有更多功能强大,网络结构更深的CNN网络,如LeNet(激活函数为sigmod,使用子采样缩小中间数据大小,而不是卷积、池化)还有AlexNet(多个卷积层和池化层,激活函数为sigmod,使用进行局部正规化的LRN层,使用Dropout)等。
参考文献
- 【Pytorch实现】——深入理解im2col(详细图解)
- 12张动图帮你看懂卷积神经网络到底是什么
- 《深度学习入门——基于Python的理论与实现》