卷积神经网络
卷积神经网络(CNN)
卷积神经网络(Convolutional Neural Network, CNN)是深度学习中一种非常重要的模型结构,特别擅长处理图像数据。CNN 通过卷积操作和池化操作来自动提取图像的特征,并使用全连接层进行分类或回归等任务。
1. 卷积神经网络的基本组成
CNN 主要由以下几部分组成:
- 卷积层(Convolutional Layer): 通过卷积操作提取输入数据中的局部特征。
- 激活函数(Activation Function): 通常使用 ReLU 函数,增加网络的非线性能力。
- 池化层(Pooling Layer): 通过下采样减少特征图的尺寸,保留重要信息。
- 全连接层(Fully Connected Layer): 将提取的特征用于分类或回归任务。
2. 卷积操作
定义: 卷积操作是用一个小的卷积核在输入数据上滑动,通过点积计算生成一个新的特征图。卷积核是一个固定大小的权重矩阵,它通过学习能够提取图像的边缘、纹理等特征。
解释: 可以将卷积操作想象成一个“特征探测器”,它在图像上滑动,寻找特定的模式(例如边缘、角点等)。
代码示例:
import torch
import torch.nn as nn
# 定义一个 2D 卷积层
conv_layer = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=1)
# 创建一个示例输入 (1, 1, 5, 5),表示 1 张单通道的 5x5 图像
input_data = torch.tensor([[[[1.0, 2.0, 3.0, 4.0, 5.0],
[6.0, 7.0, 8.0, 9.0, 10.0],
[11.0, 12.0, 13.0, 14.0, 15.0],
[16.0, 17.0, 18.0, 19.0, 20.0],
[21.0, 22.0, 23.0, 24.0, 25.0]]]])
# 进行卷积操作
output = conv_layer(input_data)
print(f"卷积层输出: \n{output}")
3. 激活函数(ReLU)
定义: ReLU(Rectified Linear Unit)是一种常用的激活函数,输出为输入值与零的最大值。
解释: ReLU 可以看作是“开启”或“关闭”神经元的开关,只有大于零的值才能通过。
代码示例:
relu = nn.ReLU()
# 使用 ReLU 激活函数
activated_output = relu(output)
print(f"ReLU 激活后的输出: \n{activated_output}")
4. 池化操作
定义: 池化操作通过下采样减少特征图的尺寸,常见的池化方式有最大池化(Max Pooling)和平均池化(Average Pooling)。
解释: 池化操作可以理解为信息压缩,将特征图中的重要信息保留,同时减少数据量和计算量。
代码示例:
pool_layer = nn.MaxPool2d(kernel_size=2, stride=2)
# 进行池化操作
pooled_output = pool_layer(activated_output)
print(f"池化层输出: \n{pooled_output}")
5. 全连接层
定义: 全连接层将池化后的特征图展平为一维向量,并通过线性变换得到最终的输出。它通常用于分类或回归任务。
解释: 全连接层就像是一个决策器,根据提取到的特征做出最终的判断。
代码示例:
flattened_output = pooled_output.view(-1, 1 * 2 * 2) # 展平
fc_layer = nn.Linear(4, 1) # 定义全连接层
final_output = fc_layer(flattened_output)
print(f"全连接层输出: {final_output.item()}")
6. 卷积神经网络的完整实现
以下是一个简单的 CNN 的完整实现,它包括一个卷积层、ReLU 激活函数、池化层和全连接层。
import torch
import torch.nn as nn
import torch.nn.functional as F
# 定义一个简单的 CNN
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.fc1 = nn.Linear(10 * 4 * 4, 1) # 假设输入图像为8x8大小
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = x.view(-1, 10 * 4 * 4) # 展平
x = self.fc1(x)
return x
# 创建网络实例
net = SimpleCNN()
# 创建示例输入 (batch_size=1, channels=1, height=8, width=8)
input_data = torch.rand(1, 1, 8, 8)
# 进行前向传播
output = net(input_data)
print(f"卷积神经网络输出: {output.item()}")
卷积神经网络(CNN)是图像处理领域中非常强大的工具。它通过卷积层提取局部特征,使用 ReLU 激活函数增加非线性能力,再通过池化层进行信息压缩,最后通过全连接层进行决策。CNN 的层次结构使它能够高效地处理图像数据,并在计算机视觉等领域表现出色。
项目示例
与我FCNN那篇文章相同,假设我们要开发一个简单的图像分类器,识别手写数字(比如经典的 MNIST 数据集,获取MNIST数据集的部分请看FCNN那篇文章中这部分内容)。我们将一步一步构建这个项目,并逐步深入讲解 CNN 的各个组成部分。
项目目标
我们将构建一个简单的卷积神经网络,输入是手写数字图片(28x28 像素),输出是图片中数字的类别(0 到 9)。我们将逐步搭建这个网络,并理解每个部分的作用。
1. 加载数据
首先,我们需要加载手写数字数据集。这些图片是灰度图像,每个像素点的值介于 0 和 255 之间。我们将把这些值标准化为 0 到 1 之间,以便更好地训练模型。
import torch
import torchvision
import torchvision.transforms as transforms
# 定义数据变换,将图片转换为张量,并标准化到 [0, 1] 范围
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))])
# 下载并加载训练数据集
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True)
# 下载并加载测试数据集
testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)
2. 卷积层:图像的特征探测器
卷积层是 CNN 的核心部分。它通过一个小的卷积核(filter)在图像上滑动,计算局部区域的特征。这个过程可以理解为扫描整张图片,寻找特定的模式(如边缘、角点等)。
2.1 第一个卷积层
我们将定义一个卷积层,输入是 28x28 的图像,输出是一个特征图。这相当于通过一组特征探测器,将图片中的重要信息提取出来。
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
# 定义第一个卷积层
self.conv1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, stride=1, padding=1)
def forward(self, x):
x = self.conv1(x)
return x
# 创建模型实例并测试输出
model = SimpleCNN()
sample_input = torch.rand(1, 1, 28, 28) # 单张28x28的灰度图像
output = model(sample_input)
print(f"卷积层输出形状: {output.shape}")
解释: 卷积层的输出形状是 [1, 16, 28, 28]
,意味着我们从一张灰度图像(单通道)提取出了 16 个不同的特征图。每个特征图都表示图像的某种特定模式,例如边缘或角点。
3. 激活函数:增加非线性
卷积操作本质上是线性的(即加权求和),但我们知道世界是非线性的。为此,我们使用激活函数(通常是 ReLU),将卷积层的输出映射到非线性空间中,从而使网络能够学习更复杂的模式。
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)
self.relu = nn.ReLU() # 添加 ReLU 激活函数
def forward(self, x):
x = self.relu(self.conv1(x))
return x
model = SimpleCNN()
output = model(sample_input)
print(f"ReLU 激活后的输出形状: {output.shape}")
解释: ReLU 函数将负值设为零,保留正值。这种激活使得网络能够更好地处理复杂的非线性关系。
4. 池化层:信息压缩器
池化层(通常使用最大池化)用于降低特征图的尺寸,同时保留重要信息。池化操作通过下采样,减少数据量和计算量,并控制过拟合。
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0) # 添加池化层
def forward(self, x):
x = self.pool(self.relu(self.conv1(x))) # 先卷积,再ReLU,最后池化
return x
model = SimpleCNN()
output = model(sample_input)
print(f"池化层后的输出形状: {output.shape}")
解释: 池化层将特征图的尺寸从 28x28 减小到 14x14(因为池化核的大小为 2x2,步长为 2),这意味着每次将相邻的 2x2 像素区域压缩为一个像素点,同时保留了最重要的信息。
5. 多层卷积和池化:逐层提取更高层次的特征
我们可以堆叠多个卷积层和池化层,逐步提取图像的更高层次特征。例如,第一层可能提取边缘特征,第二层提取更复杂的形状,最后一层提取整个图像的抽象特征。
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1) # 添加第二个卷积层
self.fc1 = nn.Linear(32 * 7 * 7, 128) # 添加全连接层
def forward(self, x):
x = self.pool(self.conv1(x))
x = self.pool(self.conv2(x))
x = x.view(-1, 32 * 7 * 7) # 展平为一维向量
x = self.fc1(x)
return x
model = SimpleCNN()
output = model(sample_input)
print(f"第二个卷积层后的输出形状: {output.shape}")
解释: 第二个卷积层提取了 32 个更复杂的特征图,并将尺寸进一步缩小到 7x7。最后,我们将这些特征展平成一维向量,为分类器做好准备。
6. 全连接层:分类器
全连接层接收从卷积层传递过来的高层次特征,并将它们映射到分类结果。对于手写数字识别问题,全连接层的输出是 10 个类别(数字 0 到 9)。
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
self.fc1 = nn.Linear(32 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10) # 添加输出层,用于分类
def forward(self, x):
x = self.pool(self.conv1(x))
x = self.pool(self.conv2(x))
x = x.view(-1, 32 * 7 * 7)
x = self.fc1(x)
x = self.fc2(x)
return x
model = SimpleCNN()
output = model(sample_input)
print(f"分类器输出形状: {output.shape}")
解释: 最后,全连接层将展平后的特征映射到 10 个类别,模型的最终输出是每个类别的得分(未经过激活的概率)。
7. 模型训练和测试
构建完模型后,我们可以使用交叉熵损失函数(适用于分类任务)和优化器(例如 Adam)来训练模型。训练过程中,模型会不断调整参数,以最小化训练集上的损失,并最终在测试集上进行评估。
import torch.optim as optim
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
for epoch in range(2): # 简单训练2个周期
running_loss = 0.0
for inputs, labels in trainloader:
optimizer.zero_grad() # 清空梯度
outputs = model(inputs) # 前向传播
loss = criterion(outputs, labels) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新参数
running_loss += loss.item()
print(f"第 {epoch + 1} 轮训练,损失: {running_loss / len(trainloader)}")
# 测试模型
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in testloader:
outputs = model(inputs)
_, predicted = torch.max(otputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f"在测试集上的准确率: {100 * correct / total}%")
在前面的内容中,我逐步讲解了卷积神经网络(CNN)的各个组成部分,并分段展示了代码。现在我将所有步骤的代码汇总成一个完整的项目示例。
完整的项目示例代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
# 1. 数据加载和预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
# 加载训练集
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True)
# 加载测试集
testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)
# 2. 定义卷积神经网络
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1) # 第一个卷积层
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0) # 池化层
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1) # 第二个卷积层
self.fc1 = nn.Linear(32 * 7 * 7, 128) # 第一个全连接层
self.fc2 = nn.Linear(128, 10) # 输出层
def forward(self, x):
x = self.pool(self.conv1(x)) # 卷积 -> ReLU -> 池化
x = self.pool(self.conv2(x)) # 卷积 -> ReLU -> 池化
x = x.view(-1, 32 * 7 * 7) # 展平为一维向量
x = self.fc1(x) # 全连接层1
x = self.fc2(x) # 输出层
return x
# 创建模型实例
model = SimpleCNN()
# 3. 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 4. 训练模型
for epoch in range(2): # 简单训练2个周期
running_loss = 0.0
for inputs, labels in trainloader:
optimizer.zero_grad() # 清空梯度
outputs = model(inputs) # 前向传播
loss = criterion(outputs, labels) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新参数
running_loss += loss.item()
print(f"第 {epoch + 1} 轮训练,损失: {running_loss / len(trainloader)}")
# 5. 测试模型
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in testloader:
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f"在测试集上的准确率: {100 * correct / total}%")
总结
通过这个项目示例,我们逐步搭建了一个简单的卷积神经网络,从卷积层、激活函数、池化层、到全连接层,最后完成图像分类任务。我们讲解了每个步骤的作用,并通过代码示例展示了如何实现和训练这个模型。卷积神经网络的这种层次化结构,使得它在图像处理任务中非常强大和有效。