目录
1. 前言
在人工智能和深度学习领域,卷积神经网络(CNN,Convolutional Neural Networks)已经成为图像识别和计算机视觉任务的核心技术。从人脸识别到自动驾驶,CNN的应用无处不在。此次博客将深入浅出地介绍CNN的基本原理,并通过一个完整的实例来学习CNN模型。
2. 卷积神经网络基本概念
卷积神经网络是一种专门用于处理具有网格结构数据(如图像)的深度学习模型。它的核心思想是通过卷积操作自动提取数据中的特征,从而实现对图像的分类、识别和分割等任务。
2.1 CNN的核心组件
-
卷积层(Convolutional Layer)
卷积层是CNN的核心,它通过卷积核(filter)在输入数据上滑动,提取局部特征。卷积操作可以捕捉图像中的边缘、纹理等信息。 -
池化层(Pooling Layer)
池化层用于降低数据的维度,减少计算量,同时保留重要特征。常见的池化方法包括最大池化(Max Pooling)和平均池化(Average Pooling)。 -
激活函数(Activation Function)
激活函数为网络引入非线性,使得模型能够学习复杂的特征。常用的激活函数包括ReLU、Sigmoid和Tanh。 -
全连接层(Fully Connected Layer)
全连接层将卷积层和池化层提取的特征映射到输出空间,用于最终的分类或回归任务。
2.2 CNN的工作原理
CNN通过多层卷积和池化操作逐步提取图像的特征。每一层的卷积核学习不同的特征,从低级特征(如边缘)到高级特征(如形状和纹理)。通过逐层特征提取,CNN能够高效地处理图像数据。
3. CNN实例
由于手写数字识别太经典且在之前文章讲过,这里换一个实例进行学习。
接下来,我们将通过一个完整的实例来实现一个CNN模型,用于识别CIFAR-10数据集中的图像。
3.1 导入必要的库
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
3.2 数据预处理
# 定义数据转换
transform = transforms.Compose([
transforms.ToTensor(), # 将图像转换为Tensor
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 归一化处理
])
# 加载CIFAR-10数据集
train_dataset = torchvision.datasets.CIFAR10(root='./data',
train=True,
transform=transform,
download=True)
test_dataset = torchvision.datasets.CIFAR10(root='./data',
train=False,
transform=transform)
# 创建数据加载器
train_loader = DataLoader(dataset=train_dataset,
batch_size=64,
shuffle=True)
test_loader = DataLoader(dataset=test_dataset,
batch_size=64,
shuffle=False)
下载速度慢建议去官网下载模型,CIFAR-10数据官网 ,下载好后去data文件夹解压缩。
3.3 定义CNN模型
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 第一个卷积块
self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1, padding=1)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
# 第二个卷积块
self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
# 第三个卷积块
self.conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1)
self.relu3 = nn.ReLU()
self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
# 全连接层
self.fc = nn.Linear(128 * 4 * 4, 10) # 输入特征维度为128*4*4,输出为10个类别
def forward(self, x):
# 第一个卷积块
x = self.conv1(x)
x = self.relu1(x)
x = self.pool1(x)
# 第二个卷积块
x = self.conv2(x)
x = self.relu2(x)
x = self.pool2(x)
# 第三个卷积块
x = self.conv3(x)
x = self.relu3(x)
x = self.pool3(x)
# 展平后输入全连接层
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
当你定义一个继承自nn.Module
的类时,你需要重写forward
方法。这个方法定义了网络的前向传播逻辑,即输入数据如何通过网络的各个层来生成输出。
x = x.view(x.size(0), -1)这行代码的作用是将张量 x
展平为一个二维张量,其中第一个维度保持不变,第二个维度自动计算。
nn.Conv2d函数解释为:
1. in_channels
-
含义:输入数据的通道数。
-
作用:指定输入特征图的深度(即通道数)。例如,对于 RGB 图像,通道数为3。
-
示例:
in_channels=3
表示输入数据是3通道的(如 RGB 图像)。
2. out_channels
-
含义:输出数据的通道数。
-
作用:指定卷积层输出的特征图数量。每个特征图由一个卷积核生成。
-
示例:
out_channels=32
表示卷积层输出32个特征图。
3. kernel_size
-
含义:卷积核的大小。
-
作用:指定卷积核的高度和宽度。通常是奇数(如3x3、5x5),以便有一个中心点。
-
示例:
kernel_size=3
表示卷积核的大小为3x3。
4. stride
-
含义:卷积核在输入数据上滑动的步长。
-
作用:控制卷积核每次移动的像素数。较大的步长会减少输出特征图的尺寸。
-
示例:
stride=1
表示卷积核每次移动1个像素。
5. padding
-
含义:在输入数据周围添加的零填充(zero padding)的宽度。
-
作用:防止输出特征图的尺寸过小,或保持输出特征图的尺寸与输入一致。
-
示例:
padding=1
表示在输入数据的上下左右各添加1行/列的零填充。
3.4 训练模型
# 初始化模型
model = CNN()
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
num_epochs = 10
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for images, labels in train_loader:
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}')
(1)nn.CrossEntropyLoss()
实际上是将 LogSoftmax
和 Negative Log Likelihood Loss
合并在一起:
-
LogSoftmax:将模型的输出转换为概率分布。
-
Negative Log Likelihood:计算预测分布和真实分布之间的损失。
(2)PyTorch在内部对nn.Module
类的__call__
方法进行了重写。当你调用model(images)
时,实际上是在调用model.__call__(images)
。这个__call__
方法会自动调用forward
方法,并处理一些额外的任务,比如自动求导的设置和梯度的追踪。
(3)Adam 为每个参数维护一个独立的学习率,能够自动调整学习率的大小。
(4)反向传播函数用法
-
optimizer.zero_grad()
:清零梯度。 -
loss.backward()
:计算梯度。 -
optimizer.step()
:更新参数。
(5)神经网络的参数更新是在整个批次的梯度计算完成后进行的。具体步骤如下:
-
前向传播:将整个批次的64个图像输入网络,计算每个图像的输出。
-
计算损失:根据每个图像的输出和真实标签,计算整个批次的平均损失。
-
反向传播:计算损失对网络参数的梯度。
-
参数更新:根据计算出的梯度更新网络参数。
3.5 测试模型
# 测试模型
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Test Accuracy: {100 * correct / total:.2f}%')
详细解析如下:
1. model.eval()
这行代码将模型设置为评估模式。在评估模式下,模型的行为会有所不同:
-
Dropout层:在训练时,Dropout层会随机失活一些神经元以防止过拟合,但在评估时,所有神经元都会被激活。
-
BatchNorm层:在训练时,BatchNorm层会使用当前批次的均值和方差进行归一化,而在评估时,它会使用整个训练集的均值和方差。
2. 初始化变量
correct = 0
total = 0
-
correct
:用于记录预测正确的样本数。 -
total
:用于记录总样本数。
3. with torch.no_grad():
这行代码进入一个上下文管理器,禁用梯度计算。在测试阶段,不需要计算梯度,因此可以使用 torch.no_grad()
来节省内存和计算资源。
4. 遍历测试集
for images, labels in test_loader:
这行代码遍历测试集中的每个批次的数据。images
是输入图像,labels
是对应的真实标签。
5. 前向传播
outputs = model(images)
这行代码将输入图像传递给模型,进行前向传播,得到模型的输出。
6. 获取预测结果
_, predicted = torch.max(outputs.data, 1)
-
outputs.data
是模型输出的原始分数。 -
torch.max(outputs.data, 1)
返回每个样本的最大值及其索引。索引即为预测的类别。
7. 更新统计变量
total += labels.size(0)
correct += (predicted == labels).sum().item()
-
labels.size(0)
是当前批次的样本数。 -
predicted == labels
比较预测结果和真实标签,返回一个布尔张量。 -
.sum().item()
计算预测正确的样本数,并将其转换为Python整数。
8. 计算准确率
print(f'Test Accuracy: {100 * correct / total:.2f}%')
这行代码计算并打印测试集上的准确率。
4. 完整代码(方便运行调试)
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# 定义数据转换
transform = transforms.Compose([
transforms.ToTensor(), # 将图像转换为Tensor
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 归一化处理
])
# 加载CIFAR-10数据集
train_dataset = torchvision.datasets.CIFAR10(root='./data',
train=True,
transform=transform,
download=False)
test_dataset = torchvision.datasets.CIFAR10(root='./data',
train=False,
transform=transform)
# 创建数据加载器
train_loader = DataLoader(dataset=train_dataset,
batch_size=64,
shuffle=True)
test_loader = DataLoader(dataset=test_dataset,
batch_size=64,
shuffle=False)
class CNN(nn.Module):
def __init__(self):
super().__init__()
# 第一个卷积块
self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1, padding=1)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
# 第二个卷积块
self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
# 第三个卷积块
self.conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1)
self.relu3 = nn.ReLU()
self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
# 全连接层
self.fc = nn.Linear(128 * 4 * 4, 10) # 输入特征维度为128*4*4,输出为10个类别
def forward(self, x):
# 第一个卷积块
x = self.conv1(x)
x = self.relu1(x)
x = self.pool1(x)
# 第二个卷积块
x = self.conv2(x)
x = self.relu2(x)
x = self.pool2(x)
# 第三个卷积块
x = self.conv3(x)
x = self.relu3(x)
x = self.pool3(x)
# 展平后输入全连接层
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
# 初始化模型
model = CNN()
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
num_epochs = 10
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
for images, labels in train_loader:
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {running_loss / len(train_loader):.4f}')
# 测试模型
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Test Accuracy: {100 * correct / total:.2f}%')
5. 总结
卷积神经网络(CNN)是一种强大的深度学习模型,特别适合处理图像数据。通过卷积层和池化层的组合,CNN能够高效地提取图像中的特征。本文通过一个CIFAR-10图像分类的实例,详细介绍了CNN的基本原理和实现方法。
未来,随着硬件性能的提升和算法的改进,CNN将在更多领域发挥重要作用,如自动驾驶、医疗影像分析和自然语言处理等。希望本文能帮助读者对CNN有一个全面的理解,并激发进一步探索的兴趣。我是橙色小博,关注我,在人工智能领域学习进步。