《Python AI入门》第8章 给机器一双眼睛——CNN卷积神经网络与图像分类实战
章节导语
“上一章我们用全连接网络(MLP)粗暴地把图片拉成了一条直线,虽然识别出了简单的手写数字,但这样做丢失了图片最重要的东西——空间结构。”
试想一下,如果把一只猫的照片倒过来放,或者把猫从图片的左上角移到右下角,普通的全连接网络可能就立刻“瞎”了。因为它死记硬背的是像素的位置,而不是“猫耳朵”或“猫胡须”这样的特征。
人类看东西不是一个像素一个像素看的,而是先看局部特征(边缘、形状、纹理),然后把这些特征组合起来。
本章我们将学习模仿人类视觉系统的神器——卷积神经网络(CNN)。它是计算机视觉(Computer Vision)的绝对统治者。更重要的是,我们将学习一种名为“迁移学习”的工程化技巧,让你不需要几百万张图片,也能训练出专业级的图像识别模型。
8.1 学习目标
在学完本章后,你将能够:
-
理解CNN原理:用直观的图解掌握卷积(Convolution)和池化(Pooling)的作用。
-
数据增强工程:掌握
torchvision.transforms,通过旋转、裁剪等手段让有限的数据集“变大”。 -
掌握迁移学习:学会“站在巨人的肩膀上”,加载预训练模型(如 ResNet),并针对自己的任务进行微调(Fine-tuning)。
-
处理自定义数据:熟练使用
ImageFolder加载你自己收集的图片文件。 -
实战落地:构建一个智能垃圾分类系统,能识别玻璃、纸张、金属和塑料。
8.2 为什么全连接网络(MLP)看图不行?
在第7章的MNIST案例中,我们的输入层有 个神经元。 如果我们要识别一张普通的手机照片(假设
像素,3个颜色通道),输入层就需要
万个神经元! 如果第一层隐藏层只有1000个神经元,那么这就需要
亿个权重参数。这会让任何一台计算机的内存瞬间爆炸。
更糟糕的是,把图片拉平成一条线,就破坏了像素之间的邻域关系。CNN 的出现,就是为了解决参数爆炸和保留空间特征。
8.3 CNN的核心零件:卷积与池化
别被数学名词吓到,CNN 的逻辑其实非常贴近生活。
8.3.1 卷积层(Convolution):特征扫描仪
想象你在玩“找茬”游戏,你拿著一个小手电筒(比如 的方框)在图片上从左到右、从上到下滑动。这个小方框就叫卷积核(Kernel)或滤波器(Filter)。
-
卷积核里存的是什么?是一些特定的模式。
-
有的核专门找“竖线”,有的找“横线”,有的找“圆弧”。
-
当卷积核滑动到和它长得很像的区域时,它会激活,输出一个很大的数值。
本质:卷积就是在做特征提取。第一层卷积提取边缘,第二层提取形状(眼睛、鼻子),第三层提取物体(猫脸)。
8.3.2 池化层(Pooling):信息压缩机
提取完特征后,数据量还是很大。我们需要“挑重点”。 最大池化(Max Pooling) 是最常用的方法。它在一个 的区域内,只保留数值最大的那个点。
作用:
-
减小尺寸:数据量减少75%,计算更快。
-
抗干扰:不管猫稍微往左偏一点还是往右偏一点,只要那个区域里有猫耳朵,最大值还是能被提取出来。这就是平移不变性。
8.4 工程化神器:迁移学习(Transfer Learning)
这是本章最重要的实战技巧。
在工业界,很少有人会从零开始(From Scratch)训练一个CNN。因为训练一个像样的CNN需要海量数据(百万级)和昂贵的显卡集群(跑好几天)。
我们会使用预训练模型(Pre-trained Model)。 比如 Google 或 Facebook 已经用 1400 万张图片(ImageNet数据集)训练好了一个超级模型(如 ResNet)。这个模型已经“见过世面”了,它知道什么是线条,什么是纹理,什么是狗眼。
我们只需要把它的最后一层(分类层)砍掉,换成我们自己的“垃圾分类层”,然后稍微训练一下。这就叫微调(Fine-tuning)。
【比喻】 预训练模型就像一位博学的大学教授。 你的任务是让他去教幼儿园(识别垃圾)。 你不需要教他怎么识字、怎么看书(基础特征提取),你只需要告诉他:“教授,这个圆的是塑料瓶,那个方的是纸盒子”就行了。
8.5 实战案例:智能垃圾分类系统
我们将构建一个模型,能够区分四类垃圾:Cardboard(纸板)、Glass(玻璃)、Metal(金属)、Plastic(塑料)。
8.5.1 第一步:准备数据与目录结构
PyTorch 提供了一个非常符合直觉的数据加载方式:ImageFolder。你只需要按照文件夹分类存放图片即可。
请确保你的项目文件夹结构如下(可以从 Kaggle 下载 "Garbage Classification" 数据集的部分数据):
data/
train/
cardboard/
001.jpg
002.jpg
glass/
...
metal/
...
plastic/
...
val/ <-- 验证集结构同上
cardboard/
...
8.5.2 第二步:数据增强(Data Augmentation)
我们的图片可能不多。为了防止模型死记硬背(过拟合),我们要对图片进行“折磨”:随机旋转、随机裁剪、水平翻转。这样一张图能当十张用。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, models, transforms
# 定义数据预处理
# 这里的 mean 和 std 是 ImageNet 数据集的统计值,这是迁移学习的标准操作
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(224), # 随机裁剪并缩放到 224x224
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.ToTensor(), # 转为 Tensor
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224), # 验证集不需要随机,只取中间
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])
]),
}
# 加载数据
data_dir = './data' # 指向你的数据文件夹
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x])
for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=32,
shuffle=True, num_workers=4)
for x in ['train', 'val']}
class_names = image_datasets['train'].classes
print(f"检测到的分类: {class_names}") # 应该是 ['cardboard', 'glass', 'metal', 'plastic']
【小白避坑】为什么是 224x224? 绝大多数经典的 CNN 模型(如 ResNet, VGG)在设计时都默认输入尺寸是 224x224。为了配合预训练模型,我们最好遵守这个标准。
8.5.3 第三步:加载“巨人”模型 (ResNet18)
我们将使用 ResNet18,它是一个轻量级但非常强大的卷积网络。
# 1. 下载预训练模型
# pretrained=True 表示我们要下载人家训练好的参数,而不是一个空壳
model = models.resnet18(pretrained=True)
# 2. 修改全连接层 (Fully Connected Layer)
# ResNet18 原始的最后一层叫 fc,输出是 1000 (ImageNet有1000类)
# 我们需要把它改成 4 (我们的垃圾只有4类)
num_ftrs = model.fc.in_features # 获取倒数第二层的输入特征数
model.fc = nn.Linear(num_ftrs, 4) # 替换为新的层,输出为4
# 3. 搬运到 GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
print("模型加载完毕,已替换输出层。")
8.5.4 第四步:训练循环 (复用与升级)
这个循环逻辑与第7章基本一致,但我们加入验证集的计算,以便实时监控模型是否过拟合。
def train_model(model, criterion, optimizer, num_epochs=5):
for epoch in range(num_epochs):
print(f'Epoch {epoch+1}/{num_epochs}')
print('-' * 10)
# 每个 Epoch 都要跑一遍训练集和验证集
for phase in ['train', 'val']:
if phase == 'train':
model.train() # 训练模式 (启用 Dropout, BatchNorm)
else:
model.eval() # 评估模式 (冻结 Dropout, BatchNorm)
running_loss = 0.0
running_corrects = 0
for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
# 只有在训练阶段才追踪梯度
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
if phase == 'train':
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / len(image_datasets[phase])
epoch_acc = running_corrects.double() / len(image_datasets[phase])
print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')
return model
# --- 开始训练 ---
criterion = nn.CrossEntropyLoss()
# 这里的学习率设为 0.001,比从头训练要小,因为只需要微调
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
model_ft = train_model(model, criterion, optimizer, num_epochs=5)
预期结果: 你会发现,仅仅训练 1 个 Epoch,验证集的准确率可能就高达 80% 以上!到了第 5 个 Epoch,可能达到 90%~95%。这就是迁移学习的威力——它学得飞快。
8.5.5 第五步:预测单张图片
模型训练好了,怎么用它来识别一张新图片?
from PIL import Image
def predict_image(image_path):
# 1. 加载并处理图片
img = Image.open(image_path)
# 必须使用和验证集一样的预处理步骤
preprocess = data_transforms['val']
img_tensor = preprocess(img).unsqueeze(0) # 增加 Batch 维度 [1, 3, 224, 224]
# 2. 搬到 GPU
img_tensor = img_tensor.to(device)
# 3. 预测
model.eval()
with torch.no_grad():
outputs = model(img_tensor)
# Softmax 将输出转换为概率
probs = torch.nn.functional.softmax(outputs, dim=1)
_, preds = torch.max(outputs, 1)
# 4. 结果解析
class_name = class_names[preds.item()]
probability = probs[0][preds.item()].item()
print(f"这是一张: {class_name}, 置信度: {probability:.2%}")
# 显示图片
import matplotlib.pyplot as plt
plt.imshow(img)
plt.title(f"{class_name} ({probability:.1%})")
plt.axis('off')
plt.show()
# 找一张网上的图试试
# predict_image('my_coke_bottle.jpg')
8.6 章节小结
本章我们跨越了从“识别数字”到“看懂世界”的鸿沟。
-
CNN 原理:卷积提取特征,池化压缩信息,保留了空间结构。
-
ImageFolder:PyTorch 处理图像数据的标准姿势,只要整理好文件夹,代码自动搞定。
-
数据增强:通过随机裁剪和翻转,让有限的数据集发挥了无限的价值。
-
迁移学习:这是本章的核心工程思想。不要重复造轮子,使用
pretrained=True,站在巨人的肩膀上,你可以用极少的数据、极短的时间训练出工业级的模型。
在下一章,我们将把目光从“图像”转向“文本”。如何让机器读懂莎士比亚?如何判断一句话是赞美还是讽刺?我们将进入 NLP(自然语言处理) 的世界。
8.7 思考与扩展练习
-
冻结层(Freezing Layers): 在上面的代码中,我们训练了整个 ResNet 的所有参数。其实前面的层(提取线条、颜色)不需要重新训练。尝试在定义优化器之前,将前面层的参数锁定:
for param in model.parameters(): param.requires_grad = False # 然后解冻最后一层 model.fc.weight.requires_grad = True model.fc.bias.requires_grad = True比较一下,锁定参数后,训练速度(每个Epoch的时间)是不是变快了?
-
混淆矩阵分析: 使用第5章学过的混淆矩阵,分析一下你的垃圾分类模型。它最容易把哪两类垃圾搞混?(比如是不是经常把“玻璃”看成“塑料”?因为它们都是透明的)。
-
自定义挑战: 拿起你的手机,拍20张你家里的猫和狗的照片,分别建立
cats和dogs文件夹。利用本章的代码,训练一个你专属的“猫狗分类器”。
更多推荐



所有评论(0)