深度学习分类模型实战:图像分类

23次阅读
没有评论

共计 14345 个字符,预计需要花费 36 分钟才能阅读完成。

第一部分:引言与背景

1.1 什么是图像分类?

图像分类是计算机视觉中最基础、最核心的任务之一。其目标是给一张输入图像分配一个预定义的类别标签。例如,给定一张图片,模型需要判断它是“猫”还是“狗”,或者是“飞机”、“汽车”等。

这是一个典型的监督学习问题,因为我们需要一个已经标注好类别的大型数据集来“教导”模型。

1.2 深度学习在图像分类中的革命

在深度学习兴起之前,传统的图像分类方法依赖于手工设计的特征,如 SIFT、HOG 等。这些方法步骤繁琐,且特征表达能力有限,难以应对复杂的视觉变化。

2012 年,AlexNet 在 ImageNet 竞赛中取得突破性胜利,深度卷积神经网络一举成名。此后,VGG、GoogLeNet、ResNet 等更深的网络结构不断刷新纪录,使得模型能够直接从原始像素中学习到层次化的、强大的特征表示,彻底改变了这一领域。

1.3 本教程的目标与项目概述

目标:构建一个能够准确区分猫和狗图像的深度学习模型。

项目流程

  1. 准备一个包含“猫”和“狗”图片的数据集。
  2. 对数据进行预处理和增强。
  3. 构建一个卷积神经网络模型。
  4. 训练模型,使其学会区分猫和狗的特征。
  5. 评估模型的性能,并进行调优。
  6. 保存训练好的模型,并编写脚本进行预测。

1.4 环境配置与工具介绍

确保你已安装以下软件和库:

  • Python:3.7 或以上版本。
  • PyTorch:深度学习框架。请根据你的 CUDA 版本从 官网 选择对应命令安装。
  • Torchvision:通常与 PyTorch 一同安装,提供了数据集、模型架构和图像转换工具。
  • NumPy:科学计算库。
  • Matplotlib:绘图库。
  • OpenCVPIL/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 数据预处理与增强

原始数据通常不能直接送入模型。我们需要:

  1. 调整尺寸:神经网络通常需要固定尺寸的输入。
  2. 张量转换:将 PIL 图像或 NumPy 数组转换为 PyTorch 张量。
  3. 归一化 :将像素值从[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 使用 DatasetDataLoader 来高效地加载和处理数据。

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 服务部署

使用 FlaskFastAPI 可以快速创建一个 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 项目复盘

在这个项目中,我们完整地实践了一个深度学习分类项目:

  1. 数据准备:理解数据、划分数据集、应用预处理和数据增强。
  2. 模型构建:学习了如何搭建简单 CNN,并掌握了迁移学习的强大技术。
  3. 模型训练:定义了损失函数和优化器,编写了训练循环,并监控了训练过程。
  4. 模型评估:使用多种指标全面评估模型性能,并分析了过拟合 / 欠拟合问题。
  5. 模型部署:将训练好的模型保存下来,并提供了简单的预测接口和 Web 服务。

7.2 更广阔的应用场景

你刚刚构建的流程可以应用到无数其他分类问题中:

  • 医疗:皮肤癌分类、X 光片分析。
  • 农业:作物病害识别。
  • 工业:产品缺陷检测。
  • 安防:人脸识别、行为识别。

7.3 学习资源与下一步方向

  • 下一步学习
    • 物体检测:不仅要分类,还要定位出物体位置。学习 Faster R-CNN, YOLO, SSD。
    • 语义分割:对每个像素进行分类。学习 U -Net, DeepLab。
    • 自监督学习:如何利用无标签数据进行学习。
  • 推荐资源
    • 课程CS231n, Fast.ai
    • 书籍:《深度学习》(花书),《Python 深度学习》
    • 社区:PyTorch 官方论坛, Stack Overflow, GitHub

正文完
 0
一诺
版权声明:本站原创文章,由 一诺 于2022-10-18发表,共计14345字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码