1. 卷积神经网络(CNN)概述
卷积神经网络(CNN)是一种前馈神经网络,主要用于处理具有网格结构的数据(如图像)。CNN 在深度学习的图像分类和物体识别等任务中取得了突破性的成果。
CNN 的基本结构:
-
卷积层(Convolutional Layer):
- 卷积层是 CNN 中的核心部分,主要用于从输入数据中提取特征。
- 卷积操作使用多个 卷积核(滤波器),通过滑动窗口的方式与输入图像进行卷积运算,生成特征图(feature map)。
- 卷积层能够自动学习图像中的局部特征,如边缘、纹理、角点等。
-
激活函数(Activation Function):
- 激活函数常用于卷积层之后,最常用的激活函数是 ReLU(Rectified Linear Unit)。
- ReLU 函数可以将所有负数置为零,保持正数不变,从而增加网络的非线性特性。
-
池化层(Pooling Layer):
- 池化层用于对特征图进行下采样,减小特征图的尺寸,同时保留最重要的信息。
- 最常见的池化操作是 最大池化(Max Pooling),它选择池化窗口内的最大值。
-
全连接层(Fully Connected Layer):
- 全连接层将卷积和池化层提取到的特征进行进一步的组合,最终输出分类结果。
- 在图像分类任务中,最后一层通常是一个 softmax 层,用于输出每个类别的概率。
-
Dropout 层:
- Dropout 层可以帮助防止过拟合,在训练过程中随机丢弃一些神经元。
CNN 的工作原理:
- 特征提取:卷积层负责从输入图像中提取低级特征(如边缘、纹理等),池化层则帮助降维和减少计算量。
- 特征组合:全连接层负责将这些特征组合成高层次的抽象,进行分类或回归任务。
2. CNN 的代码实现(PyTorch 示例)
我们通过一个简单的图像分类任务来实现 CNN。这里以 CIFAR-10 数据集(一个包含 10 类物体的图像分类数据集)为例。
代码示例:使用 PyTorch 构建 CNN
import torch # 导入 PyTorch 库
import torch.nn as nn # 导入神经网络模块
import torch.optim as optim # 导入优化器模块
import torchvision # 导入 torchvision,用于数据加载和预处理
import torchvision.transforms as transforms # 导入 transforms 用于数据的变换
from torch.utils.data import DataLoader # 从 torch.utils.data 导入 DataLoader,用于加载数据集
# 定义 CNN 模型
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 第一层卷积层:输入通道 3(RGB 图像),输出通道 16,卷积核大小 3x3,padding=1 保持图像大小不变
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
# 第二层卷积层:输入通道 16,输出通道 32,卷积核大小 3x3,padding=1 保持图像大小不变
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
# 第三层卷积层:输入通道 32,输出通道 64,卷积核大小 3x3,padding=1 保持图像大小不变
self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
# 最大池化层:每 2x2 区域取最大值,步长为 2
self.pool = nn.MaxPool2d(2, 2)
# 修改后的全连接层:将卷积层的输出展平并连接到 512 个神经元
self.fc1 = nn.Linear(64 * 4 * 4, 512) # 修改为 1024(64 * 4 * 4)
# 输出层:输出 10 个类别(CIFAR-10 数据集有 10 类)
self.fc2 = nn.Linear(512, 10)
def forward(self, x):
# 卷积层 + 激活函数 + 池化
x = self.pool(torch.relu(self.conv1(x))) # 第一层卷积 + ReLU 激活 + 池化
x = self.pool(torch.relu(self.conv2(x))) # 第二层卷积 + ReLU 激活 + 池化
x = self.pool(torch.relu(self.conv3(x))) # 第三层卷积 + ReLU 激活 + 池化
# 展平数据(将三维的图像数据转换为一维的向量)
x = x.view(-1, 64 * 4 * 4) # 修正展平后的形状(batch_size, 64 * 4 * 4)
# 全连接层
x = torch.relu(self.fc1(x)) # ReLU 激活函数
x = self.fc2(x) # 输出 10 个类别的得分
return x # 返回模型的输出
# 数据加载和预处理
transform = transforms.Compose([ # 将多个变换操作组合在一起
transforms.ToTensor(), # 将图像转换为张量格式
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化处理,均值和标准差都为 0.5
])
# 下载并加载 CIFAR-10 数据集(训练集)
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=32, shuffle=True) # 创建 DataLoader,用于加载训练数据,批量大小为 32,数据顺序打乱
# 下载并加载 CIFAR-10 数据集(测试集)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = DataLoader(testset, batch_size=32, shuffle=False) # 创建 DataLoader,用于加载测试数据,批量大小为 32
# 初始化 CNN 模型
model = CNN()
# 使用交叉熵损失函数(用于分类任务)和 SGD 优化器(随机梯度下降)
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9) # 随机梯度下降优化器,学习率 0.001,动量 0.9
# 训练模型
for epoch in range(2): # 训练 2 个 epoch
running_loss = 0.0 # 用于记录每个小批量的损失
for i, data in enumerate(trainloader, 0): # 遍历训练数据,每个数据 batch 的索引为 i
inputs, labels = data # 输入数据和对应的标签
print(inputs.shape)
optimizer.zero_grad() # 清空之前的梯度
outputs = model(inputs) # 将输入传入模型进行前向传播
print(outputs.shape) # 调试输出
print(labels.shape) # 调试输出
loss = criterion(outputs, labels) # 计算损失(预测输出与实际标签之间的差距)
loss.backward() # 反向传播,计算梯度
optimizer.step() # 根据梯度更新模型参数
running_loss += loss.item() # 累加当前批次的损失
if i % 200 == 199: # 每 200 个小批量输出一次损失
print(f"[{epoch + 1}, {i + 1}] loss: {running_loss / 200:.3f}")
running_loss = 0.0 # 重置损失
print('Finished Training') # 训练完成
# 测试模型
correct = 0 # 记录正确预测的数量
total = 0 # 记录总共测试的样本数量
with torch.no_grad(): # 不计算梯度,节省内存
for data in testloader: # 遍历测试数据
inputs, labels = data # 输入数据和对应的标签
outputs = model(inputs) # 将输入传入模型进行前向传播
_, predicted = torch.max(outputs, 1) # 取得最大概率的类别(输出类别索引)
total += labels.size(0) # 增加总样本数
correct += (predicted == labels).sum().item() # 统计正确预测的数量
# 输出模型在测试集上的准确率
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total}%')
代码详解:
1. 导入必要的模块
import torch # 导入 PyTorch 库,用于构建和训练深度学习模型
import torch.nn as nn # 导入 nn 模块,它包含了构建神经网络所需的所有组件
import torch.optim as optim # 导入优化器模块,用于训练模型时优化参数
import torchvision # 导入 torchvision 库,它提供了常见的图像数据集和预训练模型
import torchvision.transforms as transforms # 导入 transforms 用于图像数据预处理
from torch.utils.data import DataLoader # 导入 DataLoader,用于加载数据集
torch
:是 PyTorch 的核心库,包含了许多功能,如张量计算、神经网络构建等。torch.nn
:包含构建神经网络所需的所有模块和层,如卷积层(Conv2d)、全连接层(Linear)等。torch.optim
:包含了多种优化器,如随机梯度下降(SGD)、Adam 等。torchvision
:包含用于处理计算机视觉任务的功能,如加载标准数据集(如 CIFAR-10)和使用预训练模型等。transforms
:提供了图像数据预处理的方法,如归一化、随机裁剪等。DataLoader
:用于从数据集中加载批量数据,支持并行加载和数据增强。
2. 定义 CNN 模型
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__() # 初始化父类 nn.Module 的构造函数
class CNN(nn.Module):
:定义了一个新的类CNN
,继承自nn.Module
。所有的 PyTorch 神经网络模型都需要继承自nn.Module
类。super(CNN, self).__init__()
:调用父类nn.Module
的构造函数,确保可以访问 PyTorch 提供的所有功能。比如网络参数的管理、梯度计算等。
3. 定义卷积层
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) # 第一层卷积层
self.conv1
:这是卷积层的定义,self
表示将该层定义为类的属性,这样在forward()
方法中可以调用。nn.Conv2d(3, 16, kernel_size=3, padding=1)
:3
:输入通道数,表示输入是 RGB 图像,所以有 3 个通道。16
:输出通道数,表示该卷积层将有 16 个不同的卷积核(过滤器),每个卷积核会提取不同的特征。kernel_size=3
:卷积核的大小是 3x3。padding=1
:表示在图像的四周加上 1 像素的零填充,这样卷积操作后,输出的图像尺寸保持不变。
4. 定义其他卷积层
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1) # 第二层卷积层
self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # 第三层卷积层
self.conv2
和self.conv3
:第二层和第三层卷积层的定义。self.conv2
:输入通道数为 16(上一层的输出),输出通道数为 32。self.conv3
:输入通道数为 32(上一层的输出),输出通道数为 64。
kernel_size=3
:每一层的卷积核大小均为 3x3,适用于图像的特征提取。
5. 定义池化层
self.pool = nn.MaxPool2d(2, 2) # 最大池化层:每 2x2 区域取最大值
self.pool
:定义了一个最大池化层。池化层用于减小特征图的尺寸,从而减少计算量。nn.MaxPool2d(2, 2)
:2
:池化窗口的大小为 2x2。2
:步幅为 2,表示池化窗口每次移动 2 个像素。
6. 定义全连接层
self.fc1 = nn.Linear(64 * 8 * 8, 512) # 全连接层:输入 64*8*8,输出 512 个神经元
self.fc2 = nn.Linear(512, 10) # 输出层:输出 10 个类别
self.fc1
:全连接层,连接卷积层提取的特征到 512 个神经元。64 * 8 * 8
:输入的特征数量。假设输入图像为 32x32,经过卷积和池化后,图像大小变为 8x8,且通道数为 64。512
:全连接层输出 512 个神经元。
self.fc2
:输出层,输出 10 个类别的得分,适用于 CIFAR-10 数据集(有 10 类)。512
:上一层的输出。10
:输出层的类别数。
7. 定义前向传播过程(forward
)
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x))) # 第一层卷积 + ReLU 激活 + 池化
x = self.pool(torch.relu(self.conv2(x))) # 第二层卷积 + ReLU 激活 + 池化
x = self.pool(torch.relu(self.conv3(x))) # 第三层卷积 + ReLU 激活 + 池化
x = self.pool(torch.relu(self.conv1(x)))
:将输入x
通过卷积层conv1
处理,应用 ReLU 激活函数,然后进行池化。重复这一操作对conv2
和conv3
进行处理。torch.relu()
:应用 ReLU 激活函数,增加非线性,使得网络能够学习复杂的模式。self.pool()
:进行池化操作,减小特征图尺寸。
x = x.view(-1, 64 * 8 * 8) # 展平数据(将三维的图像数据转换为一维的向量)
x.view(-1, 64 * 8 * 8)
:将三维的图像数据(64个通道,8x8的特征图)展平成一个一维的向量,准备送入全连接层。-1
代表批量大小(自动计算)。
x = torch.relu(self.fc1(x)) # ReLU 激活函数
x = self.fc2(x) # 输出 10 个类别的得分
x = torch.relu(self.fc1(x))
:将展平后的数据传入全连接层fc1
,并应用 ReLU 激活函数。x = self.fc2(x)
:将fc1
的输出传入最后的输出层fc2
,生成最终的分类结果(10 个类别的得分)。
return x # 返回模型的输出
return x
:返回网络的最终输出,即每个类别的得分。
8. 数据加载和预处理
transform = transforms.Compose([ # 数据预处理步骤
transforms.ToTensor(), # 将图像转换为 Tensor 格式
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化处理,均值和标准差都为 0.5
])
transforms.Compose
:将多个图像预处理操作组合在一起。transforms.ToTensor()
:将图像转换为 PyTorch 的 Tensor 格式,便于进行张量计算。transforms.Normalize()
:对图像进行归一化处理,将像素值转换为标准正态分布。
9. 训练模型
for epoch in range(2): # 训练 2 个 epoch
running_loss = 0.0 # 用于记录每个小批量的损失
for i, data in enumerate(trainloader, 0): # 遍历训练数据
inputs, labels = data # 获取输入图像和标签
optimizer.zero_grad() # 清空之前的梯度
outputs = model(inputs) # 前向传播,获取模型输出
loss = criterion(outputs, labels) # 计算损失
loss.backward() # 反向传播,计算梯度
optimizer.step() # 更新模型参数
running_loss += loss.item() # 累加当前批次的损失
if i % 200 == 199: # 每 200 个小批量输出一次损失
print(f"[{epoch + 1}, {i + 1}] loss: {running_loss / 200:.3f}")
running_loss = 0.0 # 重置损失
- 训练循环:每次训练时,我们都遍历所有小批量数据,计算损失并更新参数。
optimizer.zero_grad()
:清空上一步计算的梯度,避免累加。loss.backward()
:反向传播,计算梯度。optimizer.step()
:使用优化器更新网络的权重。
10. 测试模型
correct = 0
total = 0
with torch.no_grad():
# 不计算梯度,节省内存
for data in testloader: # 遍历测试数据
inputs, labels = data # 获取输入图像和标签
outputs = model(inputs) # 前向传播,获取模型输出
_, predicted = torch.max(outputs, 1) # 获取最大概率的类别索引
total += labels.size(0) # 增加总样本数
correct += (predicted == labels).sum().item() # 统计正确预测的数量
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total}%')
- 测试循环:计算模型在测试集上的准确率。
torch.no_grad()
:禁用梯度计算,节省内存。torch.max(outputs, 1)
:返回每个样本的最大输出概率对应的类别索引。(predicted == labels).sum().item()
:计算模型预测正确的数量。