cs3319-project2 / docs_first_principles /12_reproduction_guide.md
NLP-beginner's picture
CS3319 Project 2 final deliverable (public F1 = 0.96626)
f28d994
|
Raw
History Blame Contribute Delete
18 kB

12 — 复现指南 (Reproduction Guide)

面向"懂一点 ML、但不熟悉图推荐系统"的读者。本文回答一个问题:给我这份代码包,我怎样一步步把 0.96626 的公开榜 F1 跑出来,并且能相信这个数字? 讲为什么,不只给命令。

一句话回顾:最终方法是两阶段 stacking —— Stage-1 多源(LightGCN 集成 / BPR-MF / 7 个 DeepWalk/Node2Vec 块 / 内容画像 / 显式 meta-path / 高阶有向引用传播 $H_k=R\cdot C^k$、$G_k=S\cdot R\cdot C^k$)生产 raw 分数与嵌入,Stage-2 由 259 维 LightGBM meta-learner 融合,测试决策用 rank cutoff top 50% + 强制已知训练正边为 1。最终公开 F1 = 0.96626


0. 复现的三档可执行性(先把预期摆正)

本副本不是"克隆即跑"的开源项目。先分清三个层次,避免踩坑后误判"代码坏了":

档次 能跑什么 当前副本状态 你需要做什么
A. 直接运行(读结果) 查看 CSV / .npy / submission ✅ 全部存在(见 README 的 Quick Verification) 不需要任何依赖,直接看文件
B. 当前副本可运行(从缓存重算) 重跑最终 stacker high_order_graph_stack.py、重生成图表、重编译论文 ✅ 缓存的 score/feature/RW 权重都在 requirements-minimal.txt 即可,CPU 够用
C. 理论完整包可运行(从零重训) 重训 LightGCN/BPR/HGT/SAGE、重跑随机游走 ⚠️ 当前副本缺 LightGCN 训练产出的若干中间产物(bigbatch_more 等仅留 scores/ 子目录,无 model_results.csv/ensemble_result.txt),且重训需 GPU 需 GPU + 完整 environment-cs3319.yml,部分历史 run 精确参数未在包内可证(见事实表 open_questions)

关键结论: 复现最终结果(B 档)是稳的;复现"整个训练历史(C 档)"只能近似,因为部分 LightGCN run 的精确 argparse 没有日志留存。


1. 环境依赖

1.1 两种安装方式

仓库 env/ 目录提供两份清单(均已确认存在):

  • env/environment-cs3319.yml —— 原始 conda 环境的完整导出,含 CUDA 12.8 系列的 torch==2.12.0.dev20260407+cu128torch-geometric==2.7.0lightgbm==4.6.0numpy==2.2.6 等,Python 3.10。适合 C 档(带 GPU 重训)。
  • env/requirements-minimal.txt —— 10 个 pinned 包(numpy/pandas/scipy/scikit-learn/lightgbm/torch/torch-geometric/tqdm/joblib/matplotlib)。适合 B 档(只用缓存的 CPU 复现 + 出图)。
# 方式一:conda(完整,带 GPU 栈)
conda env create -f env/environment-cs3319.yml
conda activate cs3319

# 方式二:pip minimal(纯 CPU 也能复现最终 stacker + 出图)
pip install -r env/requirements-minimal.txt

1.2 minimal 与完整环境的差异(为什么 minimal 缺东西)

minimal 完整 env 影响
gensim 只影响重训 Word2Vec;复现已含 7 个 .model 权重,不影响 B 档
node2vec 同上,影响重训 Node2Vec block
xgboost 仅早期搜索脚本用过,最终方法用 LightGBM
networkx 仅部分 RW/分析脚本依赖
CUDA / torch+cu128 ❌(CPU torch) 见第 6 节,GPU 仅重训需要

README 与 minimal 的措辞差:README L104-116 列的依赖里包含 xgboost / gensim / node2vec,但 requirements-minimal.txt 没列。这不是冲突 —— minimal 是"复现最终结果"的最小集,完整 README 列表是"开发全栈"。两者用途不同。


2. 数据路径约定

