一、什么是卷积神经网络(CNN)
CNN(卷积神经网络)是一种专门用于处理图像的神经网络,尤其在图像识别任务中表现出色,例如识别手写数字。它通过多层结构逐步提取图像特征,最终输出识别结果。与传统神经网络类似,CNN也可以像乐高积木一样灵活组装。不过,CNN在结构上引入了卷积层(convolution)和池化层(pooling),这两种新结构使其在处理图像数据时更具优势。
传统神经网络的Affine层在处理图像时存在一个关键问题:数据维度过高。图像通常以三维数据的形式存储,其中包含了像素值和颜色信息。然而,传统神经网络会将图像的每个像素点简单地视为一个独立的神经元输入,忽略了图像的空间结构和颜色信息,导致数据维度膨胀,计算复杂度增加。
CNN通过卷积层巧妙地解决了这一问题。卷积层能够提取图像的局部特征,同时保留图像的空间结构信息,从而实现数据的有效降维。此外,CNN的输入和输出数据仍然保持其三维结构,这种数据被称为输入特征图和输出特征图。这种设计使得CNN在处理图像时更加高效,能够更好地捕捉图像的内在特征,从而在图像识别等任务中表现出色。
二、CNN的结构
CNN主要由以下几层组成:
输入层:接收图像数据,将图像转换为二维矩阵(如灰度图像)。
卷积层:通过卷积核在图像上滑动,提取图像的特征,生成特征图。
池化层:对特征图进行降维,提取重要特征,减少计算量,防止过拟合。
全连接层:将特征图展平为一维向量,进行综合计算,输出概率。
输出层:根据全连接层的结果,输出最终的识别结果(如数字的概率)。
卷积层
卷积层直接解决了传统神经网络Affine层的关键问题——数据降维。在计算机中,图像通常以三维数据的形式存储,包含了像素值和颜色信息。然而,传统神经网络在处理图像时,会将每个单元格简单地视为一个独立的神经元输入,忽略了图像的空间结构和颜色信息,导致数据维度大幅增加,计算复杂度也随之上升。
CNN的卷积层通过卷积运算来处理图像数据。卷积运算类似于图像处理中的滤波器运算,用于提取图像的局部特征。在一些资料中,滤波器也被称为核。卷积运算的过程可以理解为乘积累加运算,即通过核与图像局部区域的逐点相乘并求和,从而生成新的特征图。
填充
在卷积层的处理过程中,有时需要对输入数据的边缘进行填充,这种操作称为填充(padding)。填充的主要目的是控制输出特征图的大小。如果不进行填充,随着卷积运算的多次进行,输出特征图的尺寸会逐渐减小,最终可能变为1×1,导致后续无法继续应用卷积运算。因此,填充操作可以有效避免这一问题,使卷积层能够在保持图像特征的同时,灵活地调整输出尺寸,从而更好地适应复杂的图像处理任务。
步幅
在卷积运算中,滤波器的移动间隔被称为步幅(stride)。如果将步幅设置为2,那么滤波器每次移动时的间隔就会变为2个元素。
步幅,输入和输出的大小关系如下:
假设输入大小为(H,W),滤波器大小为(FH,FW),输出大小为(OH,OW),填充为P,步幅为S,此时,输出大小有如下计算:
数据存储
神经网络当中进行了将数据打包的批处理,为了让卷积神经网络也进行一样的操作,我们需要按照(batch_num,channel,height,width)的顺序保存数据,这样,原本对N个数据进行的N次操作就变成了对N个数据进行一次的批处理,这样做的优点在于机器可以把大量数据用于计算而非数据传输上面
池化层
池化层是缩小高、长方向上的空间的运算,通过如上图方式,将2*2的区域集约成一个元素来处理,以此缩小空间大小
池化层特征
池化层有很多种类,像Max池化层,Average池化层
池化层只需要输入,而没有需要学习的参数,并且经过池化层后输出和输入数据的通道数不会发生改变,计算按照通道独立进行,除此之外,池化层对于微小变化具有健壮的鲁棒性,输出结果不易随误差而大幅变动
python实现
im2col
为了更好的实现卷积运算(多次嵌套for循环太麻烦了),我们使用im2col函数,im2col这一便捷函数具有以下接口。
im2col(input_data,filter_h,filter_w,stride=1,pad=0)
● input_data —— 由(数据量,通道,高,长)的4维数组构成的输入数据 ● filter_h —— 滤波器的高 ●
filter_w —— 滤波器的长 ● stride —— 步幅 ● pad —— 填充
卷积层的实现
import numpy as np
def im2col(x, FH, FW, stride, pad):
"""
将输入数据转换为适合卷积操作的矩阵形式。
参数:
x -- 输入数据,形状为 (N, C, H, W)
FH -- 滤波器的高度
FW -- 滤波器的宽度
stride -- 步幅
pad -- 填充
返回:
col -- 转换后的矩阵,形状为 (N * out_h * out_w, C * FH * FW)
"""
N, C, H, W = x.shape
out_h = int(1 + (H + 2 * pad - FH) / stride)
out_w = int(1 + (W + 2 * pad - FW) / stride)
col = np.zeros((N, C, FH, FW, out_h, out_w))
for i in range(out_h):
for j in range(out_w):
col[:, :, :, :, i, j] = x[:, :, i * stride:i * stride + FH, j * stride:j * stride + FW]
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N * out_h * out_w, -1)
return col
class Convolution:
"""
卷积层的实现。
参数:
W -- 滤波器权重,形状为 (FN, C, FH, FW)
b -- 偏置,形状为 (FN,)
stride -- 步幅
pad -- 填充
"""
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):
"""
前向传播。
参数:
x -- 输入数据,形状为 (N, C, H, W)
返回:
out -- 输出数据,形状为 (N, FN, out_h, out_w)
"""
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)
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)
return out
# 示例使用
# 假设输入数据 x 和滤波器 W, b
x = np.random.rand(1, 3, 32, 32) # 示例输入数据
W = np.random.rand(10, 3, 5, 5) # 示例滤波器
b = np.random.rand(10) # 示例偏置
conv = Convolution(W, b, stride=1, pad=2)
output = conv.forward(x)
print(output.shape)
代码说明
im2col
函数:- 该函数将输入数据
x
转换为适合卷积操作的矩阵形式。 - 输入数据
x
的形状为(N, C, H, W)
,其中N
是批量大小,C
是通道数,H
和W
是输入数据的高度和宽度。 - 输出数据
col
的形状为(N * out_h * out_w, C * FH * FW)
,其中out_h
和out_w
是输出数据的高度和宽度,FH
和FW
是滤波器的高度和宽度。
- 该函数将输入数据
Convolution
类:- 该类实现了卷积层的前向传播。
__init__
方法初始化滤波器权重W
、偏置b
、步幅stride
和填充pad
。forward
方法执行前向传播,计算卷积层的输出。
示例使用:
- 创建一个示例输入数据
x
、滤波器W
和偏置b
。 - 创建
Convolution
类的实例conv
。 - 调用
conv.forward(x)
计算卷积层的输出,并打印输出数据的形状。
- 创建一个示例输入数据
池化层的实现
池化层是实际实现时需要改变存储数据的数据结构来进行处理:
流程图如上,我们很好理解其原理
import numpy as np
class Pooling:
"""
池化层的实现。
参数:
pool_h -- 池化窗口的高度
pool_w -- 池化窗口的宽度
stride -- 步幅
pad -- 填充
"""
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):
"""
前向传播。
参数:
x -- 输入数据,形状为 (N, C, H, W)
返回:
out -- 输出数据,形状为 (N, C, out_h, out_w)
"""
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)
# 转换 (3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out
def im2col(x, pool_h, pool_w, stride, pad):
"""
将输入数据转换为适合池化操作的矩阵形式。
参数:
x -- 输入数据,形状为 (N, C, H, W)
pool_h -- 池化窗口的高度
pool_w -- 池化窗口的宽度
stride -- 步幅
pad -- 填充
返回:
col -- 转换后的矩阵,形状为 (N * pool_h * pool_w, C * out_h * out_w)
"""
N, C, H, W = x.shape
out_h = int(1 + (H - pool_h + 2 * pad) / stride)
out_w = int(1 + (W - pool_w + 2 * pad) / stride)
x_pad = np.pad(x, ((0, 0), (0, 0), (pad, pad), (pad, pad)), 'constant')
col = np.zeros((N, C, out_h, out_w, pool_h, pool_w))
for i in range(out_h):
for j in range(out_w):
col[:, :, i, j, :, :] = x_pad[:, :, i * stride:i * stride + pool_h, j * stride:j * stride + pool_w]
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N * pool_h * pool_w, -1)
return col
代码说明
Pooling
类:- 该类实现了池化层的前向传播。
__init__
方法初始化池化窗口的高度pool_h
、宽度pool_w
、步幅stride
和填充pad
。forward
方法执行前向传播,计算池化层的输出。
im2col
函数:- 该函数将输入数据
x
转换为适合池化操作的矩阵形式。 - 输入数据
x
的形状为(N, C, H, W)
,其中N
是批量大小,C
是通道数,H
和W
是输入数据的高度和宽度。 - 输出数据
col
的形状为(N * pool_h * pool_w, C * out_h * out_w)
,其中out_h
和out_w
是输出数据的高度和宽度。
- 该函数将输入数据
示例使用:
- 创建一个
Pooling
类的实例pool
。 - 调用
pool.forward(x)
计算池化层的输出,并打印输出数据的形状。
- 创建一个
三、CNN的工作流程
以手写数字识别为例:
输入图像:将手写数字图像转换为像素矩阵。
卷积操作:用卷积核在图像上滑动,提取特征,生成特征图。
池化操作:对特征图进行降维,提取重要特征。
重复卷积和池化:多次卷积和池化,进一步提取特征。
全连接:将特征图展平为一维向量,进行综合计算。
输出结果:输出每个数字的概率,取概率最大的数字作为识别结果。
四、CNN的优点
• 特征提取能力强:通过卷积操作自动提取图像特征。
• 防止过拟合:通过池化层减少特征数量。
• 平移不变性:即使图像位置稍有偏移,也能正确识别。
五、总结
CNN是一种强大的图像识别工具,通过卷积层提取特征,池化层降维,全连接层综合计算,最终输出识别结果。它广泛应用于图像识别、物体检测等领域。