目录

Jittor 计图挑战赛 赛题一:基于图学习的动态推荐任务 — 五一队

Jittor Python License

阶段 线上成绩 拆分 名次
A 榜 1.5636 dataset1 0.9386 + dataset2 0.6250 第 4
B 榜 1.4705 dataset3 0.94083 + dataset4 0.52963 第 4

简介

本项目是计图(Jittor)挑战赛 赛题一:基于图学习的动态推荐任务 五一队的完整方案实现。

赛题本质是时序链接预测的排序化形式:给定一条查询「源节点 src + 时间 time + 100 个候选 目标节点 dst」,对 100 个候选各给一个分数,官方只看行内相对排序,指标为 MRR (随机基线 ≈ 0.052),两个数据集的 MRR 直接相加为总分;测试集时间严格晚于训练集, 是真时序外推而非随机切分。

本方案的特点是:不做单个大模型,而是让四类互相异质的信息源在一个 listwise 精排器里汇合, 并用 5 折 OOF 作为每次线上提交的硬门禁。四层管线在 A、B 两榜四个数据集上共用同一套代码, 靠 is_social(ds) 分派到「社交图 / 二部图」两个特征分支。所有神经网络均为计图(Jittor) 实现(SASRec / BPR-MF / LightGCN / listwise 精排器),numpy / scipy 仅用于图统计与稀疏矩阵运算。

方法总览

                       train.csv (src, dst, time)          test.csv (src, time, 100 candidates)
                                  │                                        │
  ┌───────────────────────────────┴────────────────────────────┐           │
  │ ① 图统计特征                                                │           │
  │   直连重复 / 反向边 / 共同邻居族(有向拆分) / Adamic-Adar / RA │           │
  │   马尔可夫转移 / 多窗口共现 / as-of 时点热度 / 小时级热度窗    │           │
  │   个性化 PageRank(PPR) / 有向 2-hop / 用户侧与物品侧 CF       │           │
  ├────────────────────────────────────────────────────────────┤           │
  │ ② 表征模型分数列(全部计图实现,每类 2-4 个配置各出一列)      │           │
  │   SASRec ── 序列 Transformer                                │           │
  │   BPR-MF ── 加权 BPR,热度^0.75 负采样 + 时间衰减             │           │
  │   LightGCN ─ 端到端 K 跳对称归一传播(梯度穿过传播)           │           │
  │   item2vec ─ 共现传播物品嵌入                                │           │
  ├────────────────────────────────────────────────────────────┤           │
  │ ③ 候选矩阵自指统计 ◄────────────────────────────────────────┼───────────┘
  │   tfreq / tsib 族 / tsim 族 / trecip / tem 族(EM 软计数)      │  只统计官方发放的候选矩阵
  │   —— 只对「输入」做聚合统计,全程不接触任何标签                │  这一**输入**本身
  └───────────────────────────────┬────────────────────────────┘
                                  ▼
  ④ listwise 精排器(计图):100 候选内 softmax 交叉熵,5 折 OOF,预测取 5 折平均
     dataset1/3(社交图):MLP,d3 用**按时间划分**的 5 折
     dataset2/4(二部图):单层候选间 self-attention
     强信号列附一份**查询内百分位**副本(val/test 绝对分布会漂移,行内排名不会)
                                  ▼
             逐行 min-max 归一到 [0,1],8 位小数 → dataset{N}.csv

三条经验(每一条都有线上实测支撑,详见 docs/RESULTS.md):

  1. 分来自找到新的信息源,而不是把模型做大。 三种精排器结构(MLP / DCN / attention)的 OOF 完全等价;重组已有信号刷不出真分(多加 SASRec 列做 seed 集成:离线 +0.0043、线上 −0.0004)。
  2. 验证集与测试集是否同构,是这类候选排序任务里最容易被忽略、也最便宜的一笔分。 dataset4 把验证集从「按行抽样」改成「按用户抽样」,一列特征没加,线上 +0.0039, 而同期新增的 8 个特征列合计 −0.0001。
  3. 不同类型的特征,离线→线上的兑现率差一个量级。 模型分数列 ≈ ×1 精确转换, 候选矩阵统计列 ×2.5~12(本地验证集比线上测试集小 9.3 倍,数出来的统计量行越多越准)。 这条规矩直接救回两笔提交,其中一笔是全场最大的 +0.0628。

目录结构