所有路径相对仓库根解析,没有单独的 config 文件。每个脚本首参都是 --package-root(默认 = code/ 的父目录):

parser.add_argument("--package-root", type=Path,
                    default=Path(__file__).resolve().parents[1])

输入在 data_and_docs/,输出在 validation_runs/:

文件 行数(实测) 含义
data_and_docs/bipartite_train_ann.txt 682,421 author→paper 训练正边
data_and_docs/bipartite_test_ann.txt 2,047,262 (~2.05M) 待预测对
data_and_docs/author_file_ann.txt 9,663 author↔author 无向合著
data_and_docs/paper_file_ann.txt 327,113 paper→paper 有向引用
data_and_docs/feature.pkl (79,937, 512) torch.float32,~156 MB 每篇论文的 USE 嵌入

缓存与权重:

  • 切分:validation_runs/dynamic_seed202/val_pairs_seed202.npy (136,484 对)、val_labels_seed202.npy(1:1 正负)。
  • 缓存特征:validation_runs/feature_cache/(content_mean / content_rich / topk_content / high_order_val202 / high_order_directed_val202,带 _k1500 即 top-k 截断)。
  • RW 权重:validation_runs/dynamic_seed202/randomwalk_systematic/models/(7 个 Word2Vec .model)。
  • LightGCN checkpoint:checkpoints/extra_models/checkpoints/final_ens6/(6 个 model_lgcn_*.pt)。
  • 已知正边 mask:cached_scores/test_known_mask.npy(2,047,390 字节,force=1 用)。

3. 快速验证最终结果(零计算,A 档)

按 README "Quick Verification",不需要装任何依赖,直接读文件确认数字:

cd D:/reps/26H1_cs3319_final_deliverable

# 1. 最终验证指标(应为 rich_rw7_highorder_directed F1 = 0.966873736337297)
cat validation_runs/dynamic_seed202/high_order_graph_stack/validation_summary.csv

# 2. 各 submission 的正例比例(应见 ratio 0.500000)
cat validation_runs/dynamic_seed202/high_order_graph_stack/submission_summary.csv

# 3. 确认公开最佳提交文件存在
ls -lh validation_runs/dynamic_seed202/high_order_graph_stack/submissions/ \
        submission_rich_rw7_highorder_directed_r0.500000.csv

为什么这是可信的验证:事实表 §2.8 已交叉复核 —— val_labels_seed202.npy(136,484)对 rich_rw7_highorder_directed_oof.npy 的 best F1 = 0.966874 / AUC 0.994918,与 validation_summary.csv L2 逐位一致,证明 val-label↔OOF 已对齐、无标签泄漏。


4. 从缓存复现最终 stacker(B 档,核心命令)

这是最重要的复现命令。它不重训任何 GNN,而是把已缓存的 259 维特征 + RW 权重拼起来,重训第二阶段 LightGBM 并重出提交:

cd D:/reps/26H1_cs3319_final_deliverable
python code/high_order_graph_stack.py \
  --package-root . \
  --split-seed 202 \
  --seed 202 \
  --n-splits 5 \
  --make-submission

它在做什么(第一性原理):

  1. val_pairs_seed202.npy 与各缓存特征矩阵(content_rich / RW 7 块 / high_order directed+undirected)。
  2. 按固定顺序拼成 259 维 X(构成见事实表 §2.5:84 X_base + 18 rich + 77 RW + 11 RW-agg + 24 undir + 45 dir)。
  3. 5 折 StratifiedKFold LightGBM OOF(num_leaves=15, reg_lambda=8, lr=0.022, n_est=1400),得到 leak-free 验证 F1。
  4. best_f1(y, oof) 在 PR 曲线上取最优阈值(= 0.46173080801963806)报告验证 F1 = 0.966874
  5. 全量重训 LightGBM 打分测试对 → rank cutoff top 50% → 叠加 test_known_mask.npy 强制已知训练正边 = 1。

为什么测试用 rank 而不是阈值 0.4617? 因为验证集是人工 1:1 正负,而测试集真实正先验未知(事实表 open_questions:无法从文件核验);LightGBM 概率在 val→test 分布上漂移(阈值迁移后正例率漂到 0.5242)。rank top 50% 把决策从"概率门槛"换成"相对排序",更稳健。

