File size: 13,450 Bytes
f28d994
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# 10 · 代码地图与入口脚本(System Code Map)

> 本文档是 `code/` 目录的**导航地图**:30+ 个脚本各自干什么、谁是入口、谁是被运行时加载的"库"、谁是只能看不能跑的 legacy。读完本文你应当能在 30 秒内回答:"我想改 X,该动哪个文件?""我想重跑最终方法,敲哪条命令?""为什么没有 `utils.py`?"
> 代码定位以 `docs_first_principles/_fact_sheet.md` §6 为准(状态日期 2026-06-18)。路径相对仓库根,带行号。

目标读者:第一次进仓库、对"`importlib` 互相加载 + 无共享包"这套非典型工程组织感到困惑的人。先讲**为什么这样组织**(第一性原理),再给**地图与入口**。

---

## 1. 第一性原理:为什么没有 `utils.py`,而是"脚本互加载"

### 1.1 典型工程 vs 本仓库

典型项目会把公共函数抽到 `utils.py` / `common/` 包,其他模块 `import`**本仓库没有。** 它是研究/实验代码,追求"一个脚本跑完一个实验、改完立刻能跑"。于是演化出一种**去中心化的代码复用**:每个脚本用一段近乎相同的 `load_module()` helper 把**同目录的另一个脚本**当模块加载:

```python
def load_module(name, path):
    spec = importlib.util.spec_from_file_location(name, path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module
```

### 1.2 这样设计的后果(读者必须知道)

- **没有显式接口/版本**。函数签名、返回顺序全靠"加载方与被加载方的默契"。读下游脚本时,常需要跳到被加载脚本确认函数行为。
- **两个脚本成了事实共享库(de-facto shared library)**,被 ~14–16 个脚本加载(见 §4)。**改这两个文件的任何函数,等于同时改了十几个下游脚本的行为**——blast radius 极大。
- **每个脚本各自重定义本地 `read_txt`** 等小工具,不统一。

> 这不是"坏味道"要重构,而是研究代码的合理权衡:**快速试错优先于工程整洁**。理解了这一点,才不会被"为什么这么乱"绊住,而能专注"数据怎么流"。

---

## 2. 脚本分类速览(四类)

| 类别 | 命名模式 | 角色 | 例子 |
|---|---|---|---|
| **训练** | `train_val_*.py` | Stage-1 模型训练 + 出验证/测试分数 | `train_val_lgcn_ensemble.py` |
| **特征/分数消融** | `*_ablation.py` | 单组特征验证 + 拼特征矩阵 | `randomwalk_systematic_ablation.py` |
| **submission 生成** | `generate_*submission.py` | 把分数/特征转成提交 CSV | `generate_post95_submission.py` |
| **stacking / 校准 / 搜索** | `stack_*``score_level_*``error_group_*``search_*``rich_randomwalk_stack.py` | 二阶段融合、阈值/比例搜索、误差分析 | `stack_rank_calibration.py` |
| **(存证)** | `run_*.py``compare_gnn.py` | **legacy,不可直接跑**(见 §6) | `run_baseline.py` 等 9 个 |

---

## 3. 三条核心入口命令

### 3.1 重跑最终方法(出 submission)

```bash
python code/high_order_graph_stack.py \
  --package-root . --split-seed 202 --seed 202 --n-splits 5 --make-submission
```

- **命中全部缓存**(`feature_cache/``randomwalk_systematic/models/`、LightGCN 分数),CPU 秒级出 OOF + submission。
- 不加 `--make-submission` = 仅验证。
- 详见 `03_data_flow_and_artifacts.md` §9。

### 3.2 生成图表

本仓库有**两套图表包**(见记忆 `figures-and-label-alignment`):

```bash
# ACM 双栏正式版(3.25/6.75 in, pdf.fonttype=42),论文用
python figures_paper/scripts/make_all_figures.py --package-root .

# 早期 seaborn 版(更大字号),探索/汇报用
# (code/figures/ 下各 figN_*.py 独立可跑)
```

