第七章

图像分类实战:拍照识别书封

第六层 · 迁移学习/数据增强 用迁移学习实现“拍照搜书”

一、需求分析:拍照搜书怎么做

"王姐的需求:顾客拍一张书封,系统告诉他是哪本书、有没有货。"老周说,"技术上就是图像分类(Image Classification)——把封面图分到'具体哪本书'这个类别里。"

拍照搜书的技术方案:
  输入:书封照片
  输出:书的类别(或书名/ISBN)

  两种实现路径:
  ① 训练一个分类模型:类别 = 每一本书(几百类)
     - 每本书需要多张不同角度的样本照
     - 新书入库 → 要重新训练(麻烦)
  ② 特征提取 + 检索(更实用):
     - 用预训练模型把每本书封面变成"特征向量"
     - 拍照 → 变成特征向量 → 和数据库特征比对(余弦相似度)
     - 新书入库 → 只要算特征存进去(不用重训)
  云间书店选 ②(可扩展性好)

  本章先用最简单的 ① 理解全流程(分类)
  实际产品再升级到 ②(检索)

"先学会'分类',再理解'检索'——分类是基础,检索是进阶。本章先把分类跑通。"


二、数据集与数据加载:分类的第一步

"模型要喂数据。云间书店的场景:把封面分成'科幻/童书/文学/教材'四类。"老周说:

# 数据集结构:
# data/
#   train/
#     科幻/  100张封面
#     童书/  100张
#     文学/  100张
#     教材/  100张
#   val/     (各20张,验证用)

import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# 数据增强 + 预处理(第六章讲的防过拟合)
transform_train = transforms.Compose([
    transforms.Resize((224, 224)),          # 统一尺寸
    transforms.RandomHorizontalFlip(),      # 随机水平翻转
    transforms.RandomRotation(15),          # 随机旋转
    transforms.ColorJitter(0.2, 0.2, 0.2),  # 颜色扰动
    transforms.ToTensor(),                  # 转张量
    transforms.Normalize([0.485, 0.456, 0.406],
                         [0.229, 0.224, 0.225]),  # 标准化
])

# 验证集:不做增强(只做基本预处理)
transform_val = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                         [0.229, 0.224, 0.225]),
])

train_dataset = datasets.ImageFolder("data/train", transform_train)
val_dataset = datasets.ImageFolder("data/val", transform_val)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32)
print("类别:", train_dataset.classes)  # ['科幻','童书','文学','教材']
数据准备要点:
  每个类别一个文件夹(ImageFolder 自动打标签)
  数据增强:训练集"变花样"(防过拟合,第六章)
  标准化:用 ImageNet 的均值/方差(迁移学习需要)
  比例:训练/验证 ≈ 8:2

"数据是模型的天花板——'有多少数据,决定模型能多好'。数据增强是'无中生有'扩大数据量的标准手段。"


三、迁移学习:站在巨人的肩膀上

"从零训练 CNN 需要海量数据 + 几天 GPU。工程上几乎都迁移学习(Transfer Learning)。"老周说:

迁移学习:
  用别人在 ImageNet(1400万图)上训练好的模型
  → 微调(Fine-tune)到自己的任务

  为什么有效:
    ImageNet 模型已经学会了"通用特征"
    (边缘、纹理、形状、部件……所有图像都通用)
    我们只需要换掉"最后的分类层",重新学"我们的类别"

  流程:
    ① 加载预训练模型(ResNet18 等)
    ② 冻结前面的卷积层(保留通用特征)
    ③ 替换最后的全连接层(改成4类输出)
    ④ 用我们的数据微调(训练)

  PyTorch 一行加载预训练模型:
    model = torchvision.models.resnet18(weights="IMAGENET1K_V1")

  常见预训练模型:
    ResNet(残差网络,经典中的经典)
    EfficientNet(效率高)
    MobileNet(移动端轻量)
    ViT(视觉 Transformer,新趋势)

"迁移学习 = 站在巨人肩膀上——不从头训练,用大模型学好的'眼睛',只换'判断头'。数据少也能效果好,这是工业界默认做法。"


四、微调训练代码:完整流程

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import models

# 1. 加载预训练 ResNet18
model = models.resnet18(weights="IMAGENET1K_V1")

# 2. 冻结卷积层(特征提取部分不动)
for param in model.parameters():
    param.requires_grad = False

# 3. 替换分类头(改成 4 类)
num_classes = 4
model.fc = nn.Linear(model.fc.in_features, num_classes)
# 只训练新加的分类层

# 4. 定义损失和优化器
criterion = nn.CrossEntropyLoss()        # 分类损失
optimizer = optim.Adam(model.fc.parameters(), lr=0.001)