jittor-wuyi-dynamic-rec/
├── README.md                 本文件
├── LICENSE                   MIT
├── requirements.txt          统一依赖(两榜通用)
├── docs/
│   ├── METHOD.md             方法详解:四层管线、特征族清单、两个数据集家族的差异
│   └── RESULTS.md            成绩与上分路径:逐版本线上实测、增量归因、失败清单
├── code_A/                   ★ A 榜(dataset1 / dataset2)完整方案,一键复现
│   ├── README.md
│   ├── run_reproduce.sh      从原始数据到 result_final.zip 的全链脚本(幂等、可续跑)
│   └── *.py                  18 个源码文件
├── code_B/                   ★ B 榜(dataset3 / dataset4)完整方案,一键复现
│   ├── README.md             两分钟上手 + 文档导航
│   ├── 说明文档.md            B 榜方法说明(提交正文)
│   ├── A_TO_B_CHANGES.md     A 榜算法 → B 榜的逐项改动(含每项线上增量与判据)
│   ├── REPRODUCE.md          详细复现:资源预算、分步耗时、常见故障与对策
│   ├── MANIFEST.md           文件清单
│   ├── reproduce_d3.sh / reproduce_d4.sh / pack_submission.py / env_check.py
│   └── code/                 16 个 py + 1 个分片流水 sh
└── ckpt/                     B 榜最终精排器权重(5 折,可直接推理)

安装

运行环境

本项目在下列环境开发与验证(即比赛实际使用的 AutoDL 容器规格):

  • Ubuntu 22.04 / CUDA 12.4
  • Python 3.10
  • Jittor 1.3.10
  • 单卡 2080Ti(11GB 显存)40GB 内存、12 vCPU、≥60GB 可用磁盘

⚠️ numpy 必须是 1.x。 numpy 2.x 会让 jt.array 读出垃圾数据(表现为 IndexError / OOM, 是根因级的坑)。requirements.txt 已锁 numpy==1.24.4

安装依赖

conda create -n jt python=3.10 -y && conda activate jt
pip install -r requirements.txt
python -m jittor_utils.install_cuda      # 仅当报 "cudnn is not loaded" 时需要,约 5 分钟

安装完成后先跑环境自检,必须全部 PASS 再往下(自检包含 jt.scatter(reduce='add') 算子与 scipy 稀疏矩阵的可用性验证,LightGCN 依赖前者):

cd code_B && python env_check.py        # A 榜同名脚本在 code_A/ 下

预训练模型

ckpt/ 目录内含 B 榜最终精排器的 5 折权重与归一化参数(约 1.8 MB,已随仓库分发):

数据集 文件 结构 OOF 对应线上
dataset3 ckpt/dataset3/ranker_cv_b15t_cv5* MLP,时间划分 5 折 0.9536 0.94083 ← 提交用
dataset3 ckpt/dataset3/ranker_cv_b15_cv5* MLP,随机划分 5 折 0.9545 对照,未提交
dataset4 ckpt/dataset4/ranker_cv_b14_cv5* 候选间 self-attention 0.5338 0.52963

d3 的时间划分 OOF(0.9536)与随机划分(0.9545)不可比 —— 折的难度不同,只能看线上。

各单模型权重(SASRec / BPR-MF / LightGCN / item2vec,val + full 两套,合计约 7GB)与特征张量 (d4 单个 val 就 7.7GB)不随仓库分发,由复现脚本现算;如需在训练完的机器上收集, 用 code_B/collect_ckpt.sh

数据预处理

官方数据不含任何节点属性,只有两个 csv(train.csv(src, dst, time) 交互边, test.csv 为查询 + 100 个候选),无需额外预处理,解压后放到仓库根目录即可 (data_A/data_B/ 已写进 .gitignore,不会被提交):

jittor-wuyi-dynamic-rec/
├── data_A/
│   ├── dataset1/{train.csv, test.csv}
│   └── dataset2/{train.csv, test.csv}
├── data_B/
│   ├── dataset3/{train.csv, test.csv}
│   └── dataset4/{train.csv, test.csv}
├── code_A/  code_B/  ckpt/  docs/ ...
  • A 榜code_A 的数据路径默认就是 <仓库根>/data_A,按上面摆好即可,无需任何设置。

  • B 榜reproduce_*.sh 会先 cd code_B/code,相对路径会失效,请用绝对路径

    export DATA_DIR="$(pwd)/data_B"      # 在仓库根目录执行

数据也可以放在仓库外,此时两榜都用 export DATA_DIR=/abs/path/to/data_{A,B} 覆盖。

特征工程(图统计表、候选矩阵统计表、各模型打分)全部由复现脚本在链路内完成,无独立预处理步骤。

训练

训练与推理写在同一条幂等流水线里(**每一步先查产物,存在即 [skip]**,可随时中断续跑)。

A 榜(dataset1 + dataset2)

cd code_A
bash run_reproduce.sh            # 单卡约 8-10 小时,产出 out/result_final.zip

