共计 14345 个字符,预计需要花费 36 分钟才能阅读完成。
第一部分:引言与背景
1.1 什么是图像分类?
图像分类是计算机视觉中最基础、最核心的任务之一。其目标是给一张输入图像分配一个预定义的类别标签。例如,给定一张图片,模型需要判断它是“猫”还是“狗”,或者是“飞机”、“汽车”等。
这是一个典型的监督学习问题,因为我们需要一个已经标注好类别的大型数据集来“教导”模型。
1.2 深度学习在图像分类中的革命
在深度学习兴起之前,传统的图像分类方法依赖于手工设计的特征,如 SIFT、HOG 等。这些方法步骤繁琐,且特征表达能力有限,难以应对复杂的视觉变化。
2012 年,AlexNet 在 ImageNet 竞赛中取得突破性胜利,深度卷积神经网络一举成名。此后,VGG、GoogLeNet、ResNet 等更深的网络结构不断刷新纪录,使得模型能够直接从原始像素中学习到层次化的、强大的特征表示,彻底改变了这一领域。
1.3 本教程的目标与项目概述
目标:构建一个能够准确区分猫和狗图像的深度学习模型。
项目流程:
- 准备一个包含“猫”和“狗”图片的数据集。
- 对数据进行预处理和增强。
- 构建一个卷积神经网络模型。
- 训练模型,使其学会区分猫和狗的特征。
- 评估模型的性能,并进行调优。
- 保存训练好的模型,并编写脚本进行预测。
1.4 环境配置与工具介绍
确保你已安装以下软件和库:
- Python:3.7 或以上版本。
- PyTorch:深度学习框架。请根据你的 CUDA 版本从 官网 选择对应命令安装。
- Torchvision:通常与 PyTorch 一同安装,提供了数据集、模型架构和图像转换工具。
- NumPy:科学计算库。
- Matplotlib:绘图库。
- OpenCV 或 PIL/Pillow:图像处理库。
- Jupyter Notebook:交互式编程环境。
- TensorBoard:训练过程可视化工具。
安装命令示例:
pip install torch torchvision torchaudio
pip install numpy matplotlib opencv-python pillow jupyter tensorboard
第二部分:数据——模型的基石
2.1 数据获取与理解
我们将使用 Kaggle 上的经典数据集 Dogs vs. Cats。
- 数据集结构:训练集包含 25,000 张图片,文件名格式为
cat.0.jpg,dog.0.jpg…。测试集包含 12,500 张图片,文件名格式为0.jpg。
首先,我们解压数据并查看其结构。
import os
import matplotlib.pyplot as plt
import random
from PIL import Image
# 设定数据路径
data_dir = './data/dogs-vs-cats/train'
# 获取所有猫和狗的图片文件名
cat_files = [os.path.join(data_dir, f) for f in os.listdir(data_dir) if 'cat' in f]
dog_files = [os.path.join(data_dir, f) for f in os.listdir(data_dir) if 'dog' in f]
print(f"Number of cat images: {len(cat_files)}")
print(f"Number of dog images: {len(dog_files)}")
# 随机查看几张图片
def plot_sample_images(cat_files, dog_files, num_samples=5):
fig, axes = plt.subplots(2, num_samples, figsize=(15, 6))
for i in range(num_samples):
# 显示猫图
cat_img = Image.open(random.choice(cat_files))
axes[0, i].imshow(cat_img)
axes[0, i].set_title('Cat')
axes[0, i].axis('off')
# 显示狗图
dog_img = Image.open(random.choice(dog_files))
axes[1, i].imshow(dog_img)
axes[1, i].set_title('Dog')
axes[1, i].axis('off')
plt.show()
plot_sample_images(cat_files, dog_files)
观察:图片的尺寸不一,颜色、背景、姿态各异。这正是真实世界数据的典型特点,也说明了数据预处理和增强的必要性。
2.2 数据预处理与增强
原始数据通常不能直接送入模型。我们需要:
- 调整尺寸:神经网络通常需要固定尺寸的输入。
- 张量转换:将 PIL 图像或 NumPy 数组转换为 PyTorch 张量。
- 归一化 :将像素值从[0, 255] 缩放到 [0, 1] 或[-1, 1],有助于模型稳定和快速收敛。
数据增强 是防止过拟合、提升模型泛化能力的关键技术。通过对训练数据进行随机变换,我们“创造”了更多样的数据。
import torch
from torchvision import transforms
# 定义训练和验证时的数据转换管道
# 训练数据转换:增强 + 归一化
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224), # 随机裁剪并缩放到 224x224
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.RandomRotation(10), # 随机旋转
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), # 颜色抖动
transforms.ToTensor(), # 转换为张量,并自动归一化到 [0,1]
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 使用 ImageNet 的均值和标准差
])
# 验证 / 测试数据转换:仅归一化,不增强
val_transforms = transforms.Compose([
transforms.Resize(256), # 将短边缩放到 256
transforms.CenterCrop(224), # 中心裁剪到 224x224
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
注意 :Normalize 的参数是 ImageNet 数据集的均值和标准差,因为我们后面会使用在 ImageNet 上预训练的模型。如果你从头训练,可以计算自己数据集的均值和标准差。
2.3 构建数据管道
PyTorch 使用 Dataset 和 DataLoader 来高效地加载和处理数据。
from torch.utils.data import Dataset, DataLoader, random_split
import pandas as pd
class CatsDogsDataset(Dataset):
def __init__(self, file_list, transform=None):
self.file_list = file_list
self.transform = transform
def __len__(self):
return len(self.file_list)
def __getitem__(self, idx):
img_path = self.file_list[idx]
image = Image.open(img_path)
# 从文件名中提取标签:'cat' -> 0, 'dog' -> 1
label = 0 if 'cat' in os.path.basename(img_path) else 1
if self.transform:
image = self.transform(image)
return image, label
# 创建完整数据集
all_files = cat_files + dog_files
full_dataset = CatsDogsDataset(all_files, transform=train_transforms) # 先用 train_transforms,拆分后再分别应用
# 划分训练集和验证集 (80% - 20%)
train_size = int(0.8 * len(full_dataset))
val_size = len(full_dataset) - train_size
train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])
# 重要:为验证集应用不同的转换
# 由于 random_split 不支持分别指定 transform,我们需要手动覆盖
val_dataset.dataset.transform = val_transforms
# 创建数据加载器
batch_size = 32
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=2)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=2)
# 检查一个批次的数据
dataiter = iter(train_loader)
images, labels = next(dataiter)
print(f"Batch image shape: {images.shape}") # [32, 3, 224, 224]
print(f"Batch label shape: {labels.shape}") # [32]
第三部分:模型设计——构建你的神经网络
3.1 神经网络基础回顾
一个简单的神经网络包括:
- 输入层:接收数据。
- 隐藏层:进行特征变换和学习。常见的有全连接层、卷积层。
- 输出层:输出预测结果。对于二分类,通常使用一个 Sigmoid 激活函数;对于多分类,使用 Softmax。
3.2 卷积神经网络的核心概念
CNN 是处理图像数据的首选架构,其核心思想是:
- 卷积层:使用滤波器在图像上滑动,提取局部特征。
- 池化层:降低特征图尺寸,减少计算量,增加感受野。
- 全连接层:在最后将学到的“分布式特征表示”映射到样本标记空间。
3.3 使用 PyTorch 构建一个简单的 CNN
让我们亲手搭建一个简单的 CNN 来理解其结构。
import torch.nn as nn
import torch.nn.functional as F
class SimpleCNN(nn.Module):
def __init__(self, num_classes=2):
super(SimpleCNN, self).__init__()
# 卷积块 1: 输入 3 通道,输出 16 通道
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
# 卷积块 2: 输入 16 通道,输出 32 通道
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
# 卷积块 3: 输入 32 通道,输出 64 通道
self.conv3 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
# 全连接层
# 经过 3 次池化,图像尺寸从 224 -> 112 -> 56 -> 28
self.fc1 = nn.Linear(64 * 28 * 28, 512) # 28x28 是最后一次池化后的尺寸
self.fc2 = nn.Linear(512, num_classes)
self.dropout = nn.Dropout(0.5) # Dropout 防止过拟合
def forward(self, x):
# 卷积 -> 激活 -> 池化
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = self.pool(F.relu(self.conv3(x)))
# 展平特征图
x = x.view(-1, 64 * 28 * 28)
# 全连接层
x = self.dropout(F.relu(self.fc1(x)))
x = self.fc2(x) # 注意:这里没有用 Softmax,因为损失函数会包含
return x
model = SimpleCNN(num_classes=2)
print(model)
这个模型虽然简单,但包含了 CNN 的基本要素。我们可以用它进行训练,但它的性能通常不如深度网络。
3.4 迁移学习:借助预训练模型
在计算机视觉中,迁移学习 是实践中的黄金法则。我们不需要从头训练一个模型,而是利用在大型数据集上预训练好的模型,只微调其最后几层,以适应我们的新任务。
为什么有效? 预训练模型已经学会了如何提取通用的图像特征,这些特征对于大多数视觉任务是共通的。
import torchvision.models as models
# 方法一:微调整个预训练模型
def create_model_finetune(num_classes=2):
# 加载在 ImageNet 上预训练的 ResNet18
model = models.resnet18(pretrained=True)
# 冻结所有层(可选,这里我们不冻结,进行全网络微调)# for param in model.parameters():
# param.requires_grad = False
# 替换最后的全连接层,使其输出符合我们的类别数
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, num_classes)
return model
# 方法二:将预训练模型作为特征提取器
def create_model_feature_extractor(num_classes=2):
model = models.resnet18(pretrained=True)
# 冻结所有卷积层的参数
for param in model.parameters():
param.requires_grad = False
# 只训练最后的全连接层
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, num_classes)
return model
# 我们选择方法一进行微调
model = create_model_finetune(num_classes=2)
print(model)
第四部分:模型训练——让模型从数据中学习
4.1 损失函数:衡量模型的好坏
对于二分类问题,我们使用 二元交叉熵损失。
criterion = nn.CrossEntropyLoss()
# 注意:PyTorch 的 nn.CrossEntropyLoss 已经包含了 Softmax,所以模型输出不需要再加 Softmax。# 对于二分类,它等同于二元交叉熵。
4.2 优化器:指导模型如何学习
优化器根据损失函数的梯度来更新模型的参数。最常用的是 Adam 优化器。
import torch.optim as optim
# 只训练最后一层的参数(如果用了特征提取器方法)或者所有参数(如果用了微调方法)optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4) # weight_decay 是 L2 正则化,防止过拟合
# 学习率调度器:在训练过程中动态降低学习率
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)
4.3 训练循环与验证循环
这是深度学习的核心引擎。
def train_model(model, train_loader, val_loader, criterion, optimizer, scheduler, num_epochs=25):
# 记录训练历史
train_loss_history = []
train_acc_history = []
val_loss_history = []
val_acc_history = []
best_acc = 0.0 # 记录最佳验证准确率
for epoch in range(num_epochs):
print(f'Epoch {epoch}/{num_epochs - 1}')
print('-' * 60)
# 每个 epoch 有两个阶段:训练和验证
for phase in ['train', 'val']:
if phase == 'train':
model.train() # 设置模型为训练模式
dataloader = train_loader
else:
model.eval() # 设置模型为评估模式
dataloader = val_loader
running_loss = 0.0
running_corrects = 0
# 迭代数据
for inputs, labels in dataloader:
# 将数据移动到 GPU(如果可用)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)
if phase == 'train':
scheduler.step() # 更新学习率
epoch_loss = running_loss / len(dataloader.dataset)
epoch_acc = running_corrects.double() / len(dataloader.dataset)
print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')
# 记录历史
if phase == 'train':
train_loss_history.append(epoch_loss)
train_acc_history.append(epoch_acc.cpu().numpy())
else:
val_loss_history.append(epoch_loss)
val_acc_history.append(epoch_acc.cpu().numpy())
# 深拷贝模型(如果这是最好的模型)if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
print()
print(f'Best val Acc: {best_acc:4f}')
# 加载最佳模型权重
model.load_state_dict(best_model_wts)
return model, {
'train_loss': train_loss_history,
'train_acc': train_acc_history,
'val_loss': val_loss_history,
'val_acc': val_acc_history
}
# 检查是否有可用的 GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
model = model.to(device)
# 开始训练!import copy
model, history = train_model(model, train_loader, val_loader, criterion, optimizer, scheduler, num_epochs=15)
4.4 监控训练过程:TensorBoard 的使用
TensorBoard 可以让我们实时、直观地观察训练过程。
from torch.utils.tensorboard import SummaryWriter
# 在训练循环中添加 TensorBoard 记录
writer = SummaryWriter('runs/cats_vs_dogs_experiment_1')
# ... 在 train_model 函数的循环内添加 ...
# 在每批次或每个 epoch 后记录
# writer.add_scalar('Training Loss', loss.item(), global_step)
# writer.add_scalar('Training Accuracy', ...)
# 在一个 epoch 结束后:writer.add_scalars('Loss/Epoch', {'Train': epoch_loss_train, 'Val': epoch_loss_val}, epoch)
writer.add_scalars('Accuracy/Epoch', {'Train': epoch_acc_train, 'Val': epoch_acc_val}, epoch)
# 训练结束后,在终端运行:tensorboard --logdir=runs
第五部分:模型评估与调优——追求卓越性能
5.1 评估指标详解
除了准确率,我们还需要更细致的指标。
- 混淆矩阵:展示模型在每个类别上的分类详情。
- 精确率:在所有预测为正的样本中,真正为正的比例。
- 召回率:在所有真实为正的样本中,被预测为正的比例。
- F1-Score:精确率和召回率的调和平均数。
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns
def evaluate_model(model, val_loader):
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for inputs, labels in val_loader:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
# 分类报告
print("Classification Report:")
print(classification_report(all_labels, all_preds, target_names=['Cat', 'Dog']))
# 混淆矩阵
cm = confusion_matrix(all_labels, all_preds)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Cat', 'Dog'], yticklabels=['Cat', 'Dog'])
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title('Confusion Matrix')
plt.show()
evaluate_model(model, val_loader)
5.2 过拟合与欠拟合
- 过拟合:模型在训练集上表现很好,但在验证集上表现很差。表现为训练损失持续下降,但验证损失先降后升。
- 解决方案:更多的数据增强、Dropout、权重衰减、早停、简化模型。
- 欠拟合:模型在训练集和验证集上表现都不好。表现为训练损失和验证损失都较高。
- 解决方案:训练更久、使用更复杂的模型、减少正则化。
5.3 超参数调优策略
超参数是训练开始前设置的参数,如学习率、批大小、优化器类型等。
- 网格搜索:尝试所有超参数组合。
- 随机搜索:在超参数空间中随机采样。
- 贝叶斯优化:更高效的搜索方法。
# 示例:使用 Optuna 进行超参数调优(高级内容)# import optuna
# def objective(trial):
# lr = trial.suggest_loguniform('lr', 1e-5, 1e-2)
# batch_size = trial.suggest_categorical('batch_size', [16, 32, 64])
# # ... 使用这些超参数创建模型、数据加载器并训练 ...
# return final_val_accuracy
# study = optuna.create_study(direction='maximize')
# study.optimize(objective, n_trials=50)
5.4 常见问题与解决方案
- 梯度消失 / 爆炸:使用 Batch Normalization、ResNet 等结构。
- 训练不稳定:降低学习率、使用梯度裁剪。
- 类别不平衡:在 DataLoader 中使用
WeightedRandomSampler或使用带权重的损失函数。
第六部分:模型部署与推理——让模型创造价值
6.1 模型保存与加载
# 保存模型的状态字典
torch.save(model.state_dict(), 'best_cats_dogs_model.pth')
# 加载模型时,需要先实例化模型结构,再加载权重
loaded_model = create_model_finetune(num_classes=2)
loaded_model.load_state_dict(torch.load('best_cats_dogs_model.pth'))
loaded_model = loaded_model.to(device)
loaded_model.eval()
6.2 编写推理脚本
编写一个函数,对单张图片或一批图片进行预测。
def predict_image(image_path, model, transform, class_names=['cat', 'dog']):
model.eval()
image = Image.open(image_path).convert('RGB')
image_tensor = transform(image).unsqueeze(0) # 增加一个批次维度
image_tensor = image_tensor.to(device)
with torch.no_grad():
outputs = model(image_tensor)
_, predicted = torch.max(outputs, 1)
probability = F.softmax(outputs, dim=1) # 转换为概率
confidence = probability[0][predicted.item()].item()
predicted_class = class_names[predicted.item()]
print(f'Predicted: {predicted_class} with confidence: {confidence:.4f}')
# 显示图片
plt.imshow(image)
plt.title(f'Prediction: {predicted_class} ({confidence:.2%})')
plt.axis('off')
plt.show()
return predicted_class, confidence
# 测试一张图片
predict_image('./data/dogs-vs-cats/test1/100.jpg', loaded_model, val_transforms)
6.3 简单 Web 服务部署
使用 Flask 或 FastAPI 可以快速创建一个 Web API。
app.py
from flask import Flask, request, jsonify
from PIL import Image
import io
import torch
import torchvision.transforms as transforms
app = Flask(__name__)
model = ... # 加载你的模型
model.eval()
def transform_image(image_bytes):
my_transforms = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
return my_transforms(image).unsqueeze(0)
def get_prediction(image_bytes):
tensor = transform_image(image_bytes=image_bytes)
outputs = model.forward(tensor)
_, y_hat = outputs.max(1)
return y_hat.item()
@app.route('/predict', methods=['POST'])
def predict():
if request.method == 'POST':
file = request.files['file']
img_bytes = file.read()
class_id = get_prediction(img_bytes)
class_name = 'cat' if class_id == 0 else 'dog'
return jsonify({'class_name': class_name})
if __name__ == '__main__':
app.run(debug=True)
运行 python app.py,你就可以通过发送 POST 请求到 /predict 来对上传的图片进行分类了。
第七部分:总结与展望
7.1 项目复盘
在这个项目中,我们完整地实践了一个深度学习分类项目:
- 数据准备:理解数据、划分数据集、应用预处理和数据增强。
- 模型构建:学习了如何搭建简单 CNN,并掌握了迁移学习的强大技术。
- 模型训练:定义了损失函数和优化器,编写了训练循环,并监控了训练过程。
- 模型评估:使用多种指标全面评估模型性能,并分析了过拟合 / 欠拟合问题。
- 模型部署:将训练好的模型保存下来,并提供了简单的预测接口和 Web 服务。
7.2 更广阔的应用场景
你刚刚构建的流程可以应用到无数其他分类问题中:
- 医疗:皮肤癌分类、X 光片分析。
- 农业:作物病害识别。
- 工业:产品缺陷检测。
- 安防:人脸识别、行为识别。
7.3 学习资源与下一步方向
- 下一步学习:
- 物体检测:不仅要分类,还要定位出物体位置。学习 Faster R-CNN, YOLO, SSD。
- 语义分割:对每个像素进行分类。学习 U -Net, DeepLab。
- 自监督学习:如何利用无标签数据进行学习。
- 推荐资源:

