| |
| """Generate YOLO notebook.""" |
|
|
| import nbformat as nbf |
|
|
| nb = nbf.v4.new_notebook() |
| nb.metadata = {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},"language_info": {"name": "python", "version": "3.12.0"}} |
|
|
| cells = [] |
| def md(s): cells.append(nbf.v4.new_markdown_cell(s)) |
| def code(s): cells.append(nbf.v4.new_code_cell(s)) |
|
|
| md("# YOLO: You Only Look Once\n\nSimplified object detection with grid-based bounding box regression on Pascal VOC.") |
|
|
| md("""## 背景 |
| |
| YOLO(Redmon et al. 2016)是首个单阶段目标检测器,将检测视为回归问题。 |
| 一张图通过 CNN 一次前向传播,直接输出边界框和类别概率。 |
| |
| 核心思想:将图像分成 $S \\times S$ 网格,每个网格预测 $B$ 个边界框和 $C$ 个类别的概率。 |
| |
| 与两阶段检测器(Faster R-CNN)的区别: |
| - YOLO:一次前向 → 端到端,速度快但精度略低 |
| - Faster R-CNN:候选区域 → 分类,精度高但速度慢 |
| |
| 数据集:**Pascal VOC** — 20 类物体,含边界框标注。 |
| """) |
|
|
| md("""## 数学原理 |
| |
| ### 输出表示 |
| |
| 每个网格单元预测 $B$ 个边界框,每个框 5 个值: |
| |
| $$(x, y, w, h, \\text{confidence})$$ |
| |
| - $x, y$: 框中心相对于网格单元的偏移(0~1) |
| - $w, h$: 框宽高相对于图像尺寸的比例 |
| - $\\text{confidence}$: $P(\\text{object}) \\times \\text{IoU}_{\\text{pred}}^{\\text{truth}}$ |
| |
| 再加上 $C$ 个类别概率 $P(\\text{class}_i \\mid \\text{object})$ |
| |
| 输出张量:$S \\times S \\times (B \\times 5 + C)$ |
| |
| ### 损失函数 |
| |
| $$\\mathcal{L} = \\lambda_{\\text{coord}} \\sum \\mathbb{1}_{ij}^{\\text{obj}} [(x - \\hat{x})^2 + (y - \\hat{y})^2 + (\\sqrt{w} - \\sqrt{\\hat{w}})^2 + (\\sqrt{h} - \\sqrt{\\hat{h}})^2] + \\sum \\mathbb{1}_{ij}^{\\text{obj}} (C - \\hat{C})^2 + \\lambda_{\\text{noobj}} \\sum \\mathbb{1}_{ij}^{\\text{noobj}} (C - \\hat{C})^2 + \\sum \\mathbb{1}_{i}^{\\text{obj}} \\sum_{c=1}^C (p_i(c) - \\hat{p}_i(c))^2$$ |
| |
| ### 非极大值抑制(NMS) |
| |
| 对同一类别的重叠框,保留得分最高的,移除与其 IoU 超过阈值的框。 |
| """) |
|
|
| code("""\ |
| import torch |
| import torch.optim as optim |
| from torch.utils.data import DataLoader |
| from torchvision import transforms |
| from datasets import load_dataset |
| |
| from cv.yolo.model import YOLO |
| from cv.yolo.loss import yolo_loss |
| from utils.config import load_config |
| from utils.seed import set_seed |
| from utils.device import get_device |
| |
| device = get_device() |
| print(f"Device: {device}") |
| """) |
|
|
| code("""\ |
| from cv.yolo.data import load_voc, VOC_CLASSES |
| |
| train_loader, test_loader = load_voc( |
| batch_size=32, image_size=224, S=7, B=2, C=20, num_workers=4, |
| ) |
| print(f"Classes ({len(VOC_CLASSES)}): {VOC_CLASSES}") |
| print(f"Train batches: {len(train_loader)}") |
| """) |
|
|
| code("""\ |
| model = YOLO(S=7, B=2, C=20).to(device) |
| print(f"Parameters: {model.num_params():,}") |
| """) |
|
|
| md("""## 训练 |
| |
| > ⏱ 预估耗时:**50 epoch × ~120s/epoch ≈ 1.5 小时**(M4 Max, batch_size=32) |
| > 如果太久,把下面 `NUM_EPOCHS` 改到 5 先看 loss 趋势。 |
| """) |
|
|
| code("""\ |
| NUM_EPOCHS = 50 |
| LR = 0.0001 |
| |
| optimizer = optim.Adam(model.parameters(), lr=LR) |
| loss_hist = [] |
| |
| for epoch in range(1, NUM_EPOCHS + 1): |
| model.train() |
| total_loss = 0.0 |
| for images, targets in train_loader: |
| images, targets = images.to(device), targets.to(device) |
| pred = model(images) |
| loss = yolo_loss(pred, targets, S=7, B=2, C=20, coord_scale=5, noobj_scale=0.5) |
| optimizer.zero_grad(); loss.backward(); optimizer.step() |
| total_loss += loss.item() |
| |
| avg = total_loss / len(train_loader) |
| loss_hist.append(avg) |
| print(f"Epoch [{epoch:2d}/{NUM_EPOCHS}] Loss: {avg:.4f}") |
| """) |
|
|
| md("""## Loss 曲线""") |
|
|
| code("""\ |
| import matplotlib.pyplot as plt |
| plt.plot(loss_hist) |
| plt.xlabel("Epoch"); plt.ylabel("Loss"); plt.title("YOLO Training Loss"); plt.grid(True) |
| plt.show() |
| """) |
|
|
| md("""\ |
| ## 思考题 |
| |
| 1. YOLO 的 $S \\times S$ 网格中,一个网格只能预测一个物体(每个类)。这对检测小物体有什么影响? |
| 2. 为什么边界框的 $w, h$ 用平方根而不是直接用?这有什么物理意义? |
| 3. NMS 中的 IoU 阈值高低各有什么影响? |
| 4. YOLO 和两阶段检测器(Faster R-CNN)的核心区别是什么? |
| """) |
|
|
| nb.cells = cells |
| with open("cv/yolo/yolo.ipynb", "w") as f: |
| nbf.write(nb, f) |
| print("Generated cv/yolo/yolo.ipynb") |
|
|