卷积神经网络
视觉领域神级算法 - 卷积网络.. (*≧ω≦)
# 前言
在 CNN 出现之前, 图像识别对于人工智能(传统神经网络/人工神经网络)来说是一个难题, 有2个原因:
原因1> 人工智能实现图像识别时需要处理的数据量太大, 导致模型训练和计算成本很高, 并且效率很低

假设有张 1000×1000 像素的彩色图片,每个像素点需要RGB3个参数来表示颜色信息.
则这张原始图片有1000×1000x3个特征维度的信息 -- 即输入层应设置300w个神经元
若下一层隐藏层我们设置了1000个神经元 -- 传统神经网络中是全连接的方式,则在该线性层有300w乘以1000,即20亿个权重系数.
30亿!!这也太巨大了吧!处理起来是非常消耗资源的,而且这只是一张不算太大的图片!
--> 计算成本大,效率低,训练效果也不好!
2
3
4
5
6
原因2> 图像在数字化的过程中很难保留原有的特征, 导致图像处理的准确率不高
图片数字化的传统方式我们简化一下, 就类似下图的过程:

假如有圆形是1,没有圆形是0,那么圆形的位置不同就会产生完全不同的数据表达.
但是从视觉的角度来看,圆形的位置不同的两张图片的内容(本质)并没有发生变化,只是位置发生了变化(图像旋转).
你把这两张图片给传统神经网络,它会认为这两者图片数字化的结果是有较大的差异的.
意味着它很难抓住图片中的主要特征,导致模型对图片识别的精度不高!!
2
3
4
5
这两个问题,在CNN卷积神经网络中,可以得到有效的解决.
# CNN的组成
CNN网络主要有三部分构成: 卷积层(convolution layer)+ 池化层(pooling layer)+ 全连接层(full-connection layer).
- 卷积层负责提取图像中的主要特征;
- 池化层用来大幅降低参数量级(降维);
- 全连接层类似人工神经网络的部分, 用来输出想要的结果.

# 卷积层
卷积层是卷积神经网络中的核心模块之一.
卷积层的核心部分是 [卷积计算], 卷积层的目的是 提取输入数据的 [特征图].
图片(单通道、多通道); 卷积核(单、多、单通道、多通道); 单卷积层、多个卷积层
任意搭配 > 通常都是多通道多卷积核, 并且是多个卷积层堆叠..
在CNN中,通常会通过堆叠多个卷积层. 使网络能够学习到更加复杂和抽象的特征.

上图的卷积过程展示的是最基本的 以单通道的灰度图像, 只使用了一个卷积核 的卷积过程.
而关于单通道多卷积核多个卷积层堆叠在后面的项目示例中会提及, 至于多通道多卷积核对卷积层堆叠, 可以举一反三..
- 输入数据 : 原始图片数字化后的结果
- 卷积窗口: 又叫做卷积核, 一般都是使用正方形的, 比如 2× 2, 3×3, 5×5 等.. (历史实验证明,3X3用的最多)
- 我们通常会使用多个不同的卷积核来对同一图像求卷积 上图中只使用了一个卷积核..
- 目的就是为了可以提取出图像中多种不同的特征
- 比如 图像纹理 - "纹理则帮助我们分析物体的表面特征"
- 比如 图像边缘 - ""边缘帮助我们确定物体的轮廓"
- 而且我们不确定哪种卷积核提取出来的特征是比较有效的
- 卷积窗口滑动的步长: 在计算机视觉中一般使用1步长, 但是在其他领域, 比如NLP中步长则可能需要调整的大一点.
- 卷积计算: 在卷积窗口和输入数据的局部区域间做点积运算
接下来.我们层层递进, 来深度剖析下.
1. 单通道的灰度图像 - 单通道图像,单个卷积核,滑动窗口进行点积运算
2. 多通道的彩色图像 - RBG图片,每个通道都对应卷积核的一个通道
图片每个通道的数字化与对应卷积核通道,也是滑动窗口进行点积运算. 将每个通道的结果按位相加得到最终的特征图
"原始图片是单通道的,卷积核就是单通道的;原始图片是多通道的,卷积核就是多通道的"
3. 我们通常对同一张原始图片使用多个不同的卷积核 因为这样 可以提取 该图片 不同的特征,eg:图像纹理,图像边缘 - 多卷积核
在CNN中,通常会通过堆叠多个卷积层. 使网络能够学习到更加复杂和抽象的特征. - 多卷积层
1> 但经过多个卷积层后,即 原始图像-卷积核-特征图-池化-卷积核-特征图-池化, 特征图的特征数量会不断减少
2> 在卷积的过程中,图像数字化的中间数据会被重复使用多次,图像数字化最外围的那些数据只会被使用一次,容易丢失这些特征.
-- 针对这两个问题,应该在卷积过程中使用 same padding 来解决!!
2
3
4
5
6
7
8
9
# 卷积计算(单通道 灰色图)
卷积计算是一种计算方式, 有一个 卷积窗口(Convolution Window)在一个图像数字化后的平面上滑动.
每次滑动会进行一次 卷积计算 得到一个数值(Value)
卷积窗口对于该平面全部滑动计算完成后会得到一个用于表示图像特征的 特征图
卷积计算本质上就是在卷积窗口和输入数据的局部区域间做点积运算.
下面所演示的是: 用一个 3×3 的卷积窗口对 5×5 的图片求卷积, 卷积的移动步长为 1, 最后得到一张 3×3 的特征图.

