Download scripts/gen_gcn_notebook.py from yusiwen/dl-from-scratch: direct link, hf CLI and curl.
- Browser
- Download file 4.37 kB
-
https://huggingface.co/yusiwen/dl-from-scratch/resolve/main/scripts/gen_gcn_notebook.py
- Command line
-
hf download hf://yusiwen/dl-from-scratch/scripts/gen_gcn_notebook.py
-
curl -L -o gen_gcn_notebook.py https://huggingface.co/yusiwen/dl-from-scratch/resolve/main/scripts/gen_gcn_notebook.py
4.37 kB
| #!/usr/bin/env python3 | |
| """Generate GCN 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("# GCN: Graph Convolutional Network\n\nNode classification on citation graphs using spectral graph convolution.\n") | |
| md("""## 背景 | |
| GCN(Kipf & Welling, 2017)将卷积操作推广到图结构数据。核心思想: | |
| 每个节点的特征由其邻居节点加权聚合而来。 | |
| 与图像 CNN 的区别: | |
| - CNN:固定网格结构,卷积核在空间上滑动 | |
| - GCN:任意图结构,卷积由邻接矩阵定义的消息传递实现 | |
| 数据集:**Cora** — 2708 篇论文,每篇用 1433 维词袋向量表示,分为 7 类。边表示引用关系。 | |
| """) | |
| md("""## 数学原理 | |
| ### 图卷积层 | |
| $$H^{(l+1)} = \\sigma\\left(\\hat{A} H^{(l)} W^{(l)}\\right)$$ | |
| 其中 $\\hat{A} = D^{-1/2} A D^{-1/2}$ 是归一化邻接矩阵。 | |
| - $A$: 邻接矩阵(加自环后) | |
| - $D$: 度矩阵 $D_{ii} = \\sum_j A_{ij}$ | |
| - $H^{(l)}$: 第 $l$ 层的节点表示 | |
| - $W^{(l)}$: 可学习的权重矩阵 | |
| ### 2 层 GCN | |
| $$Z = \\text{softmax}\\left(\\hat{A}\\ \\text{ReLU}\\left(\\hat{A} X W^{(0)}\\right) W^{(1)}\\right)$$ | |
| 半监督学习:只用少量标注节点(每类 20 个)训练,模型通过图结构传播标签信息到未标注节点。 | |
| """) | |
| code("""\ | |
| import torch | |
| import torch.nn as nn | |
| import torch.optim as optim | |
| from graph.gcn.model import GCN | |
| from graph.gcn.data import load_cora | |
| 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}") | |
| features, adj_norm, labels, train_mask, val_mask, test_mask, classes = load_cora() | |
| features = features.to(device) | |
| adj_norm = adj_norm.to(device) | |
| labels = labels.to(device) | |
| train_mask = train_mask.to(device) | |
| val_mask = val_mask.to(device) | |
| """) | |
| code("""\ | |
| model = GCN( | |
| in_features=features.size(1), | |
| hidden_dim=16, | |
| num_classes=labels.max().item() + 1, | |
| dropout=0.5, | |
| ).to(device) | |
| print(f"Parameters: {model.num_params():,}") | |
| """) | |
| md("""## 训练 | |
| > ⏱ 预估耗时:**200 epoch × ~0.1s/epoch ≈ 20 秒**(CPU 即可完成) | |
| """) | |
| code("""\ | |
| NUM_EPOCHS = 200 | |
| LR = 0.01 | |
| WEIGHT_DECAY = 5e-4 | |
| criterion = nn.CrossEntropyLoss() | |
| optimizer = optim.Adam(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY) | |
| loss_hist, acc_hist = [], [] | |
| for epoch in range(1, NUM_EPOCHS + 1): | |
| model.train() | |
| optimizer.zero_grad() | |
| output = model(features, adj_norm) | |
| loss = criterion(output[train_mask], labels[train_mask]) | |
| loss.backward() | |
| optimizer.step() | |
| model.eval() | |
| with torch.no_grad(): | |
| output = model(features, adj_norm) | |
| val_acc = (output[val_mask].argmax(dim=1) == labels[val_mask]).float().mean().item() | |
| loss_hist.append(loss.item()) | |
| acc_hist.append(val_acc) | |
| if epoch % 20 == 0 or epoch == 1: | |
| print(f"Epoch [{epoch:3d}/{NUM_EPOCHS}] Loss: {loss.item():.4f} Val Acc: {val_acc:.2%}") | |
| """) | |
| md("""## Loss 曲线""") | |
| code("""\ | |
| import matplotlib.pyplot as plt | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) | |
| ax1.plot(loss_hist); ax1.set_xlabel("Epoch"); ax1.set_ylabel("Loss"); ax1.set_title("Training Loss"); ax1.grid(True) | |
| ax2.plot(acc_hist, color='green'); ax2.set_xlabel("Epoch"); ax2.set_ylabel("Val Acc"); ax2.set_title("Validation Accuracy"); ax2.grid(True) | |
| plt.tight_layout(); plt.show() | |
| """) | |
| md("""## 测试准确率""") | |
| code("""\ | |
| model.eval() | |
| with torch.no_grad(): | |
| output = model(features, adj_norm) | |
| pred = output[test_mask].argmax(dim=1) | |
| test_acc = (pred == labels[test_mask]).float().mean().item() | |
| print(f"Test Accuracy: {test_acc:.2%}") | |
| """) | |
| md("""\ | |
| ## 思考题 | |
| 1. 为什么 GCN 的归一化用 $D^{-1/2} A D^{-1/2}$ 而不是 $D^{-1} A$? | |
| 2. GCN 能处理归纳式(inductive)任务吗?还是只能直推式(transductive)? | |
| 3. 如果不用邻接矩阵只用节点特征,准确率会降到多少? | |
| 4. GCN 层数加深为什么会导致性能下降?(提示:过平滑问题) | |
| """) | |
| nb.cells = cells | |
| with open("graph/gcn/gcn.ipynb", "w") as f: | |
| nbf.write(nb, f) | |
| print("Generated graph/gcn/gcn.ipynb") | |