# 5. 训练循环
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(10):
    model.train()
    total_loss = 0
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = model(images)          # 前向
        loss = criterion(outputs, labels)
        loss.backward()                  # 反向
        optimizer.step()                 # 更新
        total_loss += loss.item()
    print(f"Epoch {epoch}: loss={total_loss/len(train_loader):.4f}")

# 6. 验证
model.eval()
correct = total = 0
with torch.no_grad():
    for images, labels in val_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, pred = torch.max(outputs, 1)
        correct += (pred == labels).sum().item()
        total += labels.size(0)
print(f"验证准确率: {correct/total:.2%}")   # ~95%+
训练流程回顾:
  冻结预训练层 → 换分类头 → 只训新层 → 评估
  为什么只训新层?数据少,动太多会过拟合
  效果:100 张/类 的小数据,也能到 90%+(迁移学习威力)

"这段代码是'图像分类微调'的标准模板——记下来,任何分类任务改改数据路径就能用。"


五、评估与部署:模型好不好用

"训练完,怎么评估、怎么用?"老周说:

评估指标:
  准确率(Accuracy):整体正确率(类别均衡时用)
  精确率/召回率/F1:类别不均衡时用
  混淆矩阵:看哪些类别容易搞混
    (科幻书和文学书如果经常混 → 数据问题或类别太像)

  评估要点:
    用"没训练过"的验证/测试集(第六章)
    看每类的准确率(不能只看整体)

模型保存与加载:
  torch.save(model.state_dict(), "book_classifier.pth")

  预测(推理):
  model.load_state_dict(torch.load("book_classifier.pth"))
  model.eval()
  # 新图 → 预处理 → 预测
  img = preprocess(new_photo)
  with torch.no_grad():
      out = model(img.unsqueeze(0))
      _, pred = torch.max(out, 1)
  print("预测类别:", classes[pred])

  拍照搜书的完整流程:
    拍照 → 预处理(第三章)→ 分类模型 → 得到类别/书名

"训练是 10% 的代码,90% 是数据、调试和评估——'准确率 95%'背后,是无数次'为什么分错'的排查。评估的意义:知道模型在哪不行,才能改进。"


六、从分类到检索:拍照搜书的"升级版"

"分类只能认'训练过的书'。新书入库怎么办?用特征检索。"老周说:

import torch
import torch.nn as nn
from torchvision import models

# 1. 加载预训练模型,去掉分类头(只保留特征提取)
base = models.resnet18(weights="IMAGENET1K_V1")
feature_extractor = nn.Sequential(*list(base.children())[:-1])

def get_feature(img_tensor):
    """把一张图变成 512 维特征向量"""
    with torch.no_grad():
        feat = feature_extractor(img_tensor.unsqueeze(0))
    return feat.flatten()          # (512,)

# 2. 数据库建索引:每本书封面 → 特征向量
book_vectors = {}
for book_id, img_path in book_covers.items():
    img = load_and_preprocess(img_path)
    book_vectors[book_id] = get_feature(img)

# 3. 拍照搜书:拍的书 → 特征向量 → 找最相似的
import numpy as np
def search_book(photo_tensor):
    query_vec = get_feature(photo_tensor)
    best_id, best_sim = None, -1
    for book_id, vec in book_vectors.items():
        sim = torch.cosine_similarity(query_vec, vec, dim=0).item()
        if sim > best_sim:
            best_sim, best_id = sim, book_id
    return best_id, best_sim
# 相似度 > 阈值 → 命中;否则提示"未收录"

# 4. 新书入库:算个特征存进去就行(不用重训!)
分类 vs 检索:
  分类:固定类别集合(新类别要重训)
  检索:任意书都能加(算特征+存向量,秒级入库)
  → 生产系统(书店 5 万本书)必须用检索方案

  进阶:用 FAISS/向量数据库(Milvus)做百万级检索

"真正的拍照搜书产品,用的是'特征检索'而不是'分类'——这就是为什么深度学习+向量检索是现在的标配。理解了分类,检索就是'把分类模型当特征提取器'。"


七、章末:老周的第七章总结

第六层:图像分类实战
├── 需求:拍照搜书 = 分类/检索
├── 数据:ImageFolder + 数据增强 + 标准化
├── 迁移学习:预训练模型(ResNet18)+ 换分类头
├── 训练:冻结特征层 → 只训分类层 → 微调
├── 评估:准确率/混淆矩阵(看每类表现)
├── 部署:保存模型 → 推理预测
└── 进阶:特征检索(新书秒级入库,用向量数据库)
第八层(预告):目标检测 —— 摄像头统计客流

"小陈,拍照搜书跑通了!顾客拍封面,系统秒回书名和库存。"老周说,"但王姐第二个需求来了:门口摄像头统计客流——这可不是'整张图分个类',而是要'在画面里找出每一个人并框出来'。下一章:目标检测。这是视觉里最实用、也最常被问到的技术。"

✌ 语言