它重写这些文件:

validation_runs/dynamic_seed202/high_order_graph_stack/
  ├─ validation_summary.csv
  ├─ rich_rw7_highorder_directed_test_pred.npy
  ├─ submission_summary.csv
  └─ submissions/submission_rich_rw7_highorder_directed_r0.500000.csv

复现核对点:重跑后 validation_summary.csv 的 F1 应为 0.966873736337297。若不一致,最常见原因是缓存特征被删或 --split-seed 不是 202(见 §7)。


5. 从 checkpoint 复现早期 LightGCN 提交(B/C 档)

复现 README 提到的早期 6-model LightGCN 集成(公开 F1 0.93044):

python code/generate_ens6_submission.py --package-root . --device cuda:0
# 无 GPU 时:
python code/generate_ens6_submission.py --package-root . --device cpu

它做什么:加载 checkpoints/final_ens6/ 下 6 个 model_lgcn_*.pt(generate_ens6_submission.py:194-199 定义 --device,默认随 CUDA 可用性),对全部测试对打分求均值 ensemble,按阈值列表 --thresholds 切 0/1 写 sub_ens6_t*.csv(L249-258)。已确认的公开最佳早期文件是 sub_ens6_t0.36.csv(F1 0.93044)。

注意:这是早期独立提交,不是最终 stacker 的输入。最终 stacker 用的是 dyn202_l2d512_bpr_bigbatch_more 的 vanilla ensemble 主分数(val_vanilla_ensemble_mean.npy),两者是不同阶段产物(事实表 §3 第 1 行 vs 时间线序号 1)。CPU 模式跑 ~2.05M 对会慢,但不影响正确性。


6. 哪些步骤需要 GPU,哪些只需 CPU

步骤 GPU? 说明
查看结果(A 档) 纯读文件
high_order_graph_stack.py 最终 stacker LightGBM 是 CPU-only,特征已缓存
generate_ens6_submission.py(inference) ⚠️ 可选 有 GPU 用 cuda:0 快很多;CPU 也能跑完
LightGCN 训练(train_val_lgcn_ensemble.py) BPR 训练需 --device cuda:0
BPR-MF 训练(train_val_mf_bpr.py / extra_score_sources_ablation.py) dim256, batch65536
HGT / SAGE(train_val_hgt_bpr.py / train_val_sage_bpr.py) 仅做变体/消融,未入最终
DeepWalk/Node2Vec 训练 gensim/node2vec 走 CPU(但需重装这俩包)

结论:若只复现最终 0.96626,纯 CPU + requirements-minimal.txt 足够。 GPU 仅在你想重训 Stage-1 各源模型(C 档)时需要。


7. 常见错误与排查

7.1 缺 lightgbm / gensim / node2vec

  • 症状:最终 stacker 报 ModuleNotFoundError: lightgbm
  • 根因:用了系统 Python 而非装了 requirements-minimal.txt 的环境。事实表 §7 备注也提到审计机 Python 3.14 缺 lightgbm/gensim,因此论文里的"特征重要性图"只能用消融贡献瀑布替代 LightGBM gain。
  • 解决:pip install -r env/requirements-minimal.txt;重训 RW 才需额外 pip install gensim node2vec

7.2 改 --split-seed 非 202 → 全链失效

  • 症状:换 seed 后 F1 大幅下降或文件找不到。
  • 根因:--split-seedload-bearingmake_notebook_style_split(seed=202)(见 train_val_lgcn_ensemble.py:132-165)把 90/10 留出 + 等量随机负例烤进每个 .npy/.npz 文件名与内容。所有缓存分数都是 seed-specific。
  • 规则:--split-seed 必须保持 202;换 seed = 必须从头重跑整个 Stage-1→Stage-2 链路,缓存全部作废。

7.3 硬编码图维度 6611 / 79937

  • 位置:high_order_graph_stack.py(sparse-matrix shape)与 train_val_lgcn_ensemble.py:216(range(6611) 采样循环)字面写死。
  • 后果:换数据集会直接 IndexError 或 shape 不匹配。
  • 注意:不要"顺手"把它改成变量 —— 它是与数据集绑定的设计选择,改了反而引入隐性 bug。

