fastai 提倡先训出能用的模型。这篇不下载 ImageNette,而是用 PIL 合成圆形/方形小数据集,再用
vision_learner(resnet18)+fine_tune训练,并export出 pkl。 合成数据让你在无 GPU、无外部数据集授权问题的机器上完整走通官方训练 API。
先看清成品
- 真实训练:80 张训练 / 20 张验证,fine_tune 2 轮后 valid accuracy=1.0,导出 exports/shapes.pkl。
- make_shapes 会清空旧类别目录再写入,避免上次 n 更大时残留文件。
- 2 项 pytest 通过(含 1 轮 smoke 训练)。
合规提醒:只用你有权使用的图像数据;不要爬取或使用未授权的人脸/版权图片。
load_learner使用 pickle,只加载自己导出或可信来源的模型文件。本教程合成几何图形,不涉及真人数据。
运行说明:在 box 上用 CPU 版 PyTorch 2.14.1 + fastai 2.8.12 真实训练与推理(2026-10-23,UTC+8)。首次训练从 download.pytorch.org 下载官方 resnet18 权重约 45MB。未使用 GPU;准确率在合成圆形/方形任务上达到 1.0,不代表复杂真实数据集表现。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
make_shapes.py |
合成数据集 |
train_shapes.py |
fastai 训练 + export |
test_95a.py |
2 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.10+ |
| 依赖 | pip install fastai torch torchvision pillow pytest(CPU 即可) |
| 磁盘 | 首次约数十 MB 权重 + 导出 pkl |
关键事实
| 事实 | 来源 |
|---|---|
ImageDataLoaders.from_folder 期望 train/、valid/ 下按类别分子目录 |
官方 Data / Vision |
vision_learner + fine_tune 是迁移学习常用写法;pretrained=True 使用 torchvision 权重 |
官方 Vision / Learner |
learn.export 默认相对 learn.path(数据根目录);导出路径建议用绝对路径 |
官方 Learner.export |
| 课程与库文档见 course.fast.ai 与 docs.fast.ai | 官方 |
文件结构
95-fast-ai-A-src/
├── make_shapes.py
├── train_shapes.py
└── test_95a.py
第 1 步:合成数据
#!/usr/bin/env python3
"""Create a tiny synthetic shapes dataset (circle vs square) as PNG files."""
from __future__ import annotations
import argparse
import random
from pathlib import Path
from PIL import Image, ImageDraw
def draw_circle(size: int, rng: random.Random) -> Image.Image:
img = Image.new("RGB", (size, size), (rng.randint(200, 255),) * 3)
d = ImageDraw.Draw(img)
m = rng.randint(4, size // 5)
color = (rng.randint(0, 180), rng.randint(0, 180), rng.randint(0, 180))
d.ellipse([m, m, size - m, size - m], fill=color)
return img
def draw_square(size: int, rng: random.Random) -> Image.Image:
img = Image.new("RGB", (size, size), (rng.randint(200, 255),) * 3)
d = ImageDraw.Draw(img)
m = rng.randint(4, size // 5)
color = (rng.randint(0, 180), rng.randint(0, 180), rng.randint(0, 180))
d.rectangle([m, m, size - m, size - m], fill=color)
return img
def build(root: Path, n_per_class: int = 40, size: int = 64, seed: int = 0) -> Path:
rng = random.Random(seed)
for split, count in (("train", n_per_class), ("valid", max(2, n_per_class // 4))):
for label, drawer in (("circle", draw_circle), ("square", draw_square)):
d = root / split / label
if d.exists():
for oldf in d.glob("*"):
oldf.unlink()
d.mkdir(parents=True, exist_ok=True)
for i in range(count):
drawer(size, rng).save(d / f"{label}_{i:03d}.png")
return root
def main(argv=None) -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--out", type=Path, default=Path("shapes"))
p.add_argument("--n", type=int, default=40)
args = p.parse_args(argv)
build(args.out, n_per_class=args.n)
print(f"wrote {args.out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
第 2 步:训练
#!/usr/bin/env python3
"""Train a tiny image classifier with fastai on synthetic shapes.
Uses vision_learner(resnet18) + fine_tune. The first run downloads official
ImageNet weights from download.pytorch.org (~45MB); no third-party mirrors.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from make_shapes import build
def train(data: Path, epochs: int, export_path: Path) -> dict:
from fastai.vision.all import ImageDataLoaders, Resize, accuracy, resnet18, vision_learner
dls = ImageDataLoaders.from_folder(
data,
train="train",
valid="valid",
item_tfms=Resize(64),
bs=16,
)
learn = vision_learner(dls, resnet18, metrics=accuracy, pretrained=True)
# 1 frozen epoch + `epochs` unfrozen — classic fastai fine_tune
learn.fine_tune(epochs, base_lr=1e-3)
met = learn.recorder.values[-1] if learn.recorder.values else []
acc = float(met[2]) if len(met) >= 3 else None
# fastai resolves relative export paths under learn.path (= data root)
export_path = export_path.resolve()
export_path.parent.mkdir(parents=True, exist_ok=True)
learn.export(export_path)
return {
"accuracy": acc,
"export": str(export_path),
"n_train": len(dls.train_ds),
"n_valid": len(dls.valid_ds),
}
def main(argv=None) -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--data", type=Path, default=Path("shapes"))
p.add_argument("--epochs", type=int, default=2)
p.add_argument("--export", type=Path, default=Path("exports/shapes.pkl"))
p.add_argument("--rebuild", action="store_true")
p.add_argument("--n", type=int, default=40, help="images per class in train/")
args = p.parse_args(argv)
if args.rebuild or not (args.data / "train").exists():
build(args.data, n_per_class=args.n)
info = train(args.data, args.epochs, args.export)
print(json.dumps(info, ensure_ascii=False, indent=2))
if info["accuracy"] is not None and info["accuracy"] < 0.9:
raise SystemExit(f"accuracy too low: {info['accuracy']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
第 3 步:测试
from pathlib import Path
from make_shapes import build
def test_build(tmp_path: Path):
root = build(tmp_path / "shapes", n_per_class=8, size=32)
assert (root / "train/circle").is_dir()
assert len(list((root / "train/circle").glob("*.png"))) == 8
assert len(list((root / "valid/square").glob("*.png"))) == 2
def test_train_smoke(tmp_path: Path):
import importlib.util
if importlib.util.find_spec("fastai") is None or importlib.util.find_spec("torch") is None:
import pytest
pytest.skip("fastai/torch not installed")
from train_shapes import train
data = build(tmp_path / "shapes", n_per_class=20, size=64)
info = train(data, epochs=1, export_path=tmp_path / "m.pkl")
assert info["n_train"] > 0
assert Path(info["export"]).is_file()
assert info["accuracy"] is None or info["accuracy"] >= 0.85
第 4 步:运行
$ python make_shapes.py --out shapes --n 40
wrote shapes
$ python train_shapes.py --data shapes --epochs 2 --export exports/shapes.pkl
# 首次会从 download.pytorch.org 下载 resnet18 权重约 45MB(官方源)
# 2026-10-23 12:38 UTC+8(box CPU)
epoch train_loss valid_loss accuracy time
0 1.051598 1.252751 0.550000 00:00 # fine_tune 冻结阶段
0 0.613625 0.308894 0.850000 00:00 # 解冻 epoch 1
1 0.396066 0.102641 1.000000 00:01 # 解冻 epoch 2
{
"accuracy": 1.0,
"export": "exports/shapes.pkl",
"n_train": 80,
"n_valid": 20
}
$ python -m pytest -q
2 passed
怎么检查结果?
- 结束后 accuracy ≥ 0.9,且 exports/shapes.pkl 存在。
- 换自己的数据时保持 train/valid/类别 目录约定。
- 第一次运行需能访问 download.pytorch.org。
调试示例
相对导出路径掉进数据集目录。 第一版 learn.export(Path("exports/shapes.pkl")) 实际写到 shapes/exports/...,因为 fastai 把相对路径解析到 learn.path。改为 export_path.resolve() 后文件落在项目 exports/。
从零训练 resnet18 不收敛。 合成任务虽简单,但 scratch 训练在短 epoch 内准确率停在 ~0.5。改用官方推荐的 pretrained + fine_tune 后两轮即到 1.0。教程默认走这条路径,并写明权重来源是 pytorch.org。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 下载权重失败 | 网络限制 | 配置可达 download.pytorch.org 的环境 |
| 准确率很差 | 数据太少或标签混 | 检查目录;增加 n 或 epochs |
| pkl 很大 | 含模型权重 | 属正常;勿提交到 Git |
模板规则
- 只用授权数据。
- export 用绝对路径。
- 预训练权重来自官方源。
项目资源
| 用途 | 地址 |
|---|---|
| fastai Vision(官方) | https://docs.fast.ai/tutorial.vision.html |
| Learner(官方) | https://docs.fast.ai/learner.html |
| 课程 | https://course.fast.ai/ |
| GitHub | https://github.com/fastai/fastai |
先在合成数据上把 API 跑顺,再换真实图片。