| |
| """Generate ResNet34 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("""\ |
| # ResNet34: Advanced ResNet |
| |
| Extended ResNet with [3,4,6,3] blocks, full CelebA (40 attributes), data augmentation. |
| """) |
|
|
| md("""\ |
| ## 背景 |
| |
| ResNet34 是 ResNet18 的升级版:从 [2,2,2,2] 扩展到 [3,4,6,3] 个残差块。 |
| 除了更深的网络,ResNet34 在此项目中还展示了更完整的训练流程: |
| |
| - **SGD + Momentum** 替代 Adam(更通用的优化器) |
| - **CosineAnnealingLR** 学习率调度 |
| - **数据增强**:随机翻转、颜色抖动、旋转 |
| - **Loss 加权**:`pos_weight` 处理属性不平衡 |
| - **Early stopping**:按验证 loss 保存最优模型 |
| """) |
|
|
| md("""\ |
| ## 架构对比 |
| |
| ``` |
| ResNet18: [BasicBlock×2] → [BasicBlock×2] → [BasicBlock×2] → [BasicBlock×2] |
| ResNet34: [BasicBlock×3] → [BasicBlock×4] → [BasicBlock×6] → [BasicBlock×3] |
| ``` |
| |
| ResNet34 的每个 `BasicBlock` 结构与 ResNet18 完全相同(2× Conv3×3 + BN + ReLU)。 |
| 差异只在于 block 数量。 |
| |
| > 本项目中的 ResNet34 直接从 `cv.resnet18.model` 复用 `ResNet` 类和 `BasicBlock`, |
| > 仅通过 `num_blocks` 参数实现架构升级。 |
| """) |
|
|
| code("""\ |
| import torch |
| import torch.nn as nn |
| import torch.optim as optim |
| from torch.optim.lr_scheduler import CosineAnnealingLR |
| from torch.utils.data import DataLoader |
| from torchvision import transforms |
| |
| from cv.resnet34.data import CelebADataset, CELEBA_ATTR_ORDER, train_transform |
| from cv.resnet34.model import resnet34 |
| from utils.device import get_device |
| |
| device = get_device() |
| print(f"Device: {device}") |
| """) |
|
|
| code("""\ |
| # 使用 20K 子集控制 notebook 训练时间 |
| # 全量 162K → 改为 20000 |
| SUBSET_SIZE = 20000 |
| |
| train_dataset = CelebADataset(split="train", transform=train_transform()) |
| # 截取前 SUBSET_SIZE 个样本 |
| train_dataset.samples = train_dataset.samples[:SUBSET_SIZE] |
| |
| val_dataset = CelebADataset(split="val", transform=train_transform()) |
| |
| train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=4, pin_memory=True) |
| val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=4, pin_memory=True) |
| |
| print(f"Train: {len(train_dataset):,} Val: {len(val_dataset):,}") |
| print(f"Attributes: {len(CELEBA_ATTR_ORDER)}") |
| """) |
|
|
| code("""\ |
| # 计算 pos_weight(从训练数据中统计正负样本比例) |
| print("Computing pos_weight...") |
| pos_counts = torch.zeros(40) |
| for _, labels in train_loader: |
| pos_counts += labels.sum(dim=0) |
| neg_counts = len(train_dataset) - pos_counts |
| pos_weight = (neg_counts / pos_counts).clamp(min=1.0) |
| print(f"pos_weight range: [{pos_weight.min():.2f}, {pos_weight.max():.2f}]") |
| """) |
|
|
| code("""\ |
| model = resnet34(num_classes=40).to(device) |
| print(f"Parameters: {sum(p.numel() for p in model.parameters()):,}") |
| """) |
|
|
| md("""\ |
| ## 训练 |
| |
| > ⏱ 预估耗时:**20 epoch × ~70s/epoch ≈ 23 分钟**(20K 子集, M4 Max, batch_size=64) |
| """) |
|
|
| code("""\ |
| NUM_EPOCHS = 20 |
| LR = 0.1 |
| MOMENTUM = 0.9 |
| WEIGHT_DECAY = 1e-4 |
| |
| criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) |
| optimizer = optim.SGD(model.parameters(), lr=LR, momentum=MOMENTUM, weight_decay=WEIGHT_DECAY) |
| scheduler = CosineAnnealingLR(optimizer, T_max=NUM_EPOCHS) |
| |
| train_loss_hist, val_loss_hist = [], [] |
| |
| for epoch in range(1, NUM_EPOCHS + 1): |
| model.train() |
| train_loss = 0.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() |
| train_loss += loss.item() |
| scheduler.step() |
| |
| model.eval() |
| val_loss = 0.0 |
| correct = total = 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) |
| val_loss += loss.item() |
| preds = (torch.sigmoid(outputs) > 0.5).float() |
| correct += (preds == labels).sum().item() |
| total += labels.numel() |
| |
| avg_train = train_loss / len(train_loader) |
| avg_val = val_loss / len(val_loader) |
| acc = correct / total * 100 |
| train_loss_hist.append(avg_train) |
| val_loss_hist.append(avg_val) |
| print(f"Epoch [{epoch:2d}/{NUM_EPOCHS}] Train: {avg_train:.4f} Val: {avg_val:.4f} Acc: {acc:.2f}%") |
| """) |
|
|
| md("""## Loss 曲线""") |
|
|
| code("""\ |
| import matplotlib.pyplot as plt |
| from utils.device import get_device |
| |
| plt.figure(figsize=(8, 4)) |
| plt.plot(train_loss_hist, label='train', marker='o') |
| plt.plot(val_loss_hist, label='val', marker='o') |
| plt.xlabel("Epoch"); plt.ylabel("Loss"); plt.legend(); plt.grid(True) |
| plt.title("ResNet34 Training on CelebA (20K subset)"); plt.show() |
| """) |
|
|
| md("""\ |
| ## 思考题 |
| |
| 1. SGD + Momentum 和 Adam 各自的优缺点是什么?什么场景下 SGD 更好? |
| 2. `pos_weight` 的作用是什么?不使用时对哪些属性影响最大? |
| 3. 数据增强(翻转、颜色抖动、旋转)为什么会提升泛化能力? |
| 4. 把 `SUBSET_SIZE` 改到 162770(全量),训练 30 epoch 观察效果。 |
| 5. 对比 ResNet18 和 ResNet34 在此任务上的表现差异。 |
| """) |
|
|
| nb.cells = cells |
| out = "cv/resnet34/resnet34.ipynb" |
| with open(out, "w") as f: |
| nbf.write(nb, f) |
| print(f"Generated {out}") |
|
|