脚本分 4 个阶段:dataset1 模型训练 → dataset1 特征链 + 精排 → dataset2 模型训练 → dataset2 特征链 + 精排 → 打包。关键自检点(±0.002 属正常随机波动):

  • dataset1 ranker OOF ≈ 0.9417_v29_cv5, MLP)
  • dataset2 ranker OOF ≈ 0.6109_v30_cv5, attention)

B 榜(dataset3 + dataset4)

cd code_B
export DATA_DIR=/abs/path/to/data_B      # 见上一节;放在仓库根目录则是 $(cd .. && pwd)/data_B
bash reproduce_d3.sh             # dataset3 全链,5 段 20 步,约 6-8 小时
bash reproduce_d4.sh             # dataset4 全链,6 段 13 步 + 九段分片,约 9 小时

两个脚本不要并行跑 —— 都要抢 GPU,且 d4 的内存峰值接近 40GB 容器上限。

关键自检点:d3 时间划分 OOF ≈ 0.9537、d4 OOF ≈ 0.5338。分步耗时、显存/内存峰值、 以及 6 类常见故障的对策见 code_B/REPRODUCE.md

若已有 ckpt/,铺回去可跳过精排器重训:

cp -r ckpt/dataset3/* code_B/code/out/dataset3/
cp -r ckpt/dataset4/* code_B/code/out/dataset4/

推理

复现脚本的最后一段即是推理,也可以在已有特征与精排器的前提下单独跑:

cd code_B/code
python predict.py --dataset dataset3 --scorer cv --cv_tag _b15t_cv5 --out_tag _b15t

dataset4 的 test 有 232 万行、特征张量 32GB,单文件装不下,必须走九段分片流水 (逐片 augment → predict → 按序拼接,用完即删分片特征):

bash code_B/code/run_d4_shards.sh

打包与格式校验:

cd code_B && python pack_submission.py      # → out/result_B_final.zip

pack_submission.py 带 4 道校验:行列数、定宽字节不变量(逐字节证明每行确实是 100 列)、 占位分数防呆、无 zip64。最后一条是踩出来的:Python 的 zipfile 对 >2GB 的成员强制启用 zip64,而官方检查器不认,会直接判「提交格式错误」。

结果格式

每个数据集一个 csv,无表头,行数与行序与 test.csv 一致,每行 100 个数 = 该行 100 个候选的 分数,行内 min-max 归一到 [0,1],8 位小数(评测只看行内排序)。

数据集 结果文件 行数
dataset3 dataset3.csv 157,670
dataset4 dataset4.csv 2,322,538

合规声明

  1. 不使用测试集标签。 第 ③ 类「候选矩阵自指统计」特征统计的是官方发放的 test 候选矩阵 这一输入本身(某物品被列为候选的次数、同一用户的多行是否共享候选、候选之间的互指结构 等),属于对输入做聚合统计,全程不接触也不推断任何真值标签;验证集侧用同构方式构造 (负例从真实候选池采样),保证 val / test 口径一致。
  2. 提供不使用任何测试集统计的对照版本。 code_A 内置 v9c / d1c 两个「干净」列集 (--rank_profile v9c|d1c),跳过 --tfreq / --tsib / --trecip / --tsim / --tem 等 augment 步骤即可得到完全只用训练数据的管线;历史上完全不使用测试集统计的最高分版本线上为 1.4557(dataset1 0.85914 + dataset2 0.59658)。
  3. 不使用任何外部数据,只用官方发放的 data_A / data_B
  4. 不与其他队伍共享代码、模型权重或预测结果。
  5. 核心模型(SASRec / BPR-MF / LightGCN / listwise 精排器)均为计图(Jittor)实现; numpy / scipy 仅用于图统计特征与稀疏矩阵运算。

一处术语说明:源码注释与部分开发文档里,第 ③ 类特征被用内部简称叫作「泄漏列 / 泄漏族」 (如 augment_features.py--tsib / --trecip 注释、run_reproduce.sh 的链路注释)。 这是我们开发期的口头简称,指的是「利用了 transductive 设定」,不是指使用了标签: 这些特征只对官方发放的 test 候选矩阵这一输入做聚合统计,从不接触也不推断任何真值标签。 正式名称是「候选矩阵自指统计」,见 docs/METHOD.md §③

致谢

  • 感谢清华大学计图(Jittor)团队与赛事主办方提供的框架、数据与算力支持。
  • 方案中的模型实现参考了以下工作,代码均为在计图上的重新实现: SASRecBPRLightGCNitem2vecDCN
  • 官方 baseline 使用的 JittorGeometric 为本方案的 起点参考。
关于
1.9 MB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802047560号