深度学习系列教程之第七章CNN
·
🔥第七章 卷积神经网络(CNN零基础秒懂)
我保证:不用任何复杂公式,不用任何晦涩术语,用生活中的例子把 CNN 讲透。看完你不仅能懂,还能给别人讲明白。
一、先搞懂一个最根本的问题:为什么不用全连接网络处理图片?
1. 全连接网络处理图片的两个致命缺陷
假设我们要识别一张 28×28 像素的手写数字图片(就是 MNIST 数据集):
缺陷 1:参数爆炸,电脑直接跑不动
- 全连接网络会先把图片拉成一条长长的一维向量:28×28=784 个像素
- 如果第一个隐藏层有 100 个神经元,那么这一层的参数数量是:784×100 + 100 = 78500 个
- 这还只是第一层!如果网络深一点,参数会指数级增长,普通电脑根本跑不动
缺陷 2:丢失了图片最重要的信息 —— 空间结构
- 全连接网络把图片拉成一维后,像素之间的相邻关系就没了
- 比如:“眼睛在鼻子上面” 这个重要的空间信息,全连接网络完全看不到
- 它只能看到一堆孤立的数字,根本不知道这是一张图片
2. CNN 的天才解决思路
CNN 就像我们人眼看东西:
- 我们不会一下子看整张图片,而是先看局部的小细节(边缘、角落、纹理)
- 然后把这些小细节组合起来,形成更大的特征(眼睛、鼻子、嘴巴)
- 最后再把这些大特征组合起来,识别出这是一张人脸
CNN 完全模仿了这个过程:
- 卷积层:负责提取局部小特征(边缘、纹理)
- 池化层:负责把特征压缩,保留最重要的信息
- 全连接层:负责把所有特征组合起来,做最终的分类
二、卷积层(CNN 的灵魂!必须彻底搞懂)
1. 用一个生活例子理解卷积
想象你在看一幅巨大的油画:
- 你手里拿着一个3×3 厘米的放大镜
- 你从油画的左上角开始,每次向右移动 1 厘米,看完一行就向下移动 1 厘米
- 每移动一次,你就把放大镜里看到的内容记下来
- 最后,你把所有记下来的内容拼在一起,就得到了这幅油画的 “特征图”
这个过程就是卷积!
- 那个3×3 厘米的放大镜,就是卷积核(Kernel/Filter)
- 每次移动的距离,就是步长(Stride)
- 最后拼出来的 “特征图”,就是卷积层的输出
2. 用数字例子一步一步算卷积(看完你就会了)
假设我们有一张 5×5 像素的灰度图片(只有 1 个通道):
图片像素值:
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
我们用一个 3×3 的卷积核 来做卷积:
卷积核的值:
1 0 -1
1 0 -1
1 0 -1
卷积的计算规则:
- 把卷积核放在图片的左上角,和图片对应位置的像素相乘
- 把所有相乘的结果加起来,得到输出特征图的第一个值
- 把卷积核向右移动 1 个像素(步长 = 1),重复上面的计算
- 看完一行后,向下移动 1 个像素,继续计算
第一步计算(左上角):
1×1 + 2×0 + 3×(-1) +
6×1 + 7×0 + 8×(-1) +
11×1 + 12×0 + 13×(-1)
= 1 + 0 - 3 + 6 + 0 - 8 + 11 + 0 - 13
= -6
第二步计算(向右移动 1 个像素):
2×1 + 3×0 + 4×(-1) +
7×1 + 8×0 + 9×(-1) +
12×1 + 13×0 + 14×(-1)
= 2 + 0 - 4 + 7 + 0 - 9 + 12 + 0 - 14
= -6
以此类推,最后我们会得到一个 3×3 的输出特征图:
-6 -6 -6
-6 -6 -6
-6 -6 -6
3. 卷积层的三个关键参数
(1)卷积核大小(Kernel Size)
- 常用:3×3(99% 的情况都用这个)、5×5
- 为什么用 3×3?因为两个 3×3 卷积核的感受野和一个 5×5 的一样,但参数更少,计算更快
(2)步长(Stride)
- 卷积核每次移动的像素数
- 步长 = 1:输出特征图和输入图片差不多大
- 步长 = 2:输出特征图的宽高会变成原来的一半
(3)填充(Padding)
- 在图片的边缘填充一圈 0
- 为什么要填充?因为如果不填充,图片边缘的像素只会被计算一次,中间的像素会被计算多次,边缘信息会丢失
- 填充 = 1(3×3 卷积核):输出特征图的宽高和输入图片完全一样!
4. 多通道卷积(彩色图片怎么处理?)
彩色图片有 3 个通道(RGB),所以卷积核也必须有 3 个通道:
- 每个通道的卷积核和图片对应通道做卷积
- 最后把三个通道的结果加起来,得到输出特征图的一个值
一个卷积核只能提取一种特征,所以我们通常会用多个卷积核:
- 比如用 6 个卷积核,就会输出 6 个特征图
- 每个特征图对应一种不同的特征(比如一个提取水平边缘,一个提取垂直边缘)
5. 卷积层的神奇特性
(1)参数共享
- 同一个卷积核在整张图片上滑动,所有位置都用相同的权重
- 这就是 CNN 参数少的根本原因!
- 比如一个 3×3 的卷积核,不管图片多大,它的参数永远只有 9 个(加偏置是 10 个)
(2)局部感受野
- 卷积核每次只看图片的一个小局部
- 这和我们人眼看东西的方式完全一致,非常适合提取局部特征
三、池化层(下采样):给图片 “减肥”
1. 池化层是干什么的?
池化层的作用非常简单:把特征图变小,保留最重要的信息。
就像你把一张 1000×1000 的图片缩小成 500×500,虽然图片变小了,但你依然能认出上面的内容。
2. 两种最常用的池化
(1)最大池化(Max Pooling)—— 99% 的情况都用这个
- 把特征图分成一个个 2×2 的小方块
- 每个小方块里取最大的那个值,作为输出
- 作用:保留最突出的特征,比如最亮的像素
数字例子:
输入特征图(4×4):
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
分成4个2×2的小方块:
[1 2; 5 6] → 最大值6
[3 4; 7 8] → 最大值8
[9 10; 13 14] → 最大值14
[11 12; 15 16] → 最大值16
输出特征图(2×2):
6 8
14 16
(2)平均池化(Average Pooling)
- 每个小方块里取平均值
- 作用:保留整体的平均信息,用得比较少
3. 池化层的作用
- 大幅减少参数数量:2×2 池化会让特征图的宽高变成原来的一半,参数变成原来的 1/4
- 防止过拟合:丢掉一些不重要的细节,让模型更关注整体特征
- 特征鲁棒性:即使图片有轻微的平移或旋转,池化后的特征依然差不多
四、全连接层:最后的分类
卷积层和池化层负责提取特征,全连接层负责把这些特征组合起来,做最终的分类。
在进入全连接层之前,我们需要把多维的特征图展平成一维向量:
- 比如经过卷积和池化后,我们得到了 6 个 5×5 的特征图
- 展平后就是:6×5×5 = 150 个特征
- 然后把这 150 个特征输入到全连接层,最后输出 10 个类别的概率(对应 0-9 十个数字)
五、一个完整的 CNN 长什么样?(LeNet-5)
LeNet-5 是 1998 年 Yann LeCun 提出的,是第一个真正成功的 CNN,专门用来识别手写数字。
它的结构非常简单,是所有现代 CNN 的鼻祖:
输入(1×28×28)
↓
卷积层1(6个5×5卷积核,padding=2)→ 输出6×28×28
↓
Sigmoid激活
↓
平均池化层1(2×2)→ 输出6×14×14
↓
卷积层2(16个5×5卷积核)→ 输出16×10×10
↓
Sigmoid激活
↓
平均池化层2(2×2)→ 输出16×5×5
↓
展平 → 16×5×5 = 400个特征
↓
全连接层1(400→120)
↓
Sigmoid激活
↓
全连接层2(120→84)
↓
Sigmoid激活
↓
全连接层3(84→10)
↓
输出(10个类别的概率)
六、经典 CNN 模型简介(知道名字和特点就行)
| 模型 | 年份 | 核心特点 | 历史地位 |
|---|---|---|---|
| AlexNet | 2012 | 用了 ReLU 激活、Dropout、GPU 加速 | 深度学习爆发的标志,证明了 CNN 在图像识别上的威力 |
| VGG | 2014 | 全部用 3×3 卷积核,结构非常简单 | 最经典的 CNN,至今仍被广泛使用 |
| GoogLeNet | 2014 | Inception 模块,用多种尺寸卷积同时提取特征 | 大幅减少了参数数量 |
| ResNet | 2015 | 残差连接(跳层连接) | 解决了 “网络越深效果越差” 的问题,让训练上百层的网络成为可能 |
七、🔥 完整实战:用 CNN 识别 Fashion MNIST 服装
下面是可直接复制运行的完整代码,加了超详细的注释。
1. 导入库
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
2. 数据预处理和加载
# 定义数据预处理
transform = transforms.Compose([
transforms.ToTensor(), # 把图片转成张量,并且归一化到[0,1]
transforms.Normalize((0.5,), (0.5,)) # 进一步归一化到[-1,1]
])
# 加载Fashion MNIST数据集
train_dataset = datasets.FashionMNIST(
root='./data', train=True, download=True, transform=transform
)
test_dataset = datasets.FashionMNIST(
root='./data', train=False, download=True, transform=transform
)
# 创建数据加载器
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# 类别名称
classes = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
3. 定义 CNN 模型
class CNN(nn.Module):
def __init__(self):
super().__init__()
# 第一个卷积块:卷积 + ReLU + 最大池化
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, 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, padding=1)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
# 全连接层
self.fc1 = nn.Linear(64 * 7 * 7, 128) # 64个通道,7×7的特征图
self.relu3 = nn.ReLU()
self.fc2 = nn.Linear(128, 10) # 10个类别
def forward(self, x):
# 前向传播
x = self.pool1(self.relu1(self.conv1(x)))
x = self.pool2(self.relu2(self.conv2(x)))
x = x.view(-1, 64 * 7 * 7) # 展平
x = self.relu3(self.fc1(x))
x = self.fc2(x)
return x
4. 训练模型
# 定义设备(优先用GPU)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
# 初始化模型、损失函数和优化器
model = CNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练轮数
epochs = 10
train_losses = []
test_losses = []
test_accs = []
# 训练循环
for epoch in range(epochs):
model.train()
train_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item() * images.size(0)
# 计算平均训练损失
train_loss = train_loss / len(train_dataset)
train_losses.append(train_loss)
# 测试模型
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item() * images.size(0)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
test_loss = test_loss / len(test_dataset)
test_acc = correct / total
test_losses.append(test_loss)
test_accs.append(test_acc)
# 打印结果
print(f'Epoch {epoch+1}/{epochs}')
print(f'训练损失: {train_loss:.4f}, 测试损失: {test_loss:.4f}, 测试准确率: {test_acc:.4f}')
5. 绘制训练曲线
plt.figure(figsize=(12, 4))
# 损失曲线
plt.subplot(1, 2, 1)
plt.plot(train_losses, label='训练损失')
plt.plot(test_losses, label='测试损失')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.title('损失曲线')
# 准确率曲线
plt.subplot(1, 2, 2)
plt.plot(test_accs, label='测试准确率')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.title('准确率曲线')
plt.tight_layout()
plt.show()
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)