一、CNN 是什么
CNN 是一类专门处理网格状数据的神经网络,最典型的是图像。它和普通全连接网络(MLP)最大的区别是:不把图像一次性拉平成一维向量,而是保留空间结构,用"卷积核"在图像上滑动提取局部特征。
可以把它理解成一套逐层加工流程:
浅层卷积:找边缘、颜色变化、简单纹理;
中层卷积:组合成角点、局部形状、图案;
深层卷积:形成物体部件甚至整体类别特征;
最后用全连接层或全局池化做分类。
CNN 适合图像的核心原因有三个:
局部连接:每个神经元只看图像的一小块区域,符合图像局部相关性。
权值共享:同一个卷积核在整张图上滑动复用,参数量大幅减少。
平移不变性:物体左右移动一点,卷积核仍能在新位置检测到相似特征。
二、CNN 的核心组件
- 卷积层
卷积层里有一组"卷积核"也叫滤波器。每个卷积核是一个小窗口,比如 3×3 或 5×5。
工作时,卷积核在输入图像上从左到右、从上到下滑动。每到一个位置,就把窗口内的输入值和卷积核对应位置的权重逐项相乘再相加,再加一个偏置,得到一个输出值。所有位置的结果拼起来,形成一张"特征图"。
一个卷积核提取一种特征。多个卷积核就得到多张特征图,表示不同特征。
关键参数:
卷积核大小:常见 3×3、5×5。3×3 最常用。
步幅 stride:每次滑动几格。步幅为 1 通常保留较多空间信息;步幅为 2 会缩小特征图。
填充 padding:在输入边缘补零。常用 padding=1 配合 3×3 卷积核,让输出尺寸和输入接近。
输入通道和输出通道:灰度图输入通道为 1,彩色图为 3;输出通道数等于该层卷积核个数。
- 激活函数
卷积后通常接 ReLU。它把负数变零,正数保留。作用是引入非线性,让网络能学习复杂模式,同时计算简单、缓解梯度消失。
- 池化层
池化用于降采样,减少特征图尺寸和计算量,同时增强一定程度的平移鲁棒性。
最常见的是最大池化:把一个小区域里的最大值保留下来。比如 2×2 最大池化,每次看 2×2 区域,只留最大值,特征图长宽减半。
池化没有可学习参数。
- 批归一化
批归一化在训练时对每层输出做标准化,让训练更稳定、可以使用更大学习率。通常放在卷积层和激活函数之间,或者卷积后接批归一化再接 ReLU。
- 全连接层
经过多轮卷积和池化后,特征图被拉平,送入全连接层做最终分类。现代网络中有时用全局平均池化代替拉平,减少参数量。
- Dropout
训练时随机丢弃一部分神经元,防止过拟合。通常放在全连接层之间。推理时关闭。
- 残差连接
深层 CNN 常用残差连接:把输入直接加到后面某层输出上,让梯度更容易传回去。ResNet 就是典型代表。入门可以先不写,但要知道这是深层网络的关键技巧。
三、CNN 的训练流程
和普通神经网络一样:
前向传播:输入图像经过卷积、激活、池化、全连接,得到各类别得分。
计算损失:把预测得分和真实标签比较。多分类常用交叉熵损失。
反向传播:从损失往回算每个卷积核权重和全连接权重的梯度。
参数更新:优化器根据梯度调整权重。常用 Adam 或带动量的随机梯度下降。
反复迭代:多轮训练,观察验证集准确率和损失。
推理:新图片前向传播一次,取得分最高的类别作为预测。
四、一个典型 CNN 结构
以 Fashion-MNIST 灰度图分类为例,输入是 28×28 单通道图片,共 10 类。
结构可以这样设计:
卷积层:输入 1 通道,输出 32 通道,3×3 卷积核,padding=1;
ReLU;
最大池化 2×2;
卷积层:32 通道到 64 通道,3×3,padding=1;
ReLU;
最大池化 2×2;
拉平;
全连接层到 128 维;
ReLU;
Dropout;
全连接层到 10 类输出。
两次 2×2 池化后,28×28 变成 7×7,所以拉平后是 64×7×7 个特征。
五、PyTorch 完整实例:Fashion-MNIST 图像分类
下面代码可直接运行。依赖:
bash
pip install torch torchvision
完整代码:
python
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
1. 设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("使用设备:", device)
2. 数据预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.2860,), (0.3530,))
])
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=256, shuffle=False)
3. 定义 CNN
class SimpleCNN(nn.Module):
def init (self, num_classes=10):
super().init ()
self.features = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(64 * 7 * 7, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, num_classes)
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
model = SimpleCNN().to(device)
4. 损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
5. 训练和评估函数
def train_one_epoch(model, loader, loss_fn, optimizer, device):
model.train()
total_loss = 0.0
correct = 0
total = 0
for images, labels in loader:
images = images.to(device)
labels = labels.to(device)
optimizer.zero_grad()
logits = model(images)
loss = loss_fn(logits, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
preds = logits.argmax(dim=1)
correct += (preds == labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
@torch.no_grad()
def evaluate(model, loader, loss_fn, device):
model.eval()
total_loss = 0.0
correct = 0
total = 0
for images, labels in loader:
images = images.to(device)
labels = labels.to(device)
logits = model(images)
loss = loss_fn(logits, labels)
total_loss += loss.item() * images.size(0)
preds = logits.argmax(dim=1)
correct += (preds == labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
6. 开始训练
epochs = 5
for epoch in range(epochs):
train_loss, train_acc = train_one_epoch(
model, train_loader, loss_fn, optimizer, device
)
test_loss, test_acc = evaluate(model, test_loader, loss_fn, device)
print(
f"Epoch {epoch + 1}/{epochs} | "
f"训练损失 {train_loss:.4f} | 训练准确率 {train_acc:.4f} | "
f"测试损失 {test_loss:.4f} | 测试准确率 {test_acc:.4f}"
)
7. 单张图片预测示例
class_names = [
"T-shirt/top", "Trouser", "Pullover", "Dress", "Coat",
"Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"
]
model.eval()
image, label = test_dataset0
with torch.no_grad():
logits = model(image.unsqueeze(0).to(device))
pred = logits.argmax(dim=1).item()
print("真实类别:", class_nameslabel)
print("预测类别:", class_namespred)
8. 显示图片
plt.imshow(image.squeeze(), cmap="gray")
plt.title(f"true: {class_nameslabel}, pred: {class_namespred}")
plt.axis("off")
plt.show()
六、代码关键点解释
datasets.FashionMNIST:自动下载并加载数据集。
ToTensor():把图片转成 PyTorch 张量,并把像素缩放到 0 到 1。
Normalize:按数据集均值和标准差做标准化,训练更稳定。
Conv2d:二维卷积层。in_channels 是输入通道,out_channels 是卷积核个数。
padding=1 配合 3×3 卷积核,让卷积后长宽不变。
MaxPool2d(kernel_size=2):2×2 最大池化,特征图长宽减半。
Flatten():把多维特征图拉平成一维,送进全连接层。
CrossEntropyLoss:多分类常用损失,内部包含 Softmax 逻辑,所以模型输出直接是 logits,不再手动加 Softmax。
optimizer.zero_grad():清空上一轮梯度。
loss.backward():反向传播。
optimizer.step():更新参数。
model.train() 和 model.eval():切换训练/推理模式,影响 Dropout 和批归一化行为。
七、一个更小的 NumPy 直观例子:卷积核如何滑动
如果想理解"卷积核在图像上滑动"这件事,可以看这个简化演示。它不做训练,只展示一次二维卷积操作。
python
import numpy as np
5x5 输入,模拟一张小图
image = np.array([
1, 2, 3, 0, 1,
0, 1, 2, 3, 1,
1, 0, 1, 2, 0,
2, 1, 0, 1, 3,
1, 2, 1, 0, 2
], dtype=float)
3x3 卷积核,模拟边缘检测
kernel = np.array([
1, 0, -1,
1, 0, -1,
1, 0, -1
], dtype=float)
def conv2d_simple(img, k):
kh, kw = k.shape
out_h = img.shape0 - kh + 1
out_w = img.shape1 - kw + 1
out = np.zeros((out_h, out_w))
for i in range(out_h):
for j in range(out_w):
window = img[i:i+kh, j:j+kw]
# 逐元素相乘再求和
out[i, j] = np.sum(window * k)
return out
feature_map = conv2d_simple(image, kernel)
print("输入:\n", image)
print("卷积核:\n", kernel)
print("输出特征图:\n", feature_map)
这个例子里,卷积核每滑动到一个 3×3 窗口,就把窗口和卷积核逐项相乘再求和,得到一个输出值。所有输出值组成特征图。实际 CNN 会加上偏置、激活函数,并且有很多个卷积核。
八、训练 CNN 的实用建议
先跑小数据集:Fashion-MNIST、CIFAR-10 很适合入门。
卷积核优先用 3×3:堆两个 3×3 通常比一个大卷积核更灵活。
通道数逐层增加:比如 32、64、128,特征越深通道越多。
池化或步幅卷积降尺寸:每降一次尺寸,通道数可适当增加。
全连接层前加 Dropout:小数据集上防过拟合。
学习率先用 1e-3 配 Adam:不收敛再调小。
看验证集:训练准确率很高但测试不涨,说明过拟合。
数据增强:随机裁剪、翻转、颜色扰动能显著提升泛化。
不要一开始堆很深:入门网络 2 到 4 个卷积层足够。
理解输出尺寸:卷积核大小、填充、步幅、池化共同决定特征图尺寸;常用"3×3 卷积加 padding=1 保持尺寸,2×2 池化减半"。
九、总结
CNN 的核心是:用卷积核在图像上滑动提取局部特征,通过多层堆叠形成从低级到高级的特征表示,再用池化降采样,最后分类。它比全连接网络更适合图像,因为局部连接、权值共享和层级特征提取大幅减少参数量并保留空间结构。上面的 PyTorch 例子是一个完整可训练的最小 CNN 流程,适合先跑通,再逐步加入批归一化、数据增强、残差连接等进阶组件。