`figures_paper/scripts/make_all_figures.py` 自动建目录、逐图 try/except 隔离、读真实数据(缺失则优雅跳过)、生成 pdf/png/svg + `README_figures.md`**单个图失败不会拖垮其余图。**

### 3.3 编译论文 TeX

> **诚实说明(重要):** 本仓库**不包含论文 `.tex` 工程或编译命令**。`docs_first_principles/SUMMARY_FOR_PAPER.md` 提供的是**可直接迁移进 TeX 的成稿段落、表格、公式、caption**(中文,保留英文术语),以及建议的 `\section` 结构。实际编译发生在用户的**独立论文项目**里,把 SUMMARY 的段落复制进 `.tex`、按需改 `\[ \]` / `equation` 环境即可。本审计遵循"不覆盖/不改论文模板"的约束,未在仓库内增设任何 TeX 构建。

---

## 4. 两大事实共享库(改动 blast radius 最大)

被 ~14–16 个脚本经 `load_module()` 运行时加载,改它们 = 改全链:

### 4.1 `train_val_lgcn_ensemble.py`(数据 + 切分 + 评分)

| 函数/符号 | 行号 | 作用 | 为什么被到处加载 |
|---|---|---|---|
| `make_notebook_style_split` | 132-165 | seed=202 切分 → 1:1 验证集(136,484 对) | **所有下游都要用同一验证 pair/label** |
| `build_parts` | 168-236 | 加载 feature.pkl + 度特征 + popular 集 + coauthor_pool(L216 硬编码 `range(6611)`) | 负采样/特征都依赖这些预计算 |
| `build_data` | 239-270 | PyG HeteroData:4 边类型,引用/合著双向拼接,论文 515→embed 投影 | LightGCN 训练数据装配 |
| `LightGCNLayer` / `encode` / `decode` | 49-104 | 纯加权邻居聚合(无 W 无非线性)+ 点积解码 | 模型本体 |
| `sample_hard_negatives` | 273-305 | 硬负采样 random50%/popular25%/coauthor25% | 训练循环 |
| `best_f1` | 340-346 | PR 曲线 argmax 最优 F1 阈值 + AUC | **所有 stacker 算验证 F1 都用它** |
| `predict_scores` | 308-337 | no_grad 批量打分(cos/dot/neg_l2) | 推断 |

### 4.2 `stack_rank_calibration.py`(显式图特征 + rank + OOF)

