目录
1. 整体结构
2. 卷积层
全连接层存在的问题
卷积运算
3维数据的卷积运算
批处理
3. 池化层
4. 卷积层和池化层的实现
卷积层
池化层的实现
5. CNN的实现
6. CNN的可视化
第1层权重的可视化
基于分层结构的信息提取
7. 具有代表性的CNN
LeNet
AlexNet
CNN被用于图像识别、语音识别等各种场景
1. 整体结构
- CNN通过组装层来构建,新出现了卷积层(Convolution层)和池化层(Pooling层)
- 全连接(fully-connected):相邻层的所有神经元之间都有连接,用Affine层实现
- 基于全连接层(Affine层):Affine - ReLU(Sigmoid)
- 基于CNN网络:Convolution - ReLU - Pooling
2. 卷积层
全连接层存在的问题
- 在全连接层中,相邻的神经元全部连接在一起,输出的数量可以任意决定
- 向全连接层输入时,会将3维数据拉平为1维数据,因此数据的形状会被“忽视”
- 卷积层可以保持形状不变,会以3维数据的形式接收输入数据,并同样传输至下一层
- CNN中,卷积层的输入数据称为输入特征图,输出为输出特征图,统称为特征图
卷积运算
卷积层进行的处理就是卷积运算,相当于图像处理中的“滤波器运算”
- 对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用
- 偏置:向应用了滤波器的元素加上某个固定值
- 填充:在进行卷积层的处理之前,向输入数据的周围填入固定的数据(比如0),为了调整输出的大小,避免反复进行卷积运算时输出不断缩小到1,导致无法进行卷积运算
- 步幅:应用滤波器的位置间隔
3维数据的卷积运算
- 和2维数据相比,纵深方向(通道方向)上增加了特征图
- 通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出
- 滤波器的通道数只能设定为和输入数据的通道数相同的值
- 结合方块思考
- 3维数据:(channel,height,width)(通道数,高度,长度)
- 滤波器
- 1个滤波器:(channel,height,width),数据输出是1张特征图
- n个滤波器:(output_channel,input_channel,height,width)(滤波器个数,通道数,高度,长度),数据输出是n张特征图
批处理
- 需要将在各层间传递的数据保存为4维数据,(batch_num,channel,height,width)
3. 池化层
池化层是缩小高、长方向上的空间的运算,在图像识别领域,主要使用Max池化,取出目标区域的最大值,此外还有Average池化等。一般来说,池化的窗口大小会和步幅设定成相同的值
- 特征:
- 没有要学习的参数
- 通道数不发生变化
- 对微小的位置变化具有健壮性
4. 卷积层和池化层的实现
- NumPy中存在使用for语句后处理变慢的缺点。因此卷积运算我们不适用for语句,而是使用im2col这个函数实现
- im2col函数(image to column)可以将输入数据展开以适合滤波器(权重)。会在所有应用滤波器的地方进行这个展开处理
- 函数的原理和实现可以参考im2col的原理和实现_dwyane12138的博客-CSDN博客_im2col
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
# 用于反向传播的逆处理
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
"""
Parameters
----------
col :
input_shape : 输入数据的形状(例:(10, 1, 28, 28))
filter_h :
filter_w
stride
pad
Returns
-------
"""
N, C, H, W = input_shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
return img[:, :, pad:H + pad, pad:W + pad]
- 使用im2col展开输入数据后,再将卷积层的滤波器纵向展开为1列,并计算2个矩阵的乘积即可
卷积层
- 先举个例子!
x1 = np.random.rand(1, 3, 7, 7)
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape) # (9,75)
x2 = np.random.rand(10, 3, 7, 7)
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape) # (90,75)
- 第一个是批大小为1,通道为3的7 x 7的数据,第二个是批大小改为10
- 第2维的元素个数均为75,是滤波器的元素个数的总和
- 实现
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):
# Filter Number(滤波器数量),Channel,Filter Height,Filter Width
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)
# 用im2col函数展开输入数据,用reshape将滤波器展开为2维数组
# 这里用了reshape(FN, -1)表示有FN行,然后-1会自动计算有几列
col = im2col(x, FH, FW, self.strde, self.pad)
col_W = self.W.reshape(FN, -1).T
out = np.dot(col, col_W) + self.b
# transpose会更改多维数组的轴的顺序
# 将原来的形状(N, H, W, C) 还原为 (N, C, H, W)
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
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)
# im2col的逆处理->col2im
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
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)
# 最大值
out = np.max(col, axis=1)
# 转换
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
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
5. CNN的实现
这里我们搭建进行手写数字识别的CNN
- 网络构成:Convolution - ReLU - Pooling - Affine - ReLU - Affine - Softmax
- 初始化部分:
class SimpleConvNet:
def __init__(self, input_dim=(1, 28, 28), conv_param=None,
hidden_size=100, output_size=10, weight_init_std=0.01):
"""
:param input_dim:输入数据的维度:(通道,高,长)
:param conv_param:卷积层的超参数(字典)。字典的关键字如下:
filter_num - 滤波器数量;filter_size - 滤波器大小
stride - 步幅;pad - 填充
:param hidden_size:隐藏层(全连接)的神经元数量
:param output_size:输出层(全连接)的神经元数量
:param weight_init_std:初始化时权重的标准差
"""
# 将超参数从字典中取出来,计算卷积层的输出大小
if conv_param is None:
conv_param = {'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1}
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))
# 权重参数的初始化
# 包括第1层的卷积层和剩余两个全连接层的权重和偏置
self.params = {'W1': weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size),
'b1': np.zeros(filter_num),
'W2': weight_init_std * np.random.randn(pool_output_size, hidden_size),
'b2': np.zeros(hidden_size),
'W3': weight_init_std * np.random.randn(hidden_size, output_size),
'b3': np.zeros(output_size)}
- 推理和求损失函数值:
# 推理
def predict(self, x):
for layer in self.layers.values():
x = layer.forward()
return x
# 求损失函数值
def loss(self, x, t):
y = self.predict(x)
return self.last_layer.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()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {'W1': self.layers['Conv1'].dW, 'b1': self.layers['Conv1'].db,
'W2': self.layers['Affine1'].dW, 'b2': self.layers['Affine1'].db,
'W3': self.layers['Affine2'].dW, 'b3': self.layers['Affine2'].db}
return grads
6. CNN的可视化
第1层权重的可视化
- 上述对MNIST数据集的学习中,第1层的卷积层的权重形状是(30,1,5,5),意味着滤波器可以可视化为1通道的灰度图像
- 可以发现,学习前的滤波器是随即进行初始化的,学习后的滤波器变成了有规律的图像,比如从白变到黑的滤波器、含有块状区域(blob)的滤波器等
- 有规律的滤波器在“观察”边缘(颜色变化的分界线)和斑块(局部的块状区域)等
- 卷积层的滤波器会提取边缘或斑块等原始信息,而实现的CNN会将这些原始信息传递给后面的层
基于分层结构的信息提取
- 第1层的卷积层中提取了边缘或斑块等“低级”信息,但如果堆叠了多层卷积层,随着层次加深,提取的信息也愈加复杂、抽象。最开始的层对简单的边缘有响应,接下来的层对纹理有响应,再后面的层对更加复杂的物体部件有响应
- 下一节会介绍堆叠了多层卷积层和池化层最后经过全连接层输出结果的AlexNet
7. 具有代表性的CNN
LeNet
是进行手写数字识别的网络
AlexNet