一、需求分析:拍照搜书怎么做
"王姐的需求:顾客拍一张书封,系统告诉他是哪本书、有没有货。"老周说,"技术上就是图像分类(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)+ 换分类头
├── 训练:冻结特征层 → 只训分类层 → 微调
├── 评估:准确率/混淆矩阵(看每类表现)
├── 部署:保存模型 → 推理预测
└── 进阶:特征检索(新书秒级入库,用向量数据库)
第八层(预告):目标检测 —— 摄像头统计客流
"小陈,拍照搜书跑通了!顾客拍封面,系统秒回书名和库存。"老周说,"但王姐第二个需求来了:门口摄像头统计客流——这可不是'整张图分个类',而是要'在画面里找出每一个人并框出来'。下一章:目标检测。这是视觉里最实用、也最常被问到的技术。"