Download scripts/gen_resnet18_notebook.py from yusiwen/dl-from-scratch: direct link, hf CLI and curl.
- Browser
- Download file 5.86 kB
-
https://huggingface.co/yusiwen/dl-from-scratch/resolve/main/scripts/gen_resnet18_notebook.py
- Command line
-
hf download hf://yusiwen/dl-from-scratch/scripts/gen_resnet18_notebook.py
-
curl -L -o gen_resnet18_notebook.py https://huggingface.co/yusiwen/dl-from-scratch/resolve/main/scripts/gen_resnet18_notebook.py
5.86 kB
| #!/usr/bin/env python3 | |
| """Generate ResNet18 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("""\ | |
| # ResNet18: Residual Networks | |
| Deep residual learning with skip connections for image classification. | |
| """) | |
| md("""\ | |
| ## 背景 | |
| ResNet(2015)解决了深度网络的退化问题:层数增加后训练误差反而上升。 | |
| 核心创新是**残差连接(skip connection / shortcut)**: | |
| $$y = \\mathcal{F}(x) + x$$ | |
| 让梯度可以直接通过 shortcut 传播,使得训练上百层的网络成为可能。 | |
| ResNet18 是其中最轻量的版本,用 4 个 stage 共 18 个卷积层。 | |
| """) | |
| md("""\ | |
| ## 数学原理 | |
| ### 残差块 | |
| $$\\mathcal{F}(x) = \\text{Conv3×3} \\to \\text{BN} \\to \\text{ReLU} \\to \\text{Conv3×3} \\to \\text{BN}$$ | |
| $$\\text{Output} = \\text{ReLU}(\\mathcal{F}(x) + x)$$ | |
| 当维度不匹配时(跨 stage 下采样),shortcut 需额外 Conv1×1 来对齐: | |
| $$\\text{Output} = \\text{ReLU}(\\mathcal{F}(x) + W_s \\cdot x)$$ | |
| ### 架构 | |
| ``` | |
| 7×7 Conv(stride=2) → BN → ReLU → 3×3 MaxPool(stride=2) | |
| → [BasicBlock×2, 64] stage1 | |
| → [BasicBlock×2, 128] stage2 | |
| → [BasicBlock×2, 256] stage3 | |
| → [BasicBlock×2, 512] stage4 | |
| → AvgPool → FC(512 → 15) | |
| ``` | |
| """) | |
| code("""\ | |
| import torch | |
| import torch.nn as nn | |
| import torch.optim as optim | |
| from torch.utils.data import DataLoader | |
| from torchvision import transforms | |
| from cv.resnet18.data import CelebADataset, ATTRIBUTES | |
| from cv.resnet18.model import resnet18 | |
| from utils.device import get_device | |
| device = get_device() | |
| print(f"Device: {device}") | |
| """) | |
| code("""\ | |
| # 数据加载 | |
| transform = transforms.Compose([ | |
| transforms.Resize(256), | |
| transforms.CenterCrop(224), | |
| transforms.ToTensor(), | |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), | |
| ]) | |
| full_dataset = CelebADataset(ATTRIBUTES, num_samples=1000, transform=transform) | |
| train_dataset, val_dataset = torch.utils.data.random_split( | |
| full_dataset, [800, 200], generator=torch.Generator().manual_seed(42) | |
| ) | |
| train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=0, pin_memory=True) | |
| val_loader = DataLoader(val_dataset, batch_size=128, shuffle=False, num_workers=0, pin_memory=True) | |
| print(f"Train: {len(train_dataset)} Val: {len(val_dataset)}") | |
| print(f"Attributes ({len(ATTRIBUTES)}): {', '.join(ATTRIBUTES)}") | |
| """) | |
| code("""\ | |
| model = resnet18(num_classes=len(ATTRIBUTES)).to(device) | |
| print(f"Parameters: {sum(p.numel() for p in model.parameters()):,}") | |
| """) | |
| md("""\ | |
| ## 训练 | |
| > ⏱ 预估耗时:**30 epoch × ~20s/epoch ≈ 10 分钟**(M4 Max, batch_size=128) | |
| """) | |
| code("""\ | |
| NUM_EPOCHS = 30 | |
| LR = 1e-3 | |
| criterion = nn.BCEWithLogitsLoss() | |
| optimizer = optim.Adam(model.parameters(), lr=LR) | |
| train_loss_hist, val_loss_hist, val_acc_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() | |
| 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) | |
| val_acc_hist.append(acc) | |
| 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 | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) | |
| ax1.plot(train_loss_hist, label='train', marker='o') | |
| ax1.plot(val_loss_hist, label='val', marker='o') | |
| ax1.set_xlabel("Epoch"); ax1.set_ylabel("Loss"); ax1.legend(); ax1.grid(True) | |
| ax2.plot(val_acc_hist, marker='o', color='green') | |
| ax2.set_xlabel("Epoch"); ax2.set_ylabel("Val Acc (%)"); ax2.grid(True) | |
| plt.tight_layout(); plt.show() | |
| """) | |
| md("""## 每属性准确率""") | |
| code("""\ | |
| model.eval() | |
| all_preds, all_labels = [], [] | |
| with torch.no_grad(): | |
| for images, labels in val_loader: | |
| images = images.to(device) | |
| outputs = torch.sigmoid(model(images)).cpu() | |
| all_preds.append((outputs > 0.5).float()) | |
| all_labels.append(labels) | |
| all_preds = torch.cat(all_preds) | |
| all_labels = torch.cat(all_labels) | |
| print(f"{'Attribute':<20} {'Accuracy':>8}") | |
| print("-" * 30) | |
| for i, attr in enumerate(ATTRIBUTES): | |
| acc = (all_preds[:, i] == all_labels[:, i]).sum().item() / all_labels.size(0) * 100 | |
| print(f"{attr:<20} {acc:>7.2f}%") | |
| """) | |
| md("""\ | |
| ## 思考题 | |
| 1. 残差连接为什么能缓解梯度消失?画出有/无 shortcut 的梯度路径。 | |
| 2. 1×1 卷积在 ResNet 中有什么作用?(提示:改变通道数) | |
| 3. ResNet18 换成 ResNet34([3,4,6,3] blocks),参数量增加多少? | |
| 4. 尝试去掉 shortcut 训练(把 `+ x` 注释掉),观察 loss 有何不同。 | |
| """) | |
| nb.cells = cells | |
| out = "cv/resnet18/resnet18.ipynb" | |
| with open(out, "w") as f: | |
| nbf.write(nb, f) | |
| print(f"Generated {out}") | |