7.4 路径写法:Windows PowerShell / bash 一律用正斜杠

  • 反斜杠 \ 在 bash 里是转义,在 PowerShell 里也易出错。统一用正斜杠 python code/high_order_graph_stack.py --package-root .,不要 code\high_order_graph_stack.py
  • --package-root.(仓库根)即可;脚本内部用 Path 拼接,跨平台安全。
  • 遗留坑:部分 metadata 文件(如 selected_variant_val_scores.txt)含绝对路径 /data/lzc/...,事实表 §18 已声明 —— 以 package-root 为准,这些只是存证残留,不影响复现命令。

7.5 legacy 脚本不能直接跑

run_baseline.py / run_improved.py / run_v2.py / run_final.py / run_ultimate.py / run_lgcn_final.py / run_lgcn_v2.py / run_graph_features.py / compare_gnn.py 含硬编码 /home/lzc/cs3319-project 路径、无 argparse。仅作存证,不要试图直接运行;用现代 train_val_* / generate_* / *_ablation 脚本替代。


8. 生成论文图表

本副本没有单一的 make_all_figures.py —— 图表拆成 code/figures/ 下 10 个独立脚本 + 共享样式 plot_style.py + 复算脚本 gen_val_labels.py:

code/figures/
  ├─ plot_style.py              # 共享 matplotlib 风格(每个 figN import 它)
  ├─ fig1_dataset_overview.py   # 数据集规模/稀疏/长尾
  ├─ fig2_framework.py          # 两阶段框架
  ├─ fig3_progression.py        # 方法演进折线(验证 F1 + 公开 F1)
  ├─ fig4_highorder_ablation.py # 高阶消融 108→190→214→259
  ├─ fig5_pr_roc.py             # PR/ROC 曲线
  ├─ fig6_feature_contribution.py # 消融贡献瀑布(非 LightGBM gain!)
  ├─ fig7_error_analysis.py     # 误差分析
  ├─ fig8_lgcn_ensemble_correlation.py
  ├─ fig9_lgcn_hyperparam.py
  ├─ fig10_rw_ensemble_size.py  # RW 1→5→7 块增益
  └─ gen_val_labels.py          # 复算 val-label↔OOF 对齐

逐个运行即可,例如:

cd D:/reps/26H1_cs3319_final_deliverable
python code/figures/fig3_progression.py
python code/figures/fig4_highorder_ablation.py

图 6 的特别说明:因审计机无 lightgbm,"特征重要性"无法用 LightGBM gain 画,改用消融贡献瀑布(事实表 §7)。复算时注意事实表 §21 的裁决:figures 标题写的 RW 稳态增益 "+0.00182" 偏高,CSV 重算应为 +0.00155,出图时以此为准。


9. 编译 ACM 论文(中文,xelatex)

论文模板在 ACM_Conference_Proceedings_Primary_Article_Template/,主文是 cs3319_final_paper_cn.tex,用 acmart 类 + xeCJK(字体 Noto Serif/Sans CJK SC)。因为是 CJK,必须用 xelatex,不能用 pdflatex:

cd ACM_Conference_Proceedings_Primary_Article_Template
xelatex cs3319_final_paper_cn.tex
bibtex cs3319_final_paper_cn
xelatex cs3319_final_paper_cn.tex
xelatex cs3319_final_paper_cn.tex
  • 模板自带 acmart.clsACM-Reference-Format.bst,目录内已有编译产物 cs3319_final_paper_cn.pdf 与逐页 PNG,可先看成品。
  • 已确认 BUILD_REPORT.md / PAPER_BUILD_AUDIT.md 记录了构建细节,编译遇问题先查这两份。
  • 前置:系统需装 Noto CJK SC 字体族;否则 \setCJKmainfont 会报 fontspec 找不到字体。

10. 端到端从头复现(C 档,仅参考)

若要完整重训(非必需),按事实表 §4 的 stage 顺序:

1. train_val_lgcn_ensemble.py    → val_vanilla_ensemble_mean.npy(主分数,需 GPU)
2. generate_post95_submission.py → 变体选择 + topk
3. extra_score_sources_ablation.py → BPR-MF + content_mean(需 GPU)
4. randomwalk_systematic_ablation.py --mode small → 6 Word2Vec;再 --mode graph → 4 个(需 gensim,CPU)
5. generate_randomwalk_ensemble_submission.py --versions <7 个块> → RW 一致性 11 维
6. high_order_graph_stack.py --make-submission → 最终 stacker(CPU)

未明确确认项(事实表 open_questions):bigbatch_more 精确的 --train-batch-size / --lr / --epochs 无日志,仅能从 run 名 l2d512 + 分数文件名反推 layers=2, dim=512, eval-mode=dot, seeds{41,141}。其余 13 个 LightGCN 变体参数同理无证。因此 C 档只能"近似复现",B 档才是严格复现。


11. 复现流程图(文本块)

解压副本
   │
   ├─(A) 读文件 → cat validation_summary.csv → 见 0.966874 ✅ 零依赖
   │
   ├─(B) pip install -r env/requirements-minimal.txt (CPU)
   │      └─ python code/high_order_graph_stack.py --package-root . --split-seed 202 --seed 202 --n-splits 5 --make-submission
   │            └─ 验证 F1=0.966874 / 公开 F1=0.96626 ✅ 严格可复现
   │
   ├─ 图表:python code/figures/figN_*.py(逐个,N=1..10)
   │
   ├─ 论文:cd ACM_*; xelatex cs3319_final_paper_cn.tex(+bibtex+两遍)
   │
   └─(C) conda env create -f env/environment-cs3319.yml + GPU
          └─ 从头重训 LightGCN/BPR/RW → 部分历史 run 参数未留存,仅近似

12. 可迁移到论文中的写法(附录 Reproducibility,可直接进 TeX)

以下为论文附录"复现性(Reproducibility)"小节的正式表述,可直接粘贴到 cs3319_final_paper_cn.tex 的 appendix 部分,术语与正文一致。

\subsection{复现性说明}
本研究提供三档可执行性,以适应不同审阅场景:
\textbf{(A) 结果核对}无需任何依赖,直接读取
\texttt{validation\_runs/dynamic\_seed202/high\_order\_graph\_stack/validation\_summary.csv}
即可确认验证 $\Fone=0.966874$;该数值已由 \texttt{val\_labels\_seed202.npy} 对
OOF 数组的交叉复算逐位对齐(AUC $0.994918$),无标签泄漏。
\textbf{(B) 严格复现}仅需 CPU 与
\texttt{env/requirements-minimal.txt}(含 \texttt{lightgbm==4.6.0} 等 10 个 pinned 包),
运行 \texttt{code/high\_order\_graph\_stack.py --make-submission}
即可从预缓存特征矩阵与 7 个随机游走权重重建最终 259 维 LightGBM 模型,
重写验证 F1 与公开提交 \texttt{submission\_rich\_rw7\_highorder\_directed\_r0.500000.csv}。
测试决策采用 rank cutoff(取分数排序 top $50\%$ 为正),而非验证最优概率阈值
($0.4617$),因前者对验证集(人工 $1{:}1$)到测试集的分布漂移更稳健。
\textbf{(C) 从零重训}需 GPU(\texttt{--device cuda:0})与完整
\texttt{env/environment-cs3319.yml}(含 \texttt{gensim}/\texttt{node2vec});
个别早期 LightGCN 配置的精确超参未保留日志,故该档仅能近似复现训练历史。

\noindent 关键约束:(i) 验证切分种子 \texttt{--split-seed} 固定为 $202$,
所有缓存分数均与其绑定,更改种子需重建整条 Stage-1 链;
(ii) 图规模字面量 $6611$ (作者) 与 $79937$ (论文) 在
\texttt{high\_order\_graph\_stack.py} 与 \texttt{train\_val\_lgcn\_ensemble.py}
中硬编码,与数据集绑定;
(iii) 路径一律相对仓库根 \texttt{--package-root} 解析,Windows 下建议使用正斜杠。