| 函数/符号 | 行号 | 作用 |
|---|---|---|
| `ExplicitGraphFeatures.__init__` | 54-106 | 默认 6611 作者 / 79937 论文;预计算 shared_paper_authors(A-P-A) + coauthor_paper_union(A-A-P) |
| `ExplicitGraphFeatures.transform` | 108-145 | 18 维手工特征(L139 与 L130 重复一列,台账 #13) |
| `add_rank_features` | 148-160 | 4 列:score/global_rank/author_pct/author_rank |
| `fit_oof` | 163-184 | 5 折 StratifiedKFold;LGBM(1200, lr0.025, reg_lambda5) |

> 关键事实:本文件**全文无 `scipy.sparse`**(台账 #12)——CLAUDE.md 所指的"sparse-matrix 高阶传播"在 `high_order_graph_stack.py`,**不**在此文件。meta-path 全用 Python set 交/并集实现。

### 4.3 其他高频被加载的 peer

| 脚本 | 被加载的关键函数 | 用途 |
|---|---|---|
| `generate_post95_submission.py` | `select_variant_val_scores``topk_content_similarity_fast` | 变体选择 + top-k 内容相似 |
| `post95_ablation.py` | `negative_evidence_features` | 8 维负证据 |
| `extra_score_sources_ablation.py` | `content_mean_score``score_to_features` | content_mean-cos + BPR-MF |
| `randomwalk_systematic_ablation.py` | `build_base_features`、`pair_feature_block` | X_base 84 维 + 每块 11 维 RW |
| `content_rich_ablation.py` | `content_rich_features` | 从 feature.pkl 产 18 维 rich |
| `generate_randomwalk_ensemble_submission.py` | `aggregate` | 11 维 RW 一致性聚合 |

---

## 5. 最终入口:`high_order_graph_stack.py` 为什么是终点

它是**唯一同时做三件事**的脚本:① 拼接全部 259 维特征;② 训练最终 LightGBM OOF stacker;③ 出 submission。逻辑上它是 Stage-2 的实现 + 决策规则的实现,所以是"最终方法入口"。

| 关键符号 | 作用 | 备注 |
|---|---|---|
| `build_high_order*` | $H_k=R\cdot C^k$、$G_k=S\cdot R\cdot C^k$,fwd/bwd/undir | **scipy.sparse 幂乘 + top-k 剪枝(k=1500)**;硬编码稀疏矩阵形状 6611×79937(见 §7) |
| `fit_full_predict` | LightGBM(num_leaves=15, reg_lambda=8, lr=0.022, n_est=1400) 5-fold OOF | 最终超参(事实表 §2.7) |
| 决策 | sort → top 50% = 1 + `test_known_mask.npy` 强制已知正边 | rank-cutoff(详见 08) |

输出目录:`validation_runs/dynamic_seed202/high_order_graph_stack/`(`validation_summary.csv``*_oof.npy``*_test_pred.npy``submissions/*.csv`)。

---

## 6. Legacy 脚本:能看不能跑

以下 9 个是早期原型,**硬编码 `/home/lzc/cs3319-project` 路径、无 argparse**,仅作存证(provenance)。要跑必须先改路径,且功能已被现代 `train_val_*` / `generate_*` / `*_ablation` 取代——**不要用它们复现结果**:

```
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
```

> 它们的价值:记录"从复杂 GNN(`HeteroMeanConv`/SAGE)退回轻量 CF(LightGCN)"的方法演进史(见 `09_experiment_timeline.md` §3.1)。`notes/experiment_history.md` 有对应叙事。

---

## 7. 完整脚本地图表

> "改动影响"列:🔴 共享库(改动影响十几个下游);🟡 多下游加载;🟢 独立入口(改了只影响自己);⚪ legacy(勿动)。
> 硬编码维度 **6611(作者)/79937(论文)** 在 `high_order_graph_stack.py`(稀疏矩阵形状)与 `train_val_lgcn_ensemble.py`(L216 采样循环)字面出现,勿随手"重构"成变量(它们匹配本数据集,见 CLAUDE.md gotchas)。

| 脚本 | 角色 | 主要输入 | 主要输出 | 改动影响 | 备注 |
|---|---|---|---|---|---|
| `train_val_lgcn_ensemble.py` | LightGCN 训练 + 切分 + 评分 | `bipartite_train_ann.txt`、`feature.pkl` | `scores/val_vanilla_ensemble_mean.npy`、切分快照 | 🔴 | 两大共享库之一;`best_f1`/`make_notebook_style_split` 全链用 |
| `stack_rank_calibration.py` | 显式图/meta-path 特征 + rank + OOF | LightGCN 分数、`bipartite_train` | explicit18 + rank4 特征、`fit_oof` | 🔴 | 两大共享库之一;**无 sparse** |
| `high_order_graph_stack.py` | **最终 stacker + submission** | 全部 259 维特征(命中缓存) | `*_oof.npy`、`*_test_pred.npy`、`submission_*.csv` | 🟢 | 最终入口(§3.1);**有 sparse 幂乘** |
| `generate_post95_submission.py` | 变体选择 + top-k 内容 + submission | LightGCN 变体分数、test 镜像 | variant43、topk3、submission | 🟡 | `select_variant_val_scores`/`topk_content_similarity_fast` |
| `post95_ablation.py` | 负证据特征 | X_base 前 84 列 | neg8 | 🟡 | `negative_evidence_features` |
| `extra_score_sources_ablation.py` | BPR-MF + content_mean-cos | LightGCN 分数、`feature.pkl` | `val_mf_bpr_s202_d256.npy`、content_mean | 🟡 | `content_mean_score`/`score_to_features` |
| `content_rich_ablation.py` | rich content 画像 | `feature.pkl` | content_rich_*.npy(18 维,命中 feature_cache) | 🟡 | `content_rich_features` |
| `randomwalk_systematic_ablation.py` | 7 块 DeepWalk/Node2Vec | 异构图边 | 7× `.model` + `pair_features/*.npz`(11 维/块) | 🟡 | `build_base_features`/`pair_feature_block`;需 GPU/Word2Vec |
| `generate_randomwalk_ensemble_submission.py` | RW 一致性聚合 + submission | 7 块 pair 特征 | aggregate 11 维、submission | 🟡 | `aggregate()` |
| `score_level_*``error_group_*``search_*` | 阈值/比例搜索、误差分桶 | OOF、各 submission | 分析 CSV(出图用) | 🟢 | 如 `error_analysis_buckets.csv` |
| `rich_randomwalk_stack.py` | RW 专用 stacker | RW 特征 | RW 阶段 OOF/CSV | 🟢 | 中间实验 |
| `generate_ens6_submission.py` | 早期 6-model ensemble submission | checkpoints/ens6 | 6-model submission | 🟢 | 0.93044 那版(早期) |
| `run_*.py``compare_gnn.py`(9 个) | legacy 原型 | — | — | ⚪ | 硬编码 `/home/lzc`、无 argparse,勿直接跑(§6) |

---

## 8. 常见问题速答

| 问题 | 答案 |
|---|---|
| 没有 `utils.py` 怎么共享代码? | `importlib.util``load_module()` 运行时加载同目录脚本(§1) |
| 改一个公共函数影响多大? | 改 `train_val_lgcn_ensemble.py`/`stack_rank_calibration.py` = 影响十几个下游(🔴);改入口脚本只影响自己(🟢) |
| 为什么有的脚本没 `argparse`? | 那是 `run_*.py` legacy,早期原型,已被现代脚本取代(§6) |
| 最终方法入口是? | `high_order_graph_stack.py --make-submission`(§3.1) |
| 图表怎么出? | `figures_paper/scripts/make_all_figures.py --package-root .`(§3.2) |
| 论文 TeX 在哪编译? | 仓库内**没有** TeX 工程;SUMMARY_FOR_PAPER 提供可迁移段落,在用户独立项目编译(§3.3) |
| 6611/79937 哪来的? | 数据集固定规模,字面硬编码在两个脚本里,匹配本数据集勿重构(§7) |

---

## 可迁移到论文中的写法

> **实现概述段(Implementation,可直译进 TeX)。** 我们的系统由若干 Python 脚本组成,按"训练 / 特征消融 / 提交生成 / 堆叠校准"四类组织。其中两个脚本——`train_val_lgcn_ensemble.py`(数据装配、确定性验证切分、最优 F1 阈值)与 `stack_rank_calibration.py`(显式图与元路径特征、排名校准、out-of-fold 训练)——作为共享基础库,被约十余个下游脚本以运行时模块加载的方式复用,故其接口在整条流水线中保持一致。最终的两阶段堆叠与 rank-cutoff 决策由 `high_order_graph_stack.py` 实现:它将来自协同过滤、矩阵分解、随机游走、内容画像与有向高阶引用传播的全部 259 维特征横向拼接,以强正则 LightGBM 作二级元学习器融合。关键的高阶传播以稀疏矩阵幂乘实现($H_k=R\,C^k$、$G_k=S\,R\,C^k$),并通过每次幂乘后的 top-k 行裁剪将 $|P|\times|P|$ 的稠密化控制在可计算规模。所有中间分数与特征均缓存于 `validation_runs/` 下,并依确定性切分种子(seed=202)组织,保证实验可复现。