目录
概述
卷积神经网络(Convolutional Neural Network),简称CNN,CNN常用于图像识别、语音识别等场合。CNN同前面专题介绍的神经网络一样,可以像乐高积木一样通过组装层来构建。CNN新出现了卷积层(Convolution)和池化层(Pooling)。
前面介绍的多层神经网络,其相邻的所有神经元都有连接,这称为全连接,而我们使用了Affine层来实现全连接层。现在我们均以5层神经网络来观察一下普通的神经网络和基于CNN的神经网络的差别。图1为基于Affine层的神经网络,图2为基于CNN的神经网络。
可见,CNN中新增了Convolution层和Pooling层,一般连接顺序为Convolution-ReLU-Pooling(池化层可省略)。而靠近输出的层中依然使用了之前的Affine-ReLU层,此外,在最后的输出层中使用了Affine-Softmax组合。
卷积层
卷积层的产生
卷积层因全连接层(Affine)的缺陷而产生。使用Affine层时,数据的形状被“忽视”了,以mnist数据集为例,其图像数据为1通道、高28像素、长28像素的(1,28,28)形状,但在实现Affine层时被转换成1列(1维),以784个数据的形式输入到Affine层。显然,这样就忽视或漏掉了3维形状的图像数据所隐藏的潜在价值信息。
图像数据,其形状中应该包含重要的信息,比如,空间上相邻的像素为相似的值、RGB的各个通道之间分别有密切的关联性等。
卷积层的出现弥补了Affine层的缺陷,卷积层可以保持形状不变。当输入数据是图像时,卷积层会以3维数据的形式接收输入数据,并同样以3维数据的形式输出至下一层。因此CNN可以理解具有形状的数据。
卷积运算
在卷积层中进行的运算,我们称之为卷积运算,其实,卷积运算就相当于图像处理中的“滤波器运算”,我们来看一个具体的卷积运算的例子,如图3所示:
输入数据是具长高形状的二维数据,大小为(4,4);滤波器也是具有长高形状的二维数据,大小为(3,3),输出数据大小为(2,2)。具体的运算过程为:以一定间隔滑动滤波器的窗口并应用到输入数据中,本示例中的窗口为图4中灰色的部分(其大小和滤波器 一样大)。首先,将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(乘积累加运算),接着将这个结果保存到输出的对应位置。最后需要将这个过程在所有位置都进行一遍,就可以得到卷积运算的输出。输出的大小我们将在稍后介绍。
在CNN中,滤波器的参数就对应神经网络中的权重参数,当然,CNN中也存在偏置,如图5所示。
如图5所示,将输入数据和滤波器(权重)进行的乘积累加运算后,得到的结果中的每个元素都加上了偏置。最后得到输出数据。
填充
所谓填充,就是在输入数据周围填入固定的数据,填充是卷积运算中经常用到的处理。如图6所示,我们对大小(4,4)的输入数据进行了幅度为1的填充(指的是幅度为1像素的0填充数据)。可见,通过幅度为1的填充,大小(4,4)变成了(6,6)的形状,然后应用大小为(3,3)的滤波器,最后生成了大小(4,4)的输出数据。当然,也可以将填充的幅度设为2、3等等。
填充的主要目的是调整输出的大小,防止在进行卷积运算时,输出数据的大小变为1,而导致卷积运算无法再进行下去。因此,填充可以帮助卷积运算在保持空间大小不变的情况下将数据传给下一层。
步幅
所谓步幅,指的是滤波器在输入数据中的移动位置间隔。在之前的例子中的步幅都是1,假如将步幅设为2,则滤波器在输入数据中的移动间隔就变为2,如图7所示。
应用滤波器后的输出数据大小计算
通过上面的分析可知,增大填充可增加输出数据的大小,增大步幅可减小输出数据的大小。实际上,我们可以通过输入数据大小、填充和步幅信息计算出输出数据的大小。假设输入大小为(H,W),滤波器大小为(FH,FW),输出大小为(OH,OW),填充为P,步幅为S。则输出大小可通过公式(1)计算:
(1)
以图6来验证,输入大小(4,4);填充为1;步幅为1,;滤波器大小(3,3)则输出大小计算得(4,4)
在一些深度学习框架中,如果公式(1)除不尽而有小数时,一般采用报错或按照四舍五入方法进行处理。
三维数据的卷积运算
前面介绍的卷积运算都是基于2维形状(高和长)进行的。实际上,像图形数据一般都是三维数据(高、长、通道)。直观地,我们可以把通道视为二维图沿着纵方向(厚度)扩展。这里的通道相当于一叠很厚的纸,每一张纸就是一张具有高和长的二维图。图8是对3维数据进行卷积运算的例子,该输入数据的通道为3、高度为4、长度为4。滤波器的通道为3、高度为3、长度为3。具体的计算过程为:通道方向的所有特征图都进行输入数据和滤波器的卷积运算,并将计算结果相加,从而得到输出。
需要指出,输入数据和滤波器的通道数要设为相同。此外,输出数据的通道数等于滤波器的个数,这里的滤波器只有一个,因此输出数据的通道数也只有一个(相当于一张纸)。
如果有多个滤波器(权重),并且把偏置也考虑进去,那么卷积运算的处理如图9所示,为了简化作图,我们把通道数记为C,高度记为H,长度记为W,滤波器个数记为FN。
这里,需要强调的是,每个通道只有一个偏置,其形状为(FN,1,1)。当滤波器(权重)的输出和偏置相加时,要按通道分别加上相同的偏置数值。
批处理(‘四维数据’)
前面介绍的输入数据都是单个数据(可以理解为一个样本),在实际应用中,我们会遇到多个数据样本,以批的概念来理解就是,一批数据会有多个样本。这样就引入四维数据:(一批数据的样本个数,通道数,单个数据样本的高度,单个数据样本的长度)。批处理数据的卷积运算的流程如图10所示。
从图10可知,在进行批处理卷积运算时,在输入数据的开头添加了批处理的样本大小N,这样在网络间传递的便是四维数据,也就是说批处理将N个样本需要N次处理汇总成了只需1次完成处理。
池化层
卷积层的作用主要为保持形状不变,在神经网络中进行传播。池化层则是对二维(高、长)数据进行缩小运算。如图11所示,对(4,4)形状的数据按的区域集约成1个数据的处理,从而达到缩小空间及数据的目的。
从图11可知,我们选择了步幅为2对数据进行目标区域为的Max池化,“Max池化”就是获得区域内的最大值。除了Max池化,还有Average池化(计算目标区域内的平均值)。一般而言,池化的目标区域大小和步幅相同,本例中区域大小为的步幅为2。
可见,池化层和卷积层有着以下明显区别:
- 不涉及权重参数,由于池化层只是从目标区域内取出最大值或平均值,因此没有学习的参数。
- 经过池化层运算后的数据,其通道数不会改变,因为池化层是按通道独立进行处理的。
- 输入数据位置发生微小变化时,池化层的输出依然会返回相同结果,如图12所示。也就是说池化层对输入数据的微小偏差具有鲁棒性。
卷积层和池化层的Python实现
大家可能觉得卷积层和池化层的实现很复杂,实际上,我们巧妙地运用Python及numpy就能轻松实现卷积层和池化层。比如,我们来构造一个CNN中各层间传递的四维数据(数组):
impor numpy as np
x=np.random.rand(5,1,20,20) #10个通道为1,长为20,高为20的数据
x[0] #访问第一个数据
x[0].shape #第一个数据的形状(1,20,20)
x[1].shape #第二个数据的形状(1,20,20)
im2col介绍
im2col是“image to column”的缩写,意思就是“图像到矩阵”的意思。这是一个用来处理输入数据的函数,将多维数据展开为列,以适合滤波器处理,如图13所示。很多深度学习框架里都有im2col函数。
对三维数据(准确地讲是包含了批量的四维数据)应用im2col后,数据转换为二维矩阵。
具体地讲,对于一个输入数据,将应用滤波器的三维区域横向展开为1列。当然,im2col会在所有应用滤波器的地方进行这个展开处理。为了便于理解,我们把步幅设置大些,便于滤波器的应用区域不重叠。如图14所示,将需要应用滤波器的输入数据进行im2col展开的过程(切记,im2col展开后的数据和原始输入数据不等效的,因为滤波器应用的有些区域是重叠的,因此展开后的数据个数会增多)。
对输入数据进行了im2col横向展开后,对滤波器进行纵向展开(一个滤波器展开为1列,n个滤波器展开后有n列),然后将展开后的两个矩阵进行乘积即可(这和Affine层的计算一样)。由于im2col处理后的输出是2维矩阵,因此最后要转换为适合CNN中的四维数组。im2col处理的具体细节如图15所示。
im2col的Python实现
# coding: utf-8
import numpy as np
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
"""
Parameters
----------
input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据
filter_h : 滤波器的高
filter_w : 滤波器的长
stride : 步幅
pad : 填充
Returns
-------
col : 2维数组
"""
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
im2col函数会接收一个四维数组构成的输入数据,现在我们来实际测试一下该函数对四维数组的展开结果。
data1=np.random.rand(1,3,7,7) #批大小为1、通道为3的7x7的数据
col1=im2col(data1,5,5,stride=1,pad=0)
print(col1.shape) #(9,75)
data2=np.random.rand(10,3,7,7) #批量为1、通道为3、高为7、宽为7的随机数据
col2=im2col(data2,5,5,stride=1,pad=0)
print(col2.shape) #(90,75)
卷积层的实现
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
# 中间数据(backward时使用)
self.x = None
self.col = None
self.col_W = None
# 权重和偏置参数的梯度
self.dW = None
self.db = None
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
out_w = 1 + int((W + 2*self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
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)
self.x = x
self.col = col
self.col_W = col_W
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展开后的结果如图16所示。
从上图可知,池化层的实现按下面3个阶段处理:
1.展开输入数据;
2.求各行的最大值;
3.转换为合适的输出大小。
Python实现如下:
import numpy as np
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的实现
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import * #可在之前的专题中找到实现过程
from common.gradient import numerical_gradient #可在之前的专题中找到实现过程
class SimpleConvNet:
"""简单的ConvNet
conv - relu - pool - affine - relu - affine - softmax
Parameters
----------
input_size : 输入大小(MNIST的情况下为784)
hidden_size_list : 隐藏层的神经元数量的列表(e.g. [100, 100, 100])
output_size : 输出大小(MNIST的情况下为10)
activation : 'relu' or 'sigmoid'
weight_init_std : 指定权重的标准差(e.g. 0.01)
指定'relu'或'he'的情况下设定“He的初始值”
指定'sigmoid'或'xavier'的情况下设定“Xavier的初始值”
"""
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):
"""求损失函数
参数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):
"""求梯度(数值微分)
Parameters
----------
x : 输入数据
t : 教师标签
Returns
-------
具有各层的梯度的字典变量
grads['W1']、grads['W2']、...是各层的权重
grads['b1']、grads['b2']、...是各层的偏置
"""
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):
"""求梯度(误差反向传播法)
Parameters
----------
x : 输入数据
t : 教师标签
Returns
-------
具有各层的梯度的字典变量
grads['W1']、grads['W2']、...是各层的权重
grads['b1']、grads['b2']、...是各层的偏置
"""
# 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
def save_params(self, file_name="params.pkl"):
params = {}
for key, val in self.params.items():
params[key] = val
with open(file_name, 'wb') as f:
pickle.dump(params, f)
def load_params(self, file_name="params.pkl"):
with open(file_name, 'rb') as f:
params = pickle.load(f)
for key, val in params.items():
self.params[key] = val
for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']):
self.layers[key].W = self.params['W' + str(i+1)]
self.layers[key].b = self.params['b' + str(i+1)]
在这里,我们介绍了典型的CNN网络,除此之外,还有包括LeNet、AlexNet等重要的CNN网络,读者可自己去了解。欢迎关注微信公众号“Python生态智联”,学知识,享生活!