具体来说, 点积运算就是 - 对应位置的数相乘后,一并相加. 滑动后,重复这一操作.

# 卷积计算(多通道 彩色图)
上面展示的是单个通道图(eg:灰色图像)的卷积过程.
实际中的图像都是多个通道图(eg:彩色图像), 我们怎么进行卷积操作呢?

计算方法如下: 当输入数据有多个通道时(例如图片可以有RGB三个通道).. 卷积核的通道需要和图像通道数量保持一致.
卷积核的通道与输入数据的对应通道进行卷积, 将每个通道的卷积结果按位相加得到最终的特征图.
红色上左卷积操作: 35*-1+19*-1+25*-1+13*0+22*1+61*-1+0*0+4*1+36*1 = -78
绿色上左卷积操作: 9*1+7*0+3*0+26*1+34*-1+61*-1+15*1+4*0+36*-1=-81
蓝色上左卷积操作: 4*0+6*1+3*1+0*0+44*1+61*0+3*1+4*-1+36*1 = 88
特征图上左: (-78)+(-81)+88=-71
2
3
4
# 卷积步长
卷积的步长理论上可以取任意正整数. 步长不同,提取到的特征图肯定是不一样的.
以单通道的灰度图像为例, 按照步长为1来移动卷积核, 计算特征图如下所示:

以单通道的灰度图像为例, 如果我们把stride增大, 比如设为2, 也是可以提取特征图的,如下图所示:

增大卷积步长能有效的减少卷积运算的次数, 降低计算力损耗, 但是也会丢失特征, 特征不够就导致欠拟合.
# 卷积核的制定
前面我们提过一嘴 通常会使用多个不同的卷积核来对同一图像求卷积, 目的就是为了可以提取出图像中多种不同的特征(特征图)
比如图像纹理、图像边缘.. 当然,我们也不知道不确定哪种卷积核提取出来的特征是比较有效的

我们思考一个问题: 卷积核的值要怎么取呢?
- 如果是使用传统的机器学习思维, 我们能想到的方法可能是人为设计大量不同的卷积核.
然后使用大量图片来做测试, 最后分析哪种卷积核提取出来的特征比较有效. - 在深度学习里面, 卷积核的取值在卷积神经网络训练最开始的阶段是随机初始化的.
之后结合误差反向传播算法, 逐渐训练得到最终的结果(权值训练). 我们只能够决定卷积核的维度即可.
维度大小的话, 常见的是3x3, 但为什么是3x3, 并没有理论依据, 通过大量的实践测试得来的, 这个大小最好用
注意:
- 训练好的卷积核就可以作为特征提取器, 用于提取图像特征, 然后传到网络后面的全连接层, 用于分类回归等任务.
卷积核中的值就可以作为提取特征时特征的权重/权值. - 在同一个卷积核中的权值对于待提取特征的图片数据是共享的, 在不同的卷积核中的权值是不共享的..
简单来说, 前半部分指的的, 滑动窗口计算, 每个区域使用的是同一个卷积核;
后半部分指的是 对同一个图像使用不同的卷积核以提取图像中不同的特征..
- 假设使用 6 个 5×5 的卷积核对一幅图像求卷积, 会产 6×5×5=150 个权值加 6 个偏置值, 卷积后会得 到 6 个不同的特征图
- 假设使用 6 个 5×5 的卷积核对十幅图像求卷积,
依旧会产生 6×5×5=150 个权值加 6 个偏置值, 卷积后会得 到 6 个不同的特征图
因为权重数量 只取决于卷积核的大小和个数,与输入图像的数量 无关
2
3
4
# Padding
在卷积神经网络中我们通常会堆叠多个卷积层的结构,形成一个深度的卷积神经网络. 堆叠多个卷积层结构会碰到一些问题:
问题1: 每一次做卷积,得到的特征图就会比原来的图像要变小一些,这样特征的数量会不断减少.
例如使用 3×3 的卷积核对 4×4 的图像求卷积,步长为 1,卷积后得到一个 2×2 的特征图
问题2: 在计算卷积的时候图像中间的数据会重复使用多次,而图像边缘的数据可能只会被用到一次!
例如使用 3×3 的卷积核对 4×4 的图像求卷积,图像四个角的四个数据只计算了一次,而图像中心的四个数据则计算了四次
这就表示卷积容易丢失掉图像的边缘特征!
2
3
4
5
针对上述两个问题, 我们可以使用 Padding 的方式来解决.
卷积层和池化层都可以使用 Padding, Padding 一般有两种方式: Valid Padding和Same Padding
- Valid Padding 其实就是不填充. 不填充数据那么卷积后得到的特征图就会比原始图像要小一些.
- Same Padding 指的是通过填充数据(一般都是填充 0), 使得卷积后的特征图的大小跟原始的图像大小相同!

