3.1 模型如何对照片分类
有两个装满照片的文件夹:一个叫 cat ,一个叫 dog 。
我们希望模型学会一件事:看到一张新照片时,能判断它属于哪一个文件夹。
模型面对猫和狗,并不会"看见毛、耳朵或眼睛",它只能处理数字。
因此,希望模型进行多次练习后,可以把"猜测"变成"更接近正确的判断"。
3.2 准备好图片
你首先做的是最朴素的一件事:把图片按类别放好。
dataset/
train/
cat/
dog/
val/
cat/
dog/
在这个结构里,"答案"已经写在了图片所在的文件夹里:
- 在
train/cat/的图片,正确答案就是 cat - 在
train/dog/的图片,正确答案就是 dog
程序会把 cat、dog 变成数字标签,通常是:
cat = 0dog = 1
这一步非常重要,因为它意味着:
训练不是凭空进行的,模型每次练习都有"标准答案"。
3.3 让图片变成模型能读的数字
模型看不懂图片文件,模型需要的是"数字形式的图片"。
因此我们做了两条最简单的统一规则:
- 每张图片统一尺寸(否则无法成批处理)
- 把图片变成张量(Tensor),让模型可以做数学计算
python
from torchvision import transforms
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])
模型并不是"看图片文件",而是"看图片转换后的数字张量"。
3.4 使用DataLoader 准备训练数据
我们把训练数据切成一份份,每份包含 8 张图片,这就是 batch。
- batch:一次练习的题量(例如 8 张)
- epoch:把训练集完整练完一遍
python
from torchvision import datasets
from torch.utils.data import DataLoader
train_dataset = datasets.ImageFolder("dataset/train", transform=transform)
val_dataset = datasets.ImageFolder("dataset/val", transform=transform)
train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=8, shuffle=False, num_workers=0)
print("classes:", train_dataset.classes) # ['cat', 'dog']
可以把训练过程想象成:
模型不断拿到一份份练习卷,一份份地做题和订正。
~~## 3.5 模型不是在对图片分类,而是在"打分"
模型第一次对图片分类时:
- 模型可能把所有图片都预测成 dog
- 或者预测得完全没有规律
这并不是程序错误,而是因为:
模型输出的不是"答案",而是"评分"。
它会为每张图片给出两个数: - 更像 cat 的评分
- 更像 dog 的评分
哪个评分大,就选哪个类别。
这就是所谓的 logits:一组用于做出选择的内部评分。
3.6 loss 可以评估模型"错得有多严重"
只告诉模型"你错了"是不够的。
你需要一个更精确的反馈:这次到底错得有多严重。
loss 做的就是这件事:
- loss 大:错得离谱
- loss 小 :更接近正确
训练的目标不是"立刻全对",而是让 loss 随着练习逐步下降。
3.7 模型进行修正学习
在整套训练流程里,真正让模型发生变化的,不是它答题,也不是你批改,而是它订正的过程。
订正集中体现在三行关键代码:
python
optimizer.zero_grad()
loss.backward()
optimizer.step()
你可以把它理解为:
- 清空上一轮的订正痕迹
- 根据这次错题,算出应该怎么改
- 把模型参数调整一点点
这就是模型的"学习"。
3.8 遍历 DataLoader 进行完整训练
如果你只拿同一份练习卷反复训练,模型很快会把那几张图背下来,loss 会迅速接近 0。
但这并不说明它会分辨猫狗,只说明它记住了少数图片。
因此真正的训练必须做到:
在一个 epoch 内,把训练集里的所有 batch 都练一遍。
3.9 全流程代码(猫狗分类:训练 + 验证)
python
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
def main():
device = "cuda" if torch.cuda.is_available() else "cpu"
print("device:", device)
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])
train_dataset = datasets.ImageFolder("dataset/train", transform=transform)
val_dataset = datasets.ImageFolder("dataset/val", transform=transform)
print("classes:", train_dataset.classes) # ['cat', 'dog']
train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=8, shuffle=False, num_workers=0)
model = models.resnet18(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, 2)
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
num_epochs = 5
for epoch in range(1, num_epochs + 1):
# 训练:做完所有"练习卷"
model.train()
train_loss_sum, train_count = 0.0, 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
bs = labels.size(0)
train_loss_sum += loss.item() * bs
train_count += bs
train_avg_loss = train_loss_sum / max(train_count, 1)
# 验证:随堂测验(只观察,不订正)
model.eval()
val_loss_sum, val_correct, val_count = 0.0, 0, 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
_, predicted = torch.max(outputs, 1)
val_correct += (predicted == labels).sum().item()
bs = labels.size(0)
val_loss_sum += loss.item() * bs
val_count += bs
val_avg_loss = val_loss_sum / max(val_count, 1)
val_acc = val_correct / max(val_count, 1)
print(f"Epoch {epoch}/{num_epochs} | "
f"train_loss={train_avg_loss:.4f} | "
f"val_loss={val_avg_loss:.4f} | "
f"val_acc={val_acc:.2%}")
if __name__ == "__main__":
main()
3.10 总结
这一章的猫狗图片分类的主要流程:
- 你先把猫狗图片按文件夹分好,答案就写在文件夹里
- 你把图片转成统一大小的数字,模型才能计算
- 你用 DataLoader 把题库切成一份份练习卷(batch)
- 模型每次先给评分,再选答案
- loss 像老师的批改分数,告诉它错得多严重
backward + step是订正动作,让模型一点点改变- 一个 epoch 必须把训练集练完一遍,才能算"上完一节课"
- 验证集是随堂测验,用来观察它是否真的学会,而不是背题