dl-from-scratch / scripts /gen_resnet34_notebook.py
yusiwen's picture
refactor: restructure into domain-grounded directories (ml/ cv/ gen/ graph/ rl/ nlp/)
73526b1 unverified
Raw
History Blame Contribute Delete
5.62 kB
#!/usr/bin/env python3
"""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}")