CNN(Convolutional Neural Network,卷积神经网络)是一种深度学习模型,广泛应用于图像识别、视频分析、自然语言处理等领域。它通过模拟人类视觉系统的工作方式,能够自动提取图像或其他数据中的特征,从而实现高效的分类、检测和生成任务。以下是CNN的详细介绍:
一、CNN的起源与发展
起源
-
CNN的灵感来源于生物视觉系统的研究。20世纪60年代,Hubel和Wiesel通过研究猫的大脑皮层,发现视觉信息在大脑中是分层处理的。这一发现为CNN的设计提供了理论基础。
-
1998年,Yann LeCun等人提出了LeNet-5,这是最早的CNN之一,用于手写数字识别(如MNIST数据集)。LeNet-5的成功展示了CNN在图像识别任务中的巨大潜力。
发展
-
2012年,AlexNet在ImageNet竞赛中取得了突破性成绩,大幅提高了图像分类的准确率。AlexNet的出现标志着深度学习时代的到来,CNN开始在学术界和工业界受到广泛关注。
随后,VGGNet、GoogLeNet(Inception系列)、ResNet等更先进的CNN架构相继出现,进一步推动了CNN的发展。这些架构在模型深度、参数数量、计算效率等方面不断优化,使得CNN在各种视觉任务中的性能不断提升。
二、CNN的基本结构
CNN通常由以下几部分组成:
输入层(Input Layer)
-
输入层接收原始数据,如图像的像素值。对于彩色图像,输入数据通常是三维的,包括高度、宽度和通道数(例如RGB图像有3个通道)。
卷积层(Convolutional Layer)
-
卷积层是CNN的核心部分,负责提取输入数据的局部特征。它通过卷积核(或滤波器)在输入数据上滑动,进行卷积运算。
-
卷积核的大小通常小于输入数据的大小,例如3×3或5×5。卷积核的参数(权重)在训练过程中会不断更新,以学习到更有用的特征。
-
卷积运算的公式为:输出=
输入
×卷积核
+偏置
-
卷积层可以有多个卷积核,每个卷积核提取不同的特征,从而生成多个特征图(Feature Map)。这些特征图组合在一起,形成下一层的输入。
激活层(Activation Layer)
-
激活层的作用是引入非线性因素,使CNN能够学习更复杂的特征。常用的激活函数包括ReLU(Rectified Linear Unit,修正线性单元)、Sigmoid、Tanh等。
-
ReLU是最常用的激活函数,其公式为:
。ReLU的优点是计算简单,且能够有效缓解梯度消失问题。
池化层(Pooling Layer)
-
池化层的作用是降低特征图的空间维度,减少计算量和参数数量,同时保留重要特征。常见的池化操作包括最大池化(Max Pooling)和平均池化(Average Pooling)。
-
最大池化是取池化窗口内的最大值,而平均池化是取池化窗口内的平均值。例如,对于一个2×2的池化窗口,最大池化会输出窗口内的最大值,平均池化会输出窗口内的平均值。
全连接层(Fully Connected Layer)
-
全连接层的输出通常经过Softmax函数(用于分类任务)或线性函数(用于回归任务)进行最终的预测。
-
全连接层的作用是将卷积层和池化层提取的特征进行整合,输出最终的分类结果或回归值。在全连接层中,每个神经元都与前一层的所有神经元相连。
输出层(Output Layer)
-
输出层根据任务类型输出最终结果。对于分类任务,输出层的神经元数量通常与类别数量相同,每个神经元的输出值表示该类别对应的概率。对于回归任务,输出层通常只有一个神经元,输出值为预测的回归值。
三、CNN的工作原理
CNN的工作原理可以分为前向传播和反向传播两个阶段:
前向传播(Forward Propagation)
-
输入数据(如图像)首先经过输入层进入CNN。
-
在卷积层中,卷积核在输入数据上滑动,进行卷积运算,生成特征图。每个卷积核提取不同的特征,多个卷积核组合生成多个特征图。
-
特征图经过激活层进行非线性变换,提取更复杂的特征。
-
池化层对特征图进行降采样,保留重要特征,同时减少计算量和参数数量。
-
经过多个卷积层、激活层和池化层后,特征图被展平为一维向量,输入到全连接层。
-
全连接层对特征进行整合,输出最终的分类结果或回归值。
反向传播(Backward Propagation)
-
在训练过程中,CNN通过反向传播算法更新网络参数,以最小化损失函数。
-
首先,计算输出层的损失值(如交叉熵损失函数用于分类任务,均方误差损失函数用于回归任务)。
-
然后,通过链式法则,将损失值逐层反向传播到卷积层、激活层和池化层,计算每一层的梯度。
-
最后,根据梯度更新卷积核的权重和偏置,以及全连接层的参数。常用的优化算法包括随机梯度下降(SGD)、Adam等。
-
反向传播的目标是使网络在训练数据上具有更好的拟合能力,同时避免过拟合。
图1,CNN卷积举例
四、CNN实例代码(简单的CNN用于 MNIST 手写数字识别任务)
1. 导入必要的包和设置
import torch
import torch.nn as nn
import torch.utils.data as Data
import torchvision
import matplotlib
matplotlib.use('TkAgg') # 或者使用 'Agg'
import matplotlib.pyplot as plt
-
torch:PyTorch 的核心库,用于张量操作。
-
torch.nn:包含神经网络构建的各种模块和层。
-
torch.utils.data:提供数据加载工具(DataLoader等),便于批量读取数据。
-
torchvision:主要用于计算机视觉任务,内置多个公开数据集和常用的图像预处理工具。
-
matplotlib:用于数据可视化,本例中可以用来显示 MNIST 图像。
matplotlib.use('TkAgg')
是设置后端显示方式(也可以使用 'Agg' 用于无图形界面环境)。
2. 参数设置
EPOCH = 2 # 训练轮数
BATCH_SIZE = 50 # 每批次样本数量
LR = 0.001 # 学习率
DOWNLOAD_MNIST = True # 是否下载MNIST数据集
-
EPOCH:定义数据将完整通过模型的次数。本例设置为 2 次;
-
BATCH_SIZE:每个训练批次的样本数,较小的批量可以提供更稳定的梯度更新;
-
LR(Learning Rate):学习率,用于控制每一步参数更新的步长;
-
DOWNLOAD_MNIST:标识是否需要下载 MNIST 数据集,第一次运行时需要下载数据。
3. 数据加载与预处理
3.1 加载训练数据
train_data = torchvision.datasets.MNIST(
root='./mnist',
train=True,
download=DOWNLOAD_MNIST,
transform=torchvision.transforms.ToTensor()
)
print("训练集数据形状:", train_data.data.size())
print("训练集标签形状:", train_data.targets.size())
-
torchvision.datasets.MNIST:从本地或者通过网络下载 MNIST 数据集。
-
root='./mnist'
指定数据存放的目录。 -
train=True
加载训练集。 -
download=DOWNLOAD_MNIST
根据参数设置是否下载数据。 -
transform=torchvision.transforms.ToTensor()
:预处理步骤,将 PIL 图片转换成浮点型 Tensor,并将像素值归一化到 [0,1] 范围。
-
-
打印训练数据和标签的尺寸,通常训练数据的尺寸为
[60000, 28, 28]
(60000 张 28×28 的图像),标签尺寸为[60000]
。
3.2 可选:显示一张图像
plt.imshow(train_data.data[0].numpy(), cmap='gray')
plt.title('%i' % train_data.targets[0])
plt.show()
- 用于可视化数据,通过
imshow
显示第一张图片,并将对应标签作为标题显示。
3.3 构造训练数据加载器
train_loader = Data.DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
- DataLoader:自动将数据按批次加载,并且
shuffle=True
表示每个 epoch 内会打乱数据,提高模型泛化性能。
3.4 加载测试数据和预处理
test_data = torchvision.datasets.MNIST(
root='./mnist',
train=False,
transform=torchvision.transforms.ToTensor()
)
# 这里只使用前3000个测试样本进行评估
test_x = test_data.data.unsqueeze(1).float()[:3000] / 255 # shape: [3000, 1, 28, 28]
test_y = test_data.targets[:3000]
-
加载测试集时,
train=False
表示加载测试数据; -
transform
同样把图片转换为 Tensor; -
注意这里获取测试数据并没有使用 DataLoader,而是直接处理 Tensor。
-
test_data.data
的原始尺寸通常为[10000, 28, 28]
。但卷积层期望输入有一个 channel 维度,因此调用unsqueeze(1)
在维度1上扩展,使其变成[N, 1, 28, 28]
。 -
将
test_data.data
转换为float
并除以 255 归一化到 [0,1]。 -
最后只取前3000个样本进行评估,避免计算量过大。
-
4. 定义卷积神经网络模型
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(
in_channels=1,
out_channels=16,
kernel_size=5,
stride=1,
padding=2
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.conv2 = nn.Sequential(
nn.Conv2d(
in_channels=16,
out_channels=32,
kernel_size=5,
stride=1,
padding=2
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.out = nn.Linear(32 * 7 * 7, 10)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
output = self.out(x)
return output
4.1 初始化模块(__init__
部分)
-
第一层卷积 (
self.conv1
):-
nn.Conv2d:接受输入图像 1 个通道,输出16个通道。
-
kernel_size=5
指定卷积核尺寸为 5×5。 -
stride=1
指定步长为 1。 -
padding=2
保持卷积之后图像尺寸不变(公式:(kernel_size - 1) / 2)。 -
后接 ReLU 激活函数(添加非线性)和
nn.MaxPool2d(kernel_size=2)
,将特征图尺寸缩减一半,即从 28×28 变成 14×14,同时通道数为 16。
-
-
第二层卷积 (
self.conv2
):-
输入通道为第一层输出 16,输出 32 个通道;
-
同样使用 5×5 卷积核,padding=2 保持尺寸;
-
后接 ReLU 激活函数和 2×2 最大池化,此时输出尺寸从 14×14 变为 7×7,通道数为 32。
-
-
全连接层 (
self.out
):-
将卷积层输出的特征先展平,尺寸变为 32×7×7,共计 1568 个特征。
-
通过全连接层映射到 10 个类别,分别对应 0~9 十个数字。
-
4.2 定义前向传播(forward
函数)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
output = self.out(x)
return output
-
输入
x
首先通过conv1
层,再通过conv2
层; -
x.view(x.size(0), -1)
将每个样本的 3D 特征图展平成一维向量,方便输入全连接层; -
最后经过全连接层输出各类别的得分。
5. 模型实例化以及优化器和损失函数的定义
cnn = CNN()
print("网络结构:")
print(cnn)
- 实例化 CNN 模型,并打印网络结构,方便验证层次和参数设置是否正确。
optimizer = torch.optim.Adam(cnn.parameters(), lr=LR)
loss_fn = nn.CrossEntropyLoss()
-
Adam 优化器:自动调整每个参数的学习率,适合大多数任务;
-
CrossEntropyLoss:用于分类任务的损失函数,内部包括 softmax 操作。
6. 训练过程
for epoch in range(EPOCH):
for step, (b_x, b_y) in enumerate(train_loader):
output = cnn(b_x) # 前向传播,得到预测输出
loss = loss_fn(output, b_y) # 计算输出与真实标签之间的损失
optimizer.zero_grad() # 清空当前 batch 所有参数的梯度
loss.backward() # 反向传播,计算梯度
optimizer.step() # 根据梯度更新网络参数
if step % 50 == 0:
cnn.eval() # 切换模型到评估模式(例如 Dropout、BatchNorm 等层在测试时的行为不同)
with torch.no_grad(): # 在验证时关闭梯度计算,提高效率
test_output = cnn(test_x)
y_pred = test_output.argmax(dim=1)
accuracy = (y_pred == test_y).float().mean().item()
print(f'Epoch: {epoch} | Step: {step} | Loss: {loss.item():.4f} | Accuracy: {accuracy:.4f}')
cnn.train() # 恢复训练模式
-
外层循环:遍历所有的 epoch,整个数据集会被完整训练多次。
-
内层循环:遍历每个 mini-batch(由 DataLoader 提供),进行如下操作:
-
前向传播:将批次图像
b_x
输入模型,得到预测结果output
。 -
计算损失:使用交叉熵损失函数计算预测
output
与真实标签b_y
之间的误差。 -
梯度清零:在每次梯度反向传播前,调用
optimizer.zero_grad()
清除上次迭代累积的梯度。 -
反向传播:通过
loss.backward()
计算每个参数的梯度。 -
参数更新:调用
optimizer.step()
根据计算得到的梯度更新模型参数。
-
-
每 50 步:对当前模型在测试集上的表现进行评估:
-
调用
cnn.eval()
切换到评估模式,确保如 Dropout 等层在测试时不再随机失活。 -
使用
with torch.no_grad()
包裹预测过程以关闭梯度计算,从而节省内存和计算资源。 -
计算测试集输出后,通过
argmax(dim=1)
选择概率最大的类别,并计算准确率。 -
打印当前的 epoch、步数、损失值和准确率,并使用
cnn.train()
切换回训练模式。
-
7. 测试结果展示
with torch.no_grad():
test_output = cnn(test_x[:10])
y_pred = test_output.argmax(dim=1)
print("预测结果:", y_pred.tolist())
print("真实标签:", test_y[:10].tolist())
-
目的:从测试集中选取前 10 个样本进行测试,并展示模型的预测结果与实际标签对比。
-
使用
with torch.no_grad()
关闭梯度计算。 -
调用
argmax(dim=1)
得到预测类别,转换为 Python 的列表打印输出。
总体代码
import torch
import torch.nn as nn
import torch.utils.data as Data
import torchvision
import matplotlib
matplotlib.use('TkAgg') # 或者使用 'Agg'
import matplotlib.pyplot as plt
# ------------------------
# 参数设定
# ------------------------
EPOCH = 2 # 训练轮数
BATCH_SIZE = 50 # 每批次样本数量
LR = 0.001 # 学习率
DOWNLOAD_MNIST = True # 是否下载MNIST数据集
# ------------------------
# 数据加载与预处理
# ------------------------
# 加载训练集,这里利用 transform 将数据转为 [0,1] 的 Tensor,同时自动添加 channel 维度
train_data = torchvision.datasets.MNIST(
root='./mnist',
train=True,
download=DOWNLOAD_MNIST,
transform=torchvision.transforms.ToTensor()
)
print("训练集数据形状:", train_data.data.size())
print("训练集标签形状:", train_data.targets.size())
# 可选:显示第一张训练图像
# plt.imshow(train_data.data[0].numpy(), cmap='gray')
# plt.title('%i' % train_data.targets[0])
# plt.show()
# 构造数据加载器,自动 shuffle
train_loader = Data.DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
# 加载测试数据
test_data = torchvision.datasets.MNIST(
root='./mnist',
train=False,
transform=torchvision.transforms.ToTensor()
)
# 这里只使用前3000个测试样本进行评估
test_x = test_data.data.unsqueeze(1).float()[:3000] / 255 # shape: [3000, 1, 28, 28]
test_y = test_data.targets[:3000]
# ------------------------
# 定义卷积神经网络
# ------------------------
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 第1个卷积层:输入1通道,输出16通道;卷积核 5x5;padding=2 保证尺寸不变;之后 ReLU 激活和2x2最大池化
self.conv1 = nn.Sequential(
nn.Conv2d(
in_channels=1,
out_channels=16,
kernel_size=5,
stride=1,
padding=2
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2) # 输出尺寸: (16, 14, 14)
)
# 第2个卷积层:输入16通道,输出32通道;卷积核5x5;padding=2;再经过 ReLU 激活和2x2最大池化
self.conv2 = nn.Sequential(
nn.Conv2d(
in_channels=16,
out_channels=32,
kernel_size=5,
stride=1,
padding=2
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2) # 输出尺寸: (32, 7, 7)
)
# 全连接层:将32*7*7的特征扁平化为一维,映射到10个类别
self.out = nn.Linear(32 * 7 * 7, 10)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
# 展平特征,准备输入全连接层(batch_size, 32*7*7)
x = x.view(x.size(0), -1)
output = self.out(x)
return output
cnn = CNN()
print("网络结构:")
print(cnn)
# ------------------------
# 定义优化器与损失函数
# ------------------------
optimizer = torch.optim.Adam(cnn.parameters(), lr=LR)
loss_fn = nn.CrossEntropyLoss()
# ------------------------
# 训练过程
# ------------------------
for epoch in range(EPOCH):
for step, (b_x, b_y) in enumerate(train_loader):
# 前向传播
output = cnn(b_x)
loss = loss_fn(output, b_y)
# 清除梯度,反向传播和参数更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 每50步进行一次评估
if step % 50 == 0:
cnn.eval() # 转换为评估模式(如有Dropout、BatchNorm则更明显)
with torch.no_grad():
test_output = cnn(test_x)
y_pred = test_output.argmax(dim=1)
accuracy = (y_pred == test_y).float().mean().item()
print(f'Epoch: {epoch} | Step: {step} | Loss: {loss.item():.4f} | Accuracy: {accuracy:.4f}')
cnn.train() # 恢复训练模式
# ------------------------
# 测试集预测结果展示
# ------------------------
with torch.no_grad():
test_output = cnn(test_x[:15])
y_pred = test_output.argmax(dim=1)
print("预测结果:", y_pred.tolist())
print("真实标签:", test_y[:15].tolist())
总结
整个代码主要分为以下几个阶段:
-
导入库和预设参数:初始化工作,为数据加载、网络构造和训练提供基础配置。
-
数据加载与预处理:从 MNIST 数据集中加载训练与测试数据,利用预处理(如归一化、扩展维度)使数据适配网络输入要求。
-
网络模型构造:使用 PyTorch 定义一个简单的 CNN,通过两层卷积和池化获得特征,最后经全连接层输出分类结果。
-
训练过程:利用训练数据迭代更新模型参数,同时每隔一定步数在测试集上评估模型准确率,监控训练状态。
-
测试和结果展示:利用训练好的模型对少量测试数据进行预测,并将预测结果与真实标签进行对比查看效果。