上图中使用 3×3 的卷积核对 5×5 的图像进行求卷积的操作,步长为 1.
给原图像外圈填充 1 圈 0 之后再做卷积,卷积后得到的特征图大小就可以跟原始图像相同,也是 5×5 的大小.
2
使用了Same Padding进行外围填充后, 则可以保证在计算卷积的时候图像的中间和边缘的数据被使用的次数是一致的.
则表示图片边缘的特征不会丢失!
思考: 如果使用Same Padding的方式进行卷积,会发现卷积后的特征图和原始特征的形状尺寸一致,这么做有何意义呢?
可以将特征中的核心特征进行最大化的提取! 卷积计算后特征图中大的数值变的更大, 凸显更重要的特征. 如下图所示:

# 池化层
在卷积神经网络架构中,当一张图像被进行了卷积层处理后,通常会将卷积层输出的结果经过一个非线性的激活函数输入给池化层.
池化层也有一个[滑动窗口]并且会在卷积后得到的特征图(经过激活函数处理后的特征图)中进行[滑动计算].
这一点跟卷积有点类似,不过要注意的是池化层中的滑动窗口中没有需要训练的权值!
池化通常可以分为三种方式:
最大池化(Max-Pooling):最大池化指的是提取池化窗口区域内的最大值
平均池化(Mean-Pooling):平均池化指的是提取池化窗口区域内的平均值
随机池化(Stochastic Pooling):随机池化指的是提取池化窗口区域内的随机值

池化的作用:
进一步提取重要的特征信息(进一步的特征选择),压缩特征, 降低计算量, 缓解过拟合的情况
使得网络的输入具有平移不变形
- 例如一张图片中有天空.天空那一大片区域颜色是很接近的.用卷积层提取出的局部特征也很相近.这样的话会造成特征信息的冗余.
带来计算量大的问题.如何将冗余的信息删除,那就要用到池化层
- 平移不变形指的是当我们对输入进行少量平移时,经过池化后的数值并不会发生太大变化.
这是一个非常有用的性质,因为我们通常关心的是某个特征是否在图像中出现,而不是关心这个特征具体出现的位置.
例如:我们要判断一张图片中是否有猫,我们并不关心猫是出现在图片上方还是下方,还是左边还是右边.我们只关心猫是否出现在图片中.
2
3
4
5

我们对输入进行少量平移时, 经过池化后的数值并不会发生太大变化!!
# 卷积网络结构

卷积神经网前面的部分进行卷积池化相当于是进行特征提取,后面部分进行全连接相当于是利用提取出来的图像特征进行分类
全连接层位于CNN网络的末端,经过卷积层的特征提取与池化层的降维后.
将特征图转换成 一维向量 送入到全连接层中进行分类或回归的操作.
思考: 卷积神经网络的网络层数如何确定?
- 这些暂时没有理论支撑. 一般都是靠感觉去设置几组候选值, 然后通过实验挑选出其中的最佳值.
这也是现在深度卷积神经网络虽然效果拔群, 但是一直为人诟病的原因之一 - 现在业界里提出的各种设想结构中不少都是试错法,试出来一个效果不错的网络结构.然后讲一个好听的故事.
因为深度学习理论还不够,所以故事一般都是看上去很美,背后到底是不是这回事只有天知道.
LeNet-5作为一个简单的卷积神经网络模型,不仅在历史上具有开创性的意义,而且至今仍然被广泛用于深度学习的教学和研究中!

