一、概述
卷积神经网络是深度学习在计算机视觉领域的突破性成果。在计算机视觉领域, 往往我们输入的图像都很大,使用全连接网络的话,计算的代价较高。另外图像也很难保留原有的特征,导致图像处理的准确率不高。
卷积神经网络(Convolutional Neural Network,CNN)是一种专门用于处理具有网格状结构数据的深度学习模型。最初,CNN主要应用于计算机视觉任务,但它的成功启发了在其他领域应用,如自然语言处理等。
卷积神经网络(Convolutional Neural Network)是含有卷积层的神经网络. 卷积层的作用就是用来自动学习、提取图像的特征。
CNN网络主要有三部分构成:卷积层、池化层和全连接层构成,其中卷积层负责提取图像中的局部特征;池化层用来大幅降低运算量并特征增强;全连接层类似神经网络的部分,用来输出想要的结果。
1. 使用场景
卷积神经网络(CNN)是专门用于处理网格状数据(如图像、音频、视频)的深度学习架构。它在计算机视觉领域有广泛应用:
- 图像分类(识别图片中的物体)
- 目标检测(定位并识别多个物体)
- 图像分割(像素级分类)
- 人脸识别
- 医学影像分析
2. 与传统网络的区别
特性 | 全连接网络 | 卷积网络 |
---|---|---|
连接方式 | 全连接 | 局部连接 |
权重使用 | 每个连接独立权重 | 权重共享 |
参数数量 | 巨大 | 大幅减少 |
空间信息 | 丢失 | 保留 |
适用领域 | 结构化数据 | 图像等网格数据 |
3. 全连接的局限性
假设处理一张256×256的RGB图像:
- 输入节点数:256×256×3 = 196,608
- 第一隐藏层节点数:1024
- 参数量:196,608×1024 ≈ 2亿
这种参数量会导致:
- 训练困难(参数量巨大):需要海量数据和计算资源
- 过拟合风险高:模型容易记住训练数据
- 空间信息丢失(表达能力有限):像素位置关系被忽略,因为全连接网络无法捕捉图像的空间结构(如边缘、纹理),每个神经元与前一层所有像素相连,忽略了局部特征的重要性。
4. 卷积思想
卷:从左往右,从上往下
积:乘积,求和
作用:
CNN通过以下机制解决上述问题:
- 局部感知:每个神经元只关注输入图像的一个局部区域(如3×3像素窗口)。
- 权重共享:同一卷积核在图像不同位置重复使用,大幅减少参数量。
- 层次化特征提取:通过多层卷积逐步从低级特征(边缘)到高级特征(物体轮廓)。
示例:
假设图像尺寸为28×28,使用3×3卷积核,输出特征图尺寸为26×26。若使用10个卷积核,则参数量仅需10×(3×3×1 + 1) = 100(1为偏置项),远小于全连接网络。
数学表示
对于输入图像III和卷积核KKK:
(I∗K)(i,j)=∑m∑nI(i+m,j+n)K(m,n)(I * K)(i,j) = \sum_{m}\sum_{n} I(i+m,j+n)K(m,n)(I∗K)(i,j)=m∑n∑I(i+m,j+n)K(m,n)
优势体现
- 参数量大幅减少:一个3×3卷积核只有9个参数(共享)
- 平移不变性:相同特征在不同位置被同等识别
- 层次特征提取:底层→边缘,中层→形状,高层→物体
二、卷积层
1. 卷积核
定义:一个小型矩阵(如3×3),用于检测特定特征(如水平边缘、垂直边缘)。
参数:卷积核的值(权重)通过训练学习得到。
卷积核(滤波器)是小的权重矩阵,在定义时需要考虑以下几方面的内容:
卷积核的个数:卷积核(过滤器)的个数决定了其输出特征矩阵的通道数。
卷积核的值:卷积核的值是初始化好的,后续进行更新。
卷积核的大小:常见的卷积核有1×1、3×3、5×5等,一般都是奇数 × 奇数。
边缘检测核示例:
[-1, 0, 1]
[-1, 0, 1]
[-1, 0, 1]
2. 卷积计算
- 滑动窗口:卷积核在输入图像上滑动,每次与局部区域进行逐元素相乘后求和。
input 表示输入的图像
filter 表示卷积核, 也叫做滤波器
input 经过 filter 的得到输出为最右侧的图像,该图叫做特征图
那么, 它是如何进行计算的呢?卷积运算本质上就是在滤波器和输入数据的局部区域间做点积。
左上角的点计算方法:
按照上面的计算方法可以得到最终的特征图为:
卷积的重要性在于它可以将图像中的特征与卷积核进行卷积操作,从而提取出图像中的特征。
可以通过不断调整卷积核的大小、卷积核的值和卷积操作的步长,可以提取出不同尺度和位置的特征。
- 数学公式:
对于输入特征图 $ X $ 和卷积核 $ K $,输出特征图 $ Y $ 的某个位置 $ (i,j) $ 计算为:
Y[i,j]=∑m=0kh−1∑n=0kw−1X[i+m,j+n]⋅K[m,n]+b Y[i,j] = \sum_{m=0}^{k_h-1} \sum_{n=0}^{k_w-1} X[i+m, j+n] \cdot K[m,n] + b Y[i,j]=m=0∑kh−1n=0∑kw−1X[i+m,j+n]⋅K[m,n]+b
其中 $ k_h, k_w $ 是卷积核的高和宽,$ b $ 是偏置项。
示例:
# 面向对象的模块化编程 from matplotlib import pyplot as plt import os import torch import torch.nn as nn def test001(): current_path = os.path.dirname(__file__) img_path = os.path.join(current_path, "data", "彩色.png") # 转换为相对路径 img_path = os.path.relpath(img_path) # 使用plt读取图片 img = plt.imread(img_path) print(img.shape) # 转换为张量:HWC ---> CHW ---> NCHW 链式调用 img = torch.tensor(img).permute(2, 0, 1).unsqueeze(0) # 创建卷积核 (501, 500, 4) conv = nn.Conv2d( in_channels=4, # 输入通道 out_channels=32, # 输出通道 kernel_size=(5, 3), # 卷积核大小 stride=1, # 步长 padding=0, # 填充 bias=True ) # 使用卷积核对图像进行卷积操作 [9999] [[[[]]]] out = conv(img) # 输出128个特征图 conv2 = nn.Conv2d( in_channels=32, # 输入通道 out_channels=128, # 输出通道 kernel_size=(5, 5), # 卷积核大小 stride=1, # 步长 padding=0, # 填充 bias=True ) out = conv2(out) print(out) # 把图像显示出来 print(out.shape) plt.imshow(out[0][10].detach().numpy(), cmap='gray') plt.show() # 作为主模块执行 if __name__ == "__main__": test001()
底层实现
并不是水平和垂直方向的循环,实际计算通过im2col优化:
- 将图像局部块展开为列
- 将卷积核展开为行
- 进行矩阵乘法
- 结果重塑为特征图
3. 边缘填充(Padding)
作用:保持特征图尺寸不变,还更好的保护了图像边缘数据的特征。
- Valid Padding:无填充,输出尺寸减小,为 $ \frac{n - k}{s} + 1 $。
- Same Padding:填充使输出尺寸等于输入尺寸
填充尺寸计算:P=F−12P = \frac{F-1}{2}P=2F−1(F为卷积核大小)
4. 步长(Stride)
卷积核移动的步长(根据实际情况而定)。步长越大,输出尺寸越小。
步长控制卷积核移动间隔:
- 步长=1:每次移动1像素
- 步长=2:输出尺寸减半
输出尺寸公式:
Wout=⌊Win+2P−FS⌋+1W_{\text{out}} = \left\lfloor \frac{W_{\text{in}} + 2P - F}{S} \right\rfloor + 1Wout=⌊SWin+2P−F⌋+1
stride太小:重复计算较多,计算量大,训练效率降低;
stride太大:会造成信息遗漏,无法有效提炼数据背后的特征;
5. 多通道卷积计算
数字图像的标识
彩色图像表示为三维张量:[高度,宽度,通道][高度, 宽度, 通道][高度,宽度,通道]
- RGB图像:3通道(红、绿、蓝)
多通道卷积计算
若输入有 $ C_{in} $ 个通道,卷积核需有相同的通道数。每个通道单独卷积后求和,再加偏置。
每个卷积核对应所有输入通道:
输出(x,y)=∑c=1Cin(Ic∗Kc)(x,y)+b \text{输出}(x,y) = \sum_{c=1}^{C_{\text{in}}} (I_c * K_c)(x,y) + b 输出(x,y)=c=1∑Cin(Ic∗Kc)(x,y)+b
其中,CinC_{\text{in}}Cin是输入通道数
计算方法如下:
- 当输入有多个通道(Channel), 例如RGB三通道, 此时要求卷积核需要有相同的通道数。
- 卷积核通道与对应的输入图像通道进行卷积。
- 将每个通道的卷积结果按位相加得到最终的特征图。
如下图所示:
6. 多卷积核卷积计算
实际对图像进行特征提取时, 我们需要使用多个卷积核进行特征提取。这个多个卷积核可以理解为从不同到的视角、不同的角度对图像特征进行提取。
每个卷积核提取一种特征:
- 输入:[H,W,Cin][H, W, C_{\text{in}}][H,W,Cin]
- 卷积核:[F,F,Cin,Cout][F, F, C_{in}, C_{out}][F,F,Cin,Cout]
- 输出:[Hout,Wout,Cout][H_{out}, W_{out}, C_{out}][Hout,Wout,Cout]
7. 特征图大小
综合公式:
Hout=⌊Hin+2PH−FHSH⌋+1 H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2P_H - F_H}{S_H} \right\rfloor + 1 Hout=⌊SHHin+2PH−FH⌋+1
Wout=⌊Win+2PW−FWSW⌋+1 W_{\text{out}} = \left\lfloor \frac{W_{\text{in}} + 2P_W - F_W}{S_W} \right\rfloor + 1 Wout=⌊SWWin+2PW−FW⌋+1
其中, $ H_{in} $ 、$ W_{in} 是输入尺寸,是输入尺寸,是输入尺寸, P $ 是填充,$ F $ 是卷积核尺寸,$ S $ 是步长。
示例:
输入尺寸 28×28,卷积核 3×3,步长 1,填充 1 → 输出尺寸 28×28。
8. 参数共享
- 权重共享:同一卷积核在不同位置使用相同权重
- 参数量:(F×F×Cin+1)×Cout(F \times F \times C_{in} + 1) \times C_{out}(F×F×Cin+1)×Cout(+1为偏置)
9. 局部特征提取
卷积核学习不同层次特征:
- 浅层:边缘、纹理
- 中层:形状、部件
- 深层:物体、场景
10. PyTorch卷积层API
import torch.nn as nn
# 创建卷积层
conv_layer = nn.Conv2d(
in_channels=3, # 输入通道数
out_channels=64, # 输出通道数(卷积核数量)
kernel_size=3, # 卷积核大小
stride=1, # 步长
padding=1, # 填充
bias=True # 是否使用偏置
)
示例:
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import os
def showimg(img):
plt.imshow(img)
# 隐藏刻度
plt.axis("off")
plt.show()
def test001():
dir = os.path.dirname(__file__)
img = plt.imread(os.path.join(dir, "彩色.png"))
# 创建卷积核
# in_channels:输入数据的通道数
# out_channels:输出特征图数,和filter数一直
conv = nn.Conv2d(in_channels=4, out_channels=1, kernel_size=3, stride=1, padding=1)
# 注意:卷积层对输入的数据有形状要求 [batch, channel, height, width]
# 需要进行形状转换 H, W, C -> C, H, W
img = torch.tensor(img, dtype=torch.float).permute(2, 0, 1)
print(img.shape)
# 接着变形:CHW -> BCHW
newimg = img.unsqueeze(0)
print(newimg.shape)
# 送入卷积核运算一下
newimg = conv(newimg)
print(newimg.shape)
# 蒋NCHW->HWC
newimg = newimg.squeeze(0).permute(1, 2, 0)
showimg(newimg.detach().numpy())
# 多卷积核
def test002():
dir = os.path.dirname(__file__)
img = plt.imread(os.path.join(dir, "彩色.png"))
# 定义一个多特征图输出的卷积核
conv = nn.Conv2d(in_channels=4, out_channels=3, kernel_size=3, stride=1, padding=1)
# 图形要进行变形处理
img = torch.tensor(img).permute(2, 0, 1).unsqueeze(0)
# 使用卷积核对图片进行卷积计算
outimg = conv(img)
print(outimg.shape)
# 把图形形状转换回来以方便显示
outimg = outimg.squeeze(0).permute(1, 2, 0)
print(outimg.shape)
# showimg(outimg)
# 显示这些特征图
for idx in range(outimg.shape[2]):
showimg(outimg[:, :, idx].squeeze(-1).detach())
if __name__ == "__main__":
test002()
11. 知识点扩展
- 空洞卷积:扩大感受野(间隔采样)
- 深度可分离卷积:减少计算量(分离空间和通道卷积)
- 转置卷积:用于上采样(反卷积)
三、池化层:特征降维与不变性
1. 概述
池化层(Pooling)用于降低特征图空间尺寸:
- 减少计算量
- 增加平移不变性
- 防止过拟合
2. 池化层计算
最大池化(Max Pooling)
取局部区域最大值:
输出(i,j)=maxm,n∈Rij输入(m,n) \text{输出}(i,j) = \max_{m,n \in R_{ij}} \text{输入}(m,n) 输出(i,j)=m,n∈Rijmax输入(m,n)
平均池化(Average Pooling)
取局部区域平均值:
输出(i,j)=1∣Rij∣∑m,n∈Rij输入(m,n) \text{输出}(i,j) = \frac{1}{|R_{ij}|} \sum_{m,n \in R_{ij}} \text{输入}(m,n) 输出(i,j)=∣Rij∣1m,n∈Rij∑输入(m,n)
3. 步长(Stride)
通常等于池化窗口大小:
- 2×2池化 → 步长=2
- 输出尺寸减半
4. 边缘填充(Padding)
池化层较少使用填充,因为目的是降维
5. 多通道池化
池化独立应用于每个通道:
- 输入通道数 = 输出通道数
- 空间尺寸减小
6. 池化层的作用
- 降维:减少后续层计算量
- 平移不变性:微小位移不影响输出
- 特征抽象:保留最显著特征
- 防止过拟合:减少参数复杂度
7. PyTorch池化层API
# 最大池化
max_pool = nn.MaxPool2d(
kernel_size=2, # 池化窗口大小
stride=2 # 步长(默认等于kernel_size)
)
# 平均池化
avg_pool = nn.AvgPool2d(
kernel_size=2,
stride=2
)
示例:
import torch
import torch.nn as nn
# 1. API 基本使用
def test01():
inputs = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]]).float()
inputs = inputs.unsqueeze(0).unsqueeze(0)
# 1. 最大池化
# 输入形状: (N, C, H, W)
polling = nn.MaxPool2d(kernel_size=2, stride=1, padding=0)
output = polling(inputs)
print(output)
# 2. 平均池化
polling = nn.AvgPool2d(kernel_size=2, stride=1, padding=0)
output = polling(inputs)
print(output)
# 2. stride 步长
def test02():
inputs = torch.tensor([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]]).float()
inputs = inputs.unsqueeze(0).unsqueeze(0)
# 1. 最大池化
polling = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
output = polling(inputs)
print(output)
# 2. 平均池化
polling = nn.AvgPool2d(kernel_size=2, stride=2, padding=0)
output = polling(inputs)
print(output)
# 3. padding 填充
def test03():
inputs = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]]).float()
inputs = inputs.unsqueeze(0).unsqueeze(0)
# 1. 最大池化
polling = nn.MaxPool2d(kernel_size=2, stride=1, padding=1)
output = polling(inputs)
print(output)
# 2. 平均池化
polling = nn.AvgPool2d(kernel_size=2, stride=1, padding=1)
output = polling(inputs)
print(output)
# 4. 多通道池化
def test04():
inputs = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
[[10, 20, 30], [40, 50, 60], [70, 80, 90]],
[[11, 22, 33], [44, 55, 66], [77, 88, 99]]]).float()
inputs = inputs.unsqueeze(0)
# 最大池化
polling = nn.MaxPool2d(kernel_size=2, stride=1, padding=0)
output = polling(inputs)
print(output)
if __name__ == '__main__':
test04()
8. 知识点扩展
- 全局平均池化:将特征图降维为1×1(用于分类)
- 重叠池化:步长小于窗口大小(保留更多信息)
- 随机池化:按值大小概率采样(正则化效果)
四、整体结构:特征图变化
CNN典型架构
特征图变化示例
以224×224 RGB图像为例:
层类型 | 参数 | 输出尺寸 | 说明 |
---|---|---|---|
输入 | - | 224×224×3 | 原始图像 |
卷积层 | 64个7×7核,步长2 | 112×112×64 | 空间尺寸减半 |
最大池化 | 3×3,步长2 | 56×56×64 | 尺寸再减半 |
卷积层 | 128个3×3核 | 56×56×128 | 通道数增加 |
卷积层 | 128个3×3核 | 56×56×128 | 特征深化 |
最大池化 | 2×2,步长2 | 28×28×128 | 尺寸减半 |
… | … | … | … |
全局平均池化 | - | 1×1×512 | 空间信息聚合 |
全连接层 | - | 1000 | 分类输出 |
特征可视化
随着网络加深:
- 浅层:响应简单边缘和纹理
- 中层:响应复杂纹理和形状
- 深层:响应高级语义特征(如物体部件)
关键概念总结
- 局部连接 vs 全连接:CNN的核心突破,极大减少参数量
- 权重共享:使模型能够检测平移不变特征
- 层次特征提取:从简单到复杂的特征抽象过程
- 空间不变性:通过池化实现的位置不变性