f是卷积核的尺寸大小,s是步长,n是卷积核的个数!!
(上图的输入是一张彩色图片,第一次卷积时,卷积核是3通道的;第二次卷积时,卷积核是6通道的!!)
注意: 该网络结构是一个5层(两个卷积层, 两个全连接层,一个输出层)的卷积神经网络.
计算神经网络层数的时候, 有权值的才算是一层. 比如池化层就不能单独算一层!!
# MNIST手写数字识别

图中原始的手写数字的图片是一张 28×28 的图片, 并且是黑白的, 所以图片的通道数是 1, 输入数据是 28×28×1 的数据.
注意: 该网络结构是一个4层(两个卷积层, 一个全连接层,一个输出层)的卷积神经网络.
# 结构分析
- 第 1 层为卷积层, 使用 32 个 5×5 的卷积核对原始图片求卷积, 步长为 1, Same Padding.
因为是 Same Padding 并且步长为 1, 所以卷积后的特征图大小跟原图片一样, 可以得到 32 张 28×28 的特征图.- 池化的计算是在卷积层中进行的, 使用 2×2, 步长为 2 的池化窗口做池化计算, 池化后得到 32 张 14×14 的特征图.
特征图的长宽都变成了之前的 1/2。 - 权值的数量为 5×5×32=800(只有卷积后的特征图才有权值, 池化中不涉及权值).
偏置值数量为 32(1 个特征图会有 1 个偏置值)。
- 池化的计算是在卷积层中进行的, 使用 2×2, 步长为 2 的池化窗口做池化计算, 池化后得到 32 张 14×14 的特征图.
- 第 2 层也是卷积层, 使用 64 个 5×5 的卷积核对 32 张 14×14 的特征图求卷积, 步长为 1, Same Padding.
(你就看作是一张 14x14 尺寸大小,有32通道的图片!).
因为是 Same Padding 并且步长为 1, 所以卷积后的特征图大小跟原图片一样, 可以得到 64 张 14×14 的特征图.- 这里64个卷积核,每个卷积核的通道数都是32
- 第 2 个卷积层卷积窗口大小 5×5, 对 32 张图像求卷积产生 64 个特征图..
参数个数是 5×5×32×64=51200 个权值加上 64 个偏置(1 个特征图 会有 1 个偏置值). - 池化的计算是在卷积层中进行的, 使用 2×2, 步长为 2 的池化窗口做池化计算, 池化后 得到 64 张 7×7 的特征图.
特征图的长宽都变成了之前的 1/2.
- 第 3 层是全连接层, 第 2 个池化层之后的 64×7×7 个神经元跟 1024 个神经元做全连接.
- 第 4 层是输出层, 输出 10 个预测值, 对应 0-9 的 10 个数字!!
# 代码实现
# 读取数据
import pickle
import gzip
from pathlib import Path
import numpy as np
DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"
FILENAME = "mnist.pkl.gz"
# 读取压缩文件数据
with gzip.open((PATH / FILENAME), "rb") as f:
# load返回三组数据:训练集、测试集和下划线表示的验证集(忽略)
((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")
2
3
4
5
6
7
8
9
10
11
12
# 网络结构设计

from torch.utils.data import TensorDataset, DataLoader
import torch
import torch.nn as nn
import torch.optim as optim
class CNN(nn.Module):
# 定义有序容器: 内部添加卷积网络层
def __init__(self):
super(CNN, self).__init__()
# 卷积层1--激活函数--池化
self.conv1 = nn.Sequential( # 输入大小 (1, 28, 28)
nn.Conv2d(
# 图片的通道数
# 如果是RGB图片,in_channels=3,如果是灰度图像为1.
# 若是第二层或者更多层卷积,当前层的输入通道数就是上一层卷积的输出通道数
in_channels=1,
out_channels=32, # 卷积核的个数,等同于最后要得到多少个特征图
kernel_size=5, # 卷积核的尺寸大小
stride=1, # 步长
# 默认padding=0
# 如果希望卷积后的特征图大小跟原来一样
# 需要设置padding=(kernel_size-1)/2 if stride=1
padding=2,
), # 第一次卷积后,输出结果为(32,28,28)
nn.ReLU(), # relu层:激活函数
# 进行池化操作(2x2 区域) 输出结果为:(32, 14, 14)
nn.MaxPool2d(kernel_size=2, stride=2),
)
# 卷积层2--激活函数--池化
self.conv2 = nn.Sequential( # 该卷积层的输入数据的维度 (32, 14, 14)
# so!!这里的通道数是32~
nn.Conv2d(32, 64, 5, 1, 2), # 输出 (64, 14, 14)
nn.ReLU(), # relu层
nn.MaxPool2d(2,2), # 输出 (64, 7, 7)
)
# 全连接层
self.fc = nn.Linear(64 * 7 * 7, 1024) # 全连接层得到的结果 参数(输入神经元个数,输出神经元个数)
self.out = nn.Linear(1024,10) # 全连接层得到的结果
# 实现向前传播
def forward(self, x):
# 输入的x的尺寸大小是 (64,1,28,28) > 64指小批次样本数 1指图片单通道 28x28指图片的大小
c1_x = self.conv1(x) # 输出形状 (64,32,14,14)
c2_x = self.conv2(c1_x) # 输出形状 (64,64,7,7)
# 在进入全连接层之前,展平拉直!!
c2_x = c2_x.view(c2_x.size(0), -1) # -1会自动计算列数 c2_x.shape > (64,64*7*7)
fc_x = self.fc(c2_x)
output = self.out(fc_x)
return output
from torchsummary import summary
device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
model = CNN().to(device)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 模型设置
# 将数据转换为tensor类型
x_train, y_train, x_valid, y_valid = map(
torch.tensor, (x_train, y_train, x_valid, y_valid)
)
# 定义评估指标
def accuracy(outputs, labels):
_, predicted = torch.max(outputs, 1)
correct = (predicted == labels).sum().item()
total = labels.size(0)
return correct / total
# 先临时使用一个基于分类任务中的某一个损失函数
criterion = nn.CrossEntropyLoss()
# 创建一个优化器
opt = optim.Adam(model.parameters(), lr=0.001)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 分小批次迭代训练

bs = 64
train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)
test_ds = TensorDataset(x_valid, y_valid)
test_dl = DataLoader(test_ds, batch_size=bs, shuffle=True)
num_epochs = 10
for step in range(num_epochs): # 迭代训练&测试
# 训练阶段
model.train()
# 存储每批次训练后的损失和正确率
batch_loss = []
batch_acc = []
# 批次训练
for xb,yb in train_dl: # 基于batch开始一次迭代
# 训练数据加入到cuda中
xb = xb.to(device) # xb.shape > (64,784)
yb = yb.to(device)
"""
★ Conv2d卷积层期望输入数据的形状为 [batch_size, channels, height, width].
对于灰度图像,通道数通常是1;对于RGB图像,通道数通常是3.
"""
xb = xb.view(-1, 1, 28, 28)
# print(xb.shape)
prediction = model(xb) # 向前传播
loss = criterion(prediction, yb) # 计算损失
loss.backward() # 反向传播
opt.step() # 更新参数
opt.zero_grad() # 梯度清零
# 每批次损失记录到batch_loss数组中
batch_loss.append(loss.item())
# 训练集准确率
train_acc = accuracy(prediction, yb)
# 每批次准确率记录到batch_acc数组中
batch_acc.append(train_acc)
# 打印迭代训练的损失和准确率
if (step+1) % 2 == 0:
print(f'Epoch [{step+1}/{num_epochs}]')
print(f'Train Loss: {np.mean(batch_loss):.4f}, Train Accuracy: {np.mean(batch_acc):.4f}')
# 批次测试
model.eval()
# 存储每批次测试后的损失和正确率
batch_loss_test = []
batch_acc_test = []
for xb,yb in test_dl: # 基于batch开始一次迭代
# 训练数据加入到cuda中
xb = xb.to(device)
yb = yb.to(device)
"""
★ Conv2d卷积层期望输入数据的形状为 [batch_size, channels, height, width].
对于灰度图像,通道数通常是1;对于RGB图像,通道数通常是3.
"""
xb = xb.view(-1, 1, 28, 28)
with torch.no_grad(): #不计算梯度,提升运行效率
test_outputs = model(xb) # 基于测试集特征的分类结果
test_loss = criterion(test_outputs, yb) # 测试集损失
test_acc = accuracy(test_outputs, yb) # 测试集准确率
batch_loss_test.append(test_loss.item())
batch_acc_test.append(test_acc)
#打印迭代训练的损失和准确率
if (step+1) % 2 == 0:
print(f'Epoch [{step+1}/{num_epochs}]')
print(f'Test Loss: {np.mean(batch_loss_test):.4f}, Test Accuracy: {np.mean(batch_acc_test):.4f}')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68