From b6dbdad80f43c309aeabb9a7e9b45231609f6c04 Mon Sep 17 00:00:00 2001 From: jeny Date: Tue, 16 Nov 2021 15:04:52 +0800 Subject: [PATCH] Initial commit --- .github/workflows/django.yml | 27 ++ .gitignore | 18 + README.md | 411 +++++++++++++++++- academic_graph/__init__.py | 0 academic_graph/asgi.py | 16 + academic_graph/settings/__init__.py | 0 academic_graph/settings/common.py | 136 ++++++ academic_graph/settings/dev.py | 6 + academic_graph/settings/prod.py | 6 + academic_graph/settings/test.py | 11 + academic_graph/urls.py | 25 ++ academic_graph/wsgi.py | 16 + gnnrec/__init__.py | 0 gnnrec/config.py | 10 + gnnrec/hge/__init__.py | 0 gnnrec/hge/cs/__init__.py | 0 gnnrec/hge/cs/model.py | 113 +++++ gnnrec/hge/cs/train.py | 101 +++++ gnnrec/hge/data/__init__.py | 1 + gnnrec/hge/data/heco.py | 204 +++++++++ gnnrec/hge/heco/__init__.py | 0 gnnrec/hge/heco/model.py | 269 ++++++++++++ gnnrec/hge/heco/sampler.py | 28 ++ gnnrec/hge/heco/train.py | 117 +++++ gnnrec/hge/hgconv/__init__.py | 0 gnnrec/hge/hgconv/model.py | 291 +++++++++++++ gnnrec/hge/hgconv/train.py | 83 ++++ gnnrec/hge/hgconv/train_full.py | 59 +++ gnnrec/hge/hgt/__init__.py | 0 gnnrec/hge/hgt/model.py | 180 ++++++++ gnnrec/hge/hgt/train.py | 88 ++++ gnnrec/hge/hgt/train_full.py | 66 +++ gnnrec/hge/metapath2vec/__init__.py | 0 gnnrec/hge/metapath2vec/random_walk.py | 56 +++ gnnrec/hge/metapath2vec/train_word2vec.py | 27 ++ gnnrec/hge/readme.md | 129 ++++++ gnnrec/hge/result/__init__.py | 0 gnnrec/hge/result/ablation_study.csv | 0 gnnrec/hge/result/node_classification.csv | 9 + gnnrec/hge/result/param_analysis.csv | 6 + gnnrec/hge/result/plot.py | 34 ++ gnnrec/hge/rgcn/__init__.py | 0 gnnrec/hge/rgcn/model.py | 95 ++++ gnnrec/hge/rgcn/train.py | 60 +++ gnnrec/hge/rhco/__init__.py | 0 gnnrec/hge/rhco/build_pos_graph.py | 138 ++++++ gnnrec/hge/rhco/build_pos_graph_full.py | 107 +++++ gnnrec/hge/rhco/model.py | 126 ++++++ gnnrec/hge/rhco/smooth.py | 75 ++++ gnnrec/hge/rhco/train.py | 127 ++++++ gnnrec/hge/rhco/train_full.py | 100 +++++ gnnrec/hge/rhgnn/__init__.py | 0 gnnrec/hge/rhgnn/model.py | 370 ++++++++++++++++ gnnrec/hge/rhgnn/train.py | 85 ++++ gnnrec/hge/rhgnn/train_full.py | 63 +++ gnnrec/hge/utils/__init__.py | 28 ++ gnnrec/hge/utils/data.py | 138 ++++++ gnnrec/hge/utils/metrics.py | 87 ++++ gnnrec/kgrec/__init__.py | 0 gnnrec/kgrec/data/__init__.py | 3 + gnnrec/kgrec/data/config.py | 38 ++ gnnrec/kgrec/data/contrast.py | 30 ++ gnnrec/kgrec/data/oagcs.py | 153 +++++++ gnnrec/kgrec/data/preprocess/__init__.py | 0 .../kgrec/data/preprocess/ai2000_crawler.py | 72 +++ gnnrec/kgrec/data/preprocess/analyze.py | 41 ++ .../data/preprocess/build_author_rank.py | 202 +++++++++ gnnrec/kgrec/data/preprocess/extract_cs.py | 129 ++++++ gnnrec/kgrec/data/preprocess/fine_tune.py | 131 ++++++ gnnrec/kgrec/data/preprocess/utils.py | 23 + gnnrec/kgrec/data/readme.md | 157 +++++++ gnnrec/kgrec/data/venue.py | 67 +++ gnnrec/kgrec/random_walk.py | 28 ++ gnnrec/kgrec/rank.py | 32 ++ gnnrec/kgrec/readme.md | 120 +++++ gnnrec/kgrec/recall.py | 53 +++ gnnrec/kgrec/scibert.py | 61 +++ gnnrec/kgrec/train.py | 133 ++++++ gnnrec/kgrec/utils/__init__.py | 2 + gnnrec/kgrec/utils/data.py | 64 +++ gnnrec/kgrec/utils/metrics.py | 10 + img/GARec.png | Bin 0 -> 28008 bytes img/RHCO.png | Bin 0 -> 43928 bytes img/学者详情.png | Bin 0 -> 123951 bytes img/搜索学者.png | Bin 0 -> 50098 bytes img/搜索论文.png | Bin 0 -> 125838 bytes img/论文详情.png | Bin 0 -> 123517 bytes manage.py | 22 + plan.md | 244 +++++++++++ rank/__init__.py | 0 rank/admin.py | 18 + rank/apps.py | 15 + rank/management/__init__.py | 0 rank/management/commands/__init__.py | 0 rank/management/commands/loadoagcs.py | 95 ++++ rank/migrations/0001_initial.py | 92 ++++ rank/migrations/0002_alter_writes_ordering.py | 17 + rank/migrations/__init__.py | 0 rank/models.py | 63 +++ rank/templates/rank/_author_list.html | 13 + rank/templates/rank/_paper_list.html | 20 + rank/templates/rank/author_detail.html | 13 + rank/templates/rank/author_rank.html | 12 + rank/templates/rank/base.html | 54 +++ rank/templates/rank/index.html | 10 + rank/templates/rank/login.html | 24 + rank/templates/rank/paper_detail.html | 24 + rank/templates/rank/register.html | 38 ++ rank/templates/rank/search_author.html | 15 + rank/templates/rank/search_paper.html | 12 + rank/tests.py | 217 +++++++++ rank/urls.py | 17 + rank/views.py | 143 ++++++ requirements.txt | 15 + requirements_cuda.txt | 15 + 115 files changed, 6897 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/django.yml create mode 100644 .gitignore create mode 100644 academic_graph/__init__.py create mode 100644 academic_graph/asgi.py create mode 100644 academic_graph/settings/__init__.py create mode 100644 academic_graph/settings/common.py create mode 100644 academic_graph/settings/dev.py create mode 100644 academic_graph/settings/prod.py create mode 100644 academic_graph/settings/test.py create mode 100644 academic_graph/urls.py create mode 100644 academic_graph/wsgi.py create mode 100644 gnnrec/__init__.py create mode 100644 gnnrec/config.py create mode 100644 gnnrec/hge/__init__.py create mode 100644 gnnrec/hge/cs/__init__.py create mode 100644 gnnrec/hge/cs/model.py create mode 100644 gnnrec/hge/cs/train.py create mode 100644 gnnrec/hge/data/__init__.py create mode 100644 gnnrec/hge/data/heco.py create mode 100644 gnnrec/hge/heco/__init__.py create mode 100644 gnnrec/hge/heco/model.py create mode 100644 gnnrec/hge/heco/sampler.py create mode 100644 gnnrec/hge/heco/train.py create mode 100644 gnnrec/hge/hgconv/__init__.py create mode 100644 gnnrec/hge/hgconv/model.py create mode 100644 gnnrec/hge/hgconv/train.py create mode 100644 gnnrec/hge/hgconv/train_full.py create mode 100644 gnnrec/hge/hgt/__init__.py create mode 100644 gnnrec/hge/hgt/model.py create mode 100644 gnnrec/hge/hgt/train.py create mode 100644 gnnrec/hge/hgt/train_full.py create mode 100644 gnnrec/hge/metapath2vec/__init__.py create mode 100644 gnnrec/hge/metapath2vec/random_walk.py create mode 100644 gnnrec/hge/metapath2vec/train_word2vec.py create mode 100644 gnnrec/hge/readme.md create mode 100644 gnnrec/hge/result/__init__.py create mode 100644 gnnrec/hge/result/ablation_study.csv create mode 100644 gnnrec/hge/result/node_classification.csv create mode 100644 gnnrec/hge/result/param_analysis.csv create mode 100644 gnnrec/hge/result/plot.py create mode 100644 gnnrec/hge/rgcn/__init__.py create mode 100644 gnnrec/hge/rgcn/model.py create mode 100644 gnnrec/hge/rgcn/train.py create mode 100644 gnnrec/hge/rhco/__init__.py create mode 100644 gnnrec/hge/rhco/build_pos_graph.py create mode 100644 gnnrec/hge/rhco/build_pos_graph_full.py create mode 100644 gnnrec/hge/rhco/model.py create mode 100644 gnnrec/hge/rhco/smooth.py create mode 100644 gnnrec/hge/rhco/train.py create mode 100644 gnnrec/hge/rhco/train_full.py create mode 100644 gnnrec/hge/rhgnn/__init__.py create mode 100644 gnnrec/hge/rhgnn/model.py create mode 100644 gnnrec/hge/rhgnn/train.py create mode 100644 gnnrec/hge/rhgnn/train_full.py create mode 100644 gnnrec/hge/utils/__init__.py create mode 100644 gnnrec/hge/utils/data.py create mode 100644 gnnrec/hge/utils/metrics.py create mode 100644 gnnrec/kgrec/__init__.py create mode 100644 gnnrec/kgrec/data/__init__.py create mode 100644 gnnrec/kgrec/data/config.py create mode 100644 gnnrec/kgrec/data/contrast.py create mode 100644 gnnrec/kgrec/data/oagcs.py create mode 100644 gnnrec/kgrec/data/preprocess/__init__.py create mode 100644 gnnrec/kgrec/data/preprocess/ai2000_crawler.py create mode 100644 gnnrec/kgrec/data/preprocess/analyze.py create mode 100644 gnnrec/kgrec/data/preprocess/build_author_rank.py create mode 100644 gnnrec/kgrec/data/preprocess/extract_cs.py create mode 100644 gnnrec/kgrec/data/preprocess/fine_tune.py create mode 100644 gnnrec/kgrec/data/preprocess/utils.py create mode 100644 gnnrec/kgrec/data/readme.md create mode 100644 gnnrec/kgrec/data/venue.py create mode 100644 gnnrec/kgrec/random_walk.py create mode 100644 gnnrec/kgrec/rank.py create mode 100644 gnnrec/kgrec/readme.md create mode 100644 gnnrec/kgrec/recall.py create mode 100644 gnnrec/kgrec/scibert.py create mode 100644 gnnrec/kgrec/train.py create mode 100644 gnnrec/kgrec/utils/__init__.py create mode 100644 gnnrec/kgrec/utils/data.py create mode 100644 gnnrec/kgrec/utils/metrics.py create mode 100644 img/GARec.png create mode 100644 img/RHCO.png create mode 100644 img/学者详情.png create mode 100644 img/搜索学者.png create mode 100644 img/搜索论文.png create mode 100644 img/论文详情.png create mode 100644 manage.py create mode 100644 plan.md create mode 100644 rank/__init__.py create mode 100644 rank/admin.py create mode 100644 rank/apps.py create mode 100644 rank/management/__init__.py create mode 100644 rank/management/commands/__init__.py create mode 100644 rank/management/commands/loadoagcs.py create mode 100644 rank/migrations/0001_initial.py create mode 100644 rank/migrations/0002_alter_writes_ordering.py create mode 100644 rank/migrations/__init__.py create mode 100644 rank/models.py create mode 100644 rank/templates/rank/_author_list.html create mode 100644 rank/templates/rank/_paper_list.html create mode 100644 rank/templates/rank/author_detail.html create mode 100644 rank/templates/rank/author_rank.html create mode 100644 rank/templates/rank/base.html create mode 100644 rank/templates/rank/index.html create mode 100644 rank/templates/rank/login.html create mode 100644 rank/templates/rank/paper_detail.html create mode 100644 rank/templates/rank/register.html create mode 100644 rank/templates/rank/search_author.html create mode 100644 rank/templates/rank/search_paper.html create mode 100644 rank/tests.py create mode 100644 rank/urls.py create mode 100644 rank/views.py create mode 100644 requirements.txt create mode 100644 requirements_cuda.txt diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml new file mode 100644 index 0000000..2ccab4a --- /dev/null +++ b/.github/workflows/django.yml @@ -0,0 +1,27 @@ +name: Django CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + env: + DJANGO_SETTINGS_MODULE: academic_graph.settings.test + SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }} + run: | + python manage.py test --noinput diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b29805 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# PyCharm +/.idea/ + +# Python +__pycache__/ + +# 数据集 +/data/ + +# 保存的模型 +/model/ + +# 日志输出目录 +/output/ + +# Django +/static/ +/.mylogin.cnf diff --git a/README.md b/README.md index 96e0a3a..cbed75c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,410 @@ -# GNNRecom +# 基于图神经网络的异构图表示学习和推荐算法研究 -毕业设计:基于图神经网络的异构图表示学习和推荐算法研究。包含基于对比学习的关系感知异构图神经网络(Relation-aware Heterogeneous Graph Neural Network with Contrastive Learning, RHCO)、基于图神经网络的学术推荐算法(Graph Neural Network based Academic Recommendation Algorithm, GARec),详细设计见md文件。 \ No newline at end of file +## 目录结构 + +``` +GNN-Recommendation/ + gnnrec/ 算法模块顶级包 + hge/ 异构图表示学习模块 + kgrec/ 基于图神经网络的推荐算法模块 + data/ 数据集目录(已添加.gitignore) + model/ 模型保存目录(已添加.gitignore) + img/ 图片目录 + academic_graph/ Django项目模块 + rank/ Django应用 + manage.py Django管理脚本 +``` + +## 安装依赖 + +Python 3.7 + +### CUDA 11.0 + +```shell +pip install -r requirements_cuda.txt +``` + +### CPU + +```shell +pip install -r requirements.txt +``` + +## 异构图表示学习(附录) + +基于对比学习的关系感知异构图神经网络(Relation-aware Heterogeneous Graph Neural Network with Contrastive Learning, RHCO) + +![](https://www.writebug.com/myres/static/uploads/2021/11/16/910786858930a83f119df93c38e8bb93.writebug) + +### 实验 + +见 [readme](gnnrec/hge/readme.md) + +## 基于图神经网络的推荐算法(附录) + +基于图神经网络的学术推荐算法(Graph Neural Network based Academic Recommendation Algorithm, GARec) + +![](https://www.writebug.com/myres/static/uploads/2021/11/16/bd66248993199f7f5260a5a7f6ad01fd.writebug) + +### 实验 + +见 [readme](gnnrec/kgrec/readme.md) + +## Django 配置 + +### MySQL 数据库配置 + +1. 创建数据库及用户 + +```sql +CREATE DATABASE academic_graph CHARACTER SET utf8mb4; +CREATE USER 'academic_graph'@'%' IDENTIFIED BY 'password'; +GRANT ALL ON academic_graph.* TO 'academic_graph'@'%'; +``` + +2. 在根目录下创建文件.mylogin.cnf + +```ini +[client] +host = x.x.x.x +port = 3306 +user = username +password = password +database = database +default-character-set = utf8mb4 +``` + +3. 创建数据库表 + +```shell +python manage.py makemigrations --settings=academic_graph.settings.prod rank +python manage.py migrate --settings=academic_graph.settings.prod +``` + +4. 导入 oag-cs 数据集 + +```shell +python manage.py loadoagcs --settings=academic_graph.settings.prod +``` + +注:由于导入一次时间很长(约 9 小时),为了避免中途发生错误,可以先用 data/oag/test 中的测试数据调试一下 + +### 拷贝静态文件 + +```shell +python manage.py collectstatic --settings=academic_graph.settings.prod +``` + +### 启动 Web 服务器 + +```shell +export SECRET_KEY=xxx +python manage.py runserver --settings=academic_graph.settings.prod 0.0.0.0:8000 +``` + +### 系统截图 + +搜索论文 +![](https://www.writebug.com/myres/static/uploads/2021/11/16/74058b5c78ebd745cc80eeec40c405d1.writebug) + +论文详情 +![](https://www.writebug.com/myres/static/uploads/2021/11/16/881f8190dce79bd56df8bd7ecf6e17a4.writebug) + +搜索学者 +![](https://www.writebug.com/myres/static/uploads/2021/11/16/021cee065306e1b24728759825ff0e17.writebug) + +学者详情 +![](https://www.writebug.com/myres/static/uploads/2021/11/16/1c427078ba1c717bcd6fc935a59fc957.writebug) + +## 附录 + +### 基于图神经网络的推荐算法 + +#### 数据集 + +oag-cs - 使用 OAG 微软学术数据构造的计算机领域的学术网络(见 [readme](data/readme.md)) + +#### 预训练顶点嵌入 + +使用 metapath2vec(随机游走 +word2vec)预训练顶点嵌入,作为 GNN 模型的顶点输入特征 + +1. 随机游走 + +```shell +python -m gnnrec.kgrec.random_walk model/word2vec/oag_cs_corpus.txt +``` + +2. 训练词向量 + +```shell +python -m gnnrec.hge.metapath2vec.train_word2vec --size=128 --workers=8 model/word2vec/oag_cs_corpus.txt model/word2vec/oag_cs.model +``` + +#### 召回 + +使用微调后的 SciBERT 模型(见 [readme](data/readme.md) 第 2 步)将查询词编码为向量,与预先计算好的论文标题向量计算余弦相似度,取 top k + +```shell +python -m gnnrec.kgrec.recall +``` + +召回结果示例: + +graph neural network + +``` +0.9629 Aggregation Graph Neural Networks +0.9579 Neural Graph Learning: Training Neural Networks Using Graphs +0.9556 Heterogeneous Graph Neural Network +0.9552 Neural Graph Machines: Learning Neural Networks Using Graphs +0.9490 On the choice of graph neural network architectures +0.9474 Measuring and Improving the Use of Graph Information in Graph Neural Networks +0.9362 Challenging the generalization capabilities of Graph Neural Networks for network modeling +0.9295 Strategies for Pre-training Graph Neural Networks +0.9142 Supervised Neural Network Models for Processing Graphs +0.9112 Geometrically Principled Connections in Graph Neural Networks +``` + +recommendation algorithm based on knowledge graph + +``` +0.9172 Research on Video Recommendation Algorithm Based on Knowledge Reasoning of Knowledge Graph +0.8972 An Improved Recommendation Algorithm in Knowledge Network +0.8558 A personalized recommendation algorithm based on interest graph +0.8431 An Improved Recommendation Algorithm Based on Graph Model +0.8334 The Research of Recommendation Algorithm based on Complete Tripartite Graph Model +0.8220 Recommendation Algorithm based on Link Prediction and Domain Knowledge in Retail Transactions +0.8167 Recommendation Algorithm Based on Graph-Model Considering User Background Information +0.8034 A Tripartite Graph Recommendation Algorithm Based on Item Information and User Preference +0.7774 Improvement of TF-IDF Algorithm Based on Knowledge Graph +0.7770 Graph Searching Algorithms for Semantic-Social Recommendation +``` + +scholar disambiguation + +``` +0.9690 Scholar search-oriented author disambiguation +0.9040 Author name disambiguation in scientific collaboration and mobility cases +0.8901 Exploring author name disambiguation on PubMed-scale +0.8852 Author Name Disambiguation in Heterogeneous Academic Networks +0.8797 KDD Cup 2013: author disambiguation +0.8796 A survey of author name disambiguation techniques: 2010–2016 +0.8721 Who is Who: Name Disambiguation in Large-Scale Scientific Literature +0.8660 Use of ResearchGate and Google CSE for author name disambiguation +0.8643 Automatic Methods for Disambiguating Author Names in Bibliographic Data Repositories +0.8641 A brief survey of automatic methods for author name disambiguation +``` + +### 精排 + +#### 构造 ground truth + +(1)验证集 + +从 AMiner 发布的 [AI 2000 人工智能全球最具影响力学者榜单](https://www.aminer.cn/ai2000) 抓取人工智能 20 个子领域的 top 100 学者 + +```shell +pip install scrapy>=2.3.0 +cd gnnrec/kgrec/data/preprocess +scrapy runspider ai2000_crawler.py -a save_path=/home/zzy/GNN-Recommendation/data/rank/ai2000.json +``` + +与 oag-cs 数据集的学者匹配,并人工确认一些排名较高但未匹配上的学者,作为学者排名 ground truth 验证集 + +```shell +export DJANGO_SETTINGS_MODULE=academic_graph.settings.common +export SECRET_KEY=xxx +python -m gnnrec.kgrec.data.preprocess.build_author_rank build-val +``` + +(2)训练集 + +参考 AI 2000 的计算公式,根据某个领域的论文引用数加权求和构造学者排名,作为 ground truth 训练集 + +计算公式: +![](https://www.writebug.com/myres/static/uploads/2021/11/16/cd74b5d12a50a99f664863ae6ccb94c9.writebug) +即:假设一篇论文有 n 个作者,第 k 作者的权重为 1/k,最后一个视为通讯作者,权重为 1/2,归一化之后计算论文引用数的加权求和 + +```shell +python -m gnnrec.kgrec.data.preprocess.build_author_rank build-train +``` + +(3)评估 ground truth 训练集的质量 + +```shell +python -m gnnrec.kgrec.data.preprocess.build_author_rank eval +``` + +``` +nDGC@100=0.2420 Precision@100=0.1859 Recall@100=0.2016 +nDGC@50=0.2308 Precision@50=0.2494 Recall@50=0.1351 +nDGC@20=0.2492 Precision@20=0.3118 Recall@20=0.0678 +nDGC@10=0.2743 Precision@10=0.3471 Recall@10=0.0376 +nDGC@5=0.3165 Precision@5=0.3765 Recall@5=0.0203 +``` + +(4)采样三元组 + +从学者排名训练集中采样三元组(t, ap, an),表示对于领域 t,学者 ap 的排名在 an 之前 + +```shell +python -m gnnrec.kgrec.data.preprocess.build_author_rank sample +``` + +#### 训练 GNN 模型 + +```shell +python -m gnnrec.kgrec.train model/word2vec/oag-cs.model model/garec_gnn.pt data/rank/author_embed.pt +``` + +## 异构图表示学习 + +### 数据集 + +* [ACM](https://github.com/liun-online/HeCo/tree/main/data/acm) - ACM 学术网络数据集 +* [DBLP](https://github.com/liun-online/HeCo/tree/main/data/dblp) - DBLP 学术网络数据集 +* [ogbn-mag](https://ogb.stanford.edu/docs/nodeprop/#ogbn-mag) - OGB 提供的微软学术数据集 +* [oag-venue](../kgrec/data/venue.py) - oag-cs 期刊分类数据集 + +| 数据集 | 顶点数 | 边数 | 目标顶点 | 类别数 | +| --------- | ------- | -------- | -------- | ------ | +| ACM | 11246 | 34852 | paper | 3 | +| DBLP | 26128 | 239566 | author | 4 | +| ogbn-mag | 1939743 | 21111007 | paper | 349 | +| oag-venue | 4235169 | 34520417 | paper | 360 | + +### Baselines + +* [R-GCN](https://arxiv.org/pdf/1703.06103) +* [HGT](https://arxiv.org/pdf/2003.01332) +* [HGConv](https://arxiv.org/pdf/2012.14722) +* [R-HGNN](https://arxiv.org/pdf/2105.11122) +* [C&S](https://arxiv.org/pdf/2010.13993) +* [HeCo](https://arxiv.org/pdf/2105.09111) + +#### R-GCN (full batch) + +```shell +python -m gnnrec.hge.rgcn.train --dataset=acm --epochs=10 +python -m gnnrec.hge.rgcn.train --dataset=dblp --epochs=10 +python -m gnnrec.hge.rgcn.train --dataset=ogbn-mag --num-hidden=48 +python -m gnnrec.hge.rgcn.train --dataset=oag-venue --num-hidden=48 --epochs=30 +``` + +(使用 minibatch 训练准确率就是只有 20% 多,不知道为什么) + +#### 预训练顶点嵌入 + +使用 metapath2vec(随机游走 +word2vec)预训练顶点嵌入,作为 GNN 模型的顶点输入特征 + +```shell +python -m gnnrec.hge.metapath2vec.random_walk model/word2vec/ogbn-mag_corpus.txt +python -m gnnrec.hge.metapath2vec.train_word2vec --size=128 --workers=8 model/word2vec/ogbn-mag_corpus.txt model/word2vec/ogbn-mag.model +``` + +#### HGT + +```shell +python -m gnnrec.hge.hgt.train_full --dataset=acm +python -m gnnrec.hge.hgt.train_full --dataset=dblp +python -m gnnrec.hge.hgt.train --dataset=ogbn-mag --node-embed-path=model/word2vec/ogbn-mag.model --epochs=40 +python -m gnnrec.hge.hgt.train --dataset=oag-venue --node-embed-path=model/word2vec/oag-cs.model --epochs=40 +``` + +#### HGConv + +```shell +python -m gnnrec.hge.hgconv.train_full --dataset=acm --epochs=5 +python -m gnnrec.hge.hgconv.train_full --dataset=dblp --epochs=20 +python -m gnnrec.hge.hgconv.train --dataset=ogbn-mag --node-embed-path=model/word2vec/ogbn-mag.model +python -m gnnrec.hge.hgconv.train --dataset=oag-venue --node-embed-path=model/word2vec/oag-cs.model +``` + +#### R-HGNN + +```shell +python -m gnnrec.hge.rhgnn.train_full --dataset=acm --num-layers=1 --epochs=15 +python -m gnnrec.hge.rhgnn.train_full --dataset=dblp --epochs=20 +python -m gnnrec.hge.rhgnn.train --dataset=ogbn-mag model/word2vec/ogbn-mag.model +python -m gnnrec.hge.rhgnn.train --dataset=oag-venue --epochs=50 model/word2vec/oag-cs.model +``` + +#### C&S + +```shell +python -m gnnrec.hge.cs.train --dataset=acm --epochs=5 +python -m gnnrec.hge.cs.train --dataset=dblp --epochs=5 +python -m gnnrec.hge.cs.train --dataset=ogbn-mag --prop-graph=data/graph/pos_graph_ogbn-mag_t5.bin +python -m gnnrec.hge.cs.train --dataset=oag-venue --prop-graph=data/graph/pos_graph_oag-venue_t5.bin +``` + +#### HeCo + +```shell +python -m gnnrec.hge.heco.train --dataset=ogbn-mag model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5.bin +python -m gnnrec.hge.heco.train --dataset=oag-venue model/word2vec/oag-cs.model data/graph/pos_graph_oag-venue_t5.bin +``` + +(ACM 和 DBLP 的数据来自 [https://github.com/ZZy979/pytorch-tutorial/tree/master/gnn/heco](https://github.com/ZZy979/pytorch-tutorial/tree/master/gnn/heco) ,准确率和 Micro-F1 相等) + +#### RHCO + +基于对比学习的关系感知异构图神经网络(Relation-aware Heterogeneous Graph Neural Network with Contrastive Learning, RHCO) + +在 HeCo 的基础上改进: + +* 网络结构编码器中的注意力向量改为关系的表示(类似于 R-HGNN) +* 正样本选择方式由元路径条数改为预训练的 HGT 计算的注意力权重、训练集使用真实标签 +* 元路径视图编码器改为正样本图编码器,适配 mini-batch 训练 +* Loss 增加分类损失,训练方式由无监督改为半监督 +* 在最后增加 C&S 后处理步骤 + +ACM + +```shell +python -m gnnrec.hge.hgt.train_full --dataset=acm --save-path=model/hgt/hgt_acm.pt +python -m gnnrec.hge.rhco.build_pos_graph_full --dataset=acm --num-samples=5 --use-label model/hgt/hgt_acm.pt data/graph/pos_graph_acm_t5l.bin +python -m gnnrec.hge.rhco.train_full --dataset=acm data/graph/pos_graph_acm_t5l.bin +``` + +DBLP + +```shell +python -m gnnrec.hge.hgt.train_full --dataset=dblp --save-path=model/hgt/hgt_dblp.pt +python -m gnnrec.hge.rhco.build_pos_graph_full --dataset=dblp --num-samples=5 --use-label model/hgt/hgt_dblp.pt data/graph/pos_graph_dblp_t5l.bin +python -m gnnrec.hge.rhco.train_full --dataset=dblp --use-data-pos data/graph/pos_graph_dblp_t5l.bin +``` + +ogbn-mag(第 3 步如果中断可使用--load-path 参数继续训练) + +```shell +python -m gnnrec.hge.hgt.train --dataset=ogbn-mag --node-embed-path=model/word2vec/ogbn-mag.model --epochs=40 --save-path=model/hgt/hgt_ogbn-mag.pt +python -m gnnrec.hge.rhco.build_pos_graph --dataset=ogbn-mag --num-samples=5 --use-label model/word2vec/ogbn-mag.model model/hgt/hgt_ogbn-mag.pt data/graph/pos_graph_ogbn-mag_t5l.bin +python -m gnnrec.hge.rhco.train --dataset=ogbn-mag --num-hidden=64 --contrast-weight=0.9 model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5l.bin model/rhco_ogbn-mag_d64_a0.9_t5l.pt +python -m gnnrec.hge.rhco.smooth --dataset=ogbn-mag model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5l.bin model/rhco_ogbn-mag_d64_a0.9_t5l.pt +``` + +oag-venue + +```shell +python -m gnnrec.hge.hgt.train --dataset=oag-venue --node-embed-path=model/word2vec/oag-cs.model --epochs=40 --save-path=model/hgt/hgt_oag-venue.pt +python -m gnnrec.hge.rhco.build_pos_graph --dataset=oag-venue --num-samples=5 --use-label model/word2vec/oag-cs.model model/hgt/hgt_oag-venue.pt data/graph/pos_graph_oag-venue_t5l.bin +python -m gnnrec.hge.rhco.train --dataset=oag-venue --num-hidden=64 --contrast-weight=0.9 model/word2vec/oag-cs.model data/graph/pos_graph_oag-venue_t5l.bin model/rhco_oag-venue.pt +python -m gnnrec.hge.rhco.smooth --dataset=oag-venue model/word2vec/oag-cs.model data/graph/pos_graph_oag-venue_t5l.bin model/rhco_oag-venue.pt +``` + +消融实验 + +```shell +python -m gnnrec.hge.rhco.train --dataset=ogbn-mag --model=RHCO_sc model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5l.bin model/rhco_sc_ogbn-mag.pt +python -m gnnrec.hge.rhco.train --dataset=ogbn-mag --model=RHCO_pg model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5l.bin model/rhco_pg_ogbn-mag.pt +``` + +### 实验结果 + +[顶点分类](gnnrec/hge/result/node_classification.csv) + +[参数敏感性分析](gnnrec/hge/result/param_analysis.csv) + +[消融实验](gnnrec/hge/result/ablation_study.csv) diff --git a/academic_graph/__init__.py b/academic_graph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/academic_graph/asgi.py b/academic_graph/asgi.py new file mode 100644 index 0000000..fe5a822 --- /dev/null +++ b/academic_graph/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for academic_graph project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'academic_graph.settings') + +application = get_asgi_application() diff --git a/academic_graph/settings/__init__.py b/academic_graph/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/academic_graph/settings/common.py b/academic_graph/settings/common.py new file mode 100644 index 0000000..fb28e4a --- /dev/null +++ b/academic_graph/settings/common.py @@ -0,0 +1,136 @@ +""" +Django settings for academic_graph project. + +Generated by 'django-admin startproject' using Django 3.2.8. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ['SECRET_KEY'] + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]', '10.2.4.100'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rank', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'academic_graph.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'academic_graph.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'OPTIONS': { + 'read_default_file': '.mylogin.cnf', + 'charset': 'utf8mb4', + }, + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'zh-hans' + +TIME_ZONE = 'Asia/Shanghai' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'static' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_URL = 'rank:login' + +# 自定义设置 +PAGE_SIZE = 20 +TESTING = True diff --git a/academic_graph/settings/dev.py b/academic_graph/settings/dev.py new file mode 100644 index 0000000..96a7ba8 --- /dev/null +++ b/academic_graph/settings/dev.py @@ -0,0 +1,6 @@ +from .common import * # noqa + +DEBUG = True + +# 自定义设置 +TESTING = False diff --git a/academic_graph/settings/prod.py b/academic_graph/settings/prod.py new file mode 100644 index 0000000..db45264 --- /dev/null +++ b/academic_graph/settings/prod.py @@ -0,0 +1,6 @@ +from .common import * # noqa + +DEBUG = False + +# 自定义设置 +TESTING = False diff --git a/academic_graph/settings/test.py b/academic_graph/settings/test.py new file mode 100644 index 0000000..550384d --- /dev/null +++ b/academic_graph/settings/test.py @@ -0,0 +1,11 @@ +from .common import * # noqa + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'test.sqlite3', + } +} + +# 自定义设置 +TESTING = True diff --git a/academic_graph/urls.py b/academic_graph/urls.py new file mode 100644 index 0000000..aa9e719 --- /dev/null +++ b/academic_graph/urls.py @@ -0,0 +1,25 @@ +"""academic_graph URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.conf import settings +from django.contrib import admin +from django.urls import path, include +from django.views import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('static/', static.serve, {'document_root': settings.STATIC_ROOT}, name='static'), + path('rank/', include('rank.urls')), +] diff --git a/academic_graph/wsgi.py b/academic_graph/wsgi.py new file mode 100644 index 0000000..3325cb6 --- /dev/null +++ b/academic_graph/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for academic_graph project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'academic_graph.settings') + +application = get_wsgi_application() diff --git a/gnnrec/__init__.py b/gnnrec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/config.py b/gnnrec/config.py new file mode 100644 index 0000000..6c3e9a9 --- /dev/null +++ b/gnnrec/config.py @@ -0,0 +1,10 @@ +from pathlib import Path + +# 项目根目录 +BASE_DIR = Path(__file__).resolve().parent.parent + +# 数据集目录 +DATA_DIR = BASE_DIR / 'data' + +# 模型保存目录 +MODEL_DIR = BASE_DIR / 'model' diff --git a/gnnrec/hge/__init__.py b/gnnrec/hge/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/cs/__init__.py b/gnnrec/hge/cs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/cs/model.py b/gnnrec/hge/cs/model.py new file mode 100644 index 0000000..4da0458 --- /dev/null +++ b/gnnrec/hge/cs/model.py @@ -0,0 +1,113 @@ +import dgl.function as fn +import torch +import torch.nn as nn + + +class LabelPropagation(nn.Module): + + def __init__(self, num_layers, alpha, norm): + """标签传播模型 + + .. math:: + Y^{(t+1)} = \\alpha SY^{(t)} + (1-\\alpha)Y, Y^{(0)} = Y + + :param num_layers: int 传播层数 + :param alpha: float α参数 + :param norm: str 邻接矩阵归一化方式 + 'left': S=D^{-1}A, 'right': S=AD^{-1}, 'both': S=D^{-1/2}AD^{-1/2} + """ + super().__init__() + self.num_layers = num_layers + self.alpha = alpha + self.norm = norm + + @torch.no_grad() + def forward(self, g, labels, mask=None, post_step=None): + """ + :param g: DGLGraph 无向图 + :param labels: tensor(N, C) one-hot标签 + :param mask: tensor(N), optional 有标签顶点mask + :param post_step: callable, optional f: tensor(N, C) -> tensor(N, C) + :return: tensor(N, C) 预测标签概率 + """ + with g.local_scope(): + if mask is not None: + y = torch.zeros_like(labels) + y[mask] = labels[mask] + else: + y = labels + + residual = (1 - self.alpha) * y + degs = g.in_degrees().float().clamp(min=1) + norm = torch.pow(degs, -0.5 if self.norm == 'both' else -1).unsqueeze(1) # (N, 1) + for _ in range(self.num_layers): + if self.norm in ('both', 'right'): + y *= norm + g.ndata['h'] = y + g.update_all(fn.copy_u('h', 'm'), fn.sum('m', 'h')) + y = self.alpha * g.ndata.pop('h') + if self.norm in ('both', 'left'): + y *= norm + y += residual + if post_step is not None: + y = post_step(y) + return y + + +class CorrectAndSmooth(nn.Module): + + def __init__( + self, num_correct_layers, correct_alpha, correct_norm, + num_smooth_layers, smooth_alpha, smooth_norm, scale=1.0): + """C&S模型""" + super().__init__() + self.correct_prop = LabelPropagation(num_correct_layers, correct_alpha, correct_norm) + self.smooth_prop = LabelPropagation(num_smooth_layers, smooth_alpha, smooth_norm) + self.scale = scale + + def correct(self, g, labels, base_pred, mask): + """Correct步,修正基础预测中的误差 + + :param g: DGLGraph 无向图 + :param labels: tensor(N, C) one-hot标签 + :param base_pred: tensor(N, C) 基础预测 + :param mask: tensor(N) 训练集mask + :return: tensor(N, C) 修正后的预测 + """ + err = torch.zeros_like(base_pred) # (N, C) + err[mask] = labels[mask] - base_pred[mask] + + # FDiff-scale: 对训练集固定误差 + def fix_input(y): + y[mask] = err[mask] + return y + + smoothed_err = self.correct_prop(g, err, post_step=fix_input) # \hat{E} + corrected_pred = base_pred + self.scale * smoothed_err # Z^{(r)} + corrected_pred[corrected_pred.isnan()] = base_pred[corrected_pred.isnan()] + return corrected_pred + + def smooth(self, g, labels, corrected_pred, mask): + """Smooth步,平滑最终预测 + + :param g: DGLGraph 无向图 + :param labels: tensor(N, C) one-hot标签 + :param corrected_pred: tensor(N, C) 修正后的预测 + :param mask: tensor(N) 训练集mask + :return: tensor(N, C) 最终预测 + """ + guess = corrected_pred + guess[mask] = labels[mask] + return self.smooth_prop(g, guess) + + def forward(self, g, labels, base_pred, mask): + """ + :param g: DGLGraph 无向图 + :param labels: tensor(N, C) one-hot标签 + :param base_pred: tensor(N, C) 基础预测 + :param mask: tensor(N) 训练集mask + :return: tensor(N, C) 最终预测 + """ + # corrected_pred = self.correct(g, labels, base_pred, mask) + corrected_pred = base_pred + return self.smooth(g, labels, corrected_pred, mask) diff --git a/gnnrec/hge/cs/train.py b/gnnrec/hge/cs/train.py new file mode 100644 index 0000000..a0aa912 --- /dev/null +++ b/gnnrec/hge/cs/train.py @@ -0,0 +1,101 @@ +import argparse + +import dgl +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim + +from gnnrec.hge.cs.model import CorrectAndSmooth +from gnnrec.hge.utils import set_random_seed, get_device, load_data, calc_metrics, METRICS_STR + + +def train_base_model(base_model, feats, labels, train_idx, val_idx, test_idx, evaluator, args): + print('Training base model...') + optimizer = optim.Adam(base_model.parameters(), lr=args.lr) + for epoch in range(args.epochs): + base_model.train() + logits = base_model(feats) + loss = F.cross_entropy(logits[train_idx], labels[train_idx]) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(('Epoch {:d} | Loss {:.4f} | ' + METRICS_STR).format( + epoch, loss.item(), + *evaluate(base_model, feats, labels, train_idx, val_idx, test_idx, evaluator) + )) + + +@torch.no_grad() +def evaluate(model, feats, labels, train_idx, val_idx, test_idx, evaluator): + model.eval() + logits = model(feats) + return calc_metrics(logits, labels, train_idx, val_idx, test_idx, evaluator) + + +def correct_and_smooth(base_model, g, feats, labels, train_idx, val_idx, test_idx, evaluator, args): + print('Training C&S...') + base_model.eval() + base_pred = base_model(feats).softmax(dim=1) # 注意要softmax + + cs = CorrectAndSmooth( + args.num_correct_layers, args.correct_alpha, args.correct_norm, + args.num_smooth_layers, args.smooth_alpha, args.smooth_norm, args.scale + ) + mask = torch.cat([train_idx, val_idx]) + logits = cs(g, F.one_hot(labels).float(), base_pred, mask) + _, _, test_acc, _, _, test_f1 = calc_metrics(logits, labels, train_idx, val_idx, test_idx, evaluator) + print('Test Acc {:.4f} | Test Macro-F1 {:.4f}'.format(test_acc, test_f1)) + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, _, feat, labels, _, train_idx, val_idx, test_idx, evaluator = \ + load_data(args.dataset, device) + feat = (feat - feat.mean(dim=0)) / feat.std(dim=0) + # 标签传播图 + if args.dataset in ('acm', 'dblp'): + pos_v, pos_u = data.pos + pg = dgl.graph((pos_u, pos_v), device=device) + else: + pg = dgl.load_graphs(args.prop_graph)[0][-1].to(device) + + if args.dataset == 'oag-venue': + labels[labels == -1] = 0 + + base_model = nn.Linear(feat.shape[1], data.num_classes).to(device) + train_base_model(base_model, feat, labels, train_idx, val_idx, test_idx, evaluator, args) + correct_and_smooth(base_model, pg, feat, labels, train_idx, val_idx, test_idx, evaluator, args) + + +def main(): + parser = argparse.ArgumentParser(description='训练C&S模型') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['acm', 'dblp', 'ogbn-mag', 'oag-venue'], default='ogbn-mag', help='数据集') + # 基础模型 + parser.add_argument('--epochs', type=int, default=300, help='基础模型训练epoch数') + parser.add_argument('--lr', type=float, default=0.01, help='基础模型学习率') + # C&S + parser.add_argument('--prop-graph', help='标签传播图所在路径') + parser.add_argument('--num-correct-layers', type=int, default=50, help='Correct步骤传播层数') + parser.add_argument('--correct-alpha', type=float, default=0.5, help='Correct步骤α值') + parser.add_argument( + '--correct-norm', choices=['left', 'right', 'both'], default='both', + help='Correct步骤归一化方式' + ) + parser.add_argument('--num-smooth-layers', type=int, default=50, help='Smooth步骤传播层数') + parser.add_argument('--smooth-alpha', type=float, default=0.5, help='Smooth步骤α值') + parser.add_argument( + '--smooth-norm', choices=['left', 'right', 'both'], default='both', + help='Smooth步骤归一化方式' + ) + parser.add_argument('--scale', type=float, default=20, help='放缩系数') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/data/__init__.py b/gnnrec/hge/data/__init__.py new file mode 100644 index 0000000..5d1e3f5 --- /dev/null +++ b/gnnrec/hge/data/__init__.py @@ -0,0 +1 @@ +from .heco import ACMDataset, DBLPDataset diff --git a/gnnrec/hge/data/heco.py b/gnnrec/hge/data/heco.py new file mode 100644 index 0000000..439bc33 --- /dev/null +++ b/gnnrec/hge/data/heco.py @@ -0,0 +1,204 @@ +import os +import shutil +import zipfile + +import dgl +import numpy as np +import pandas as pd +import scipy.sparse as sp +import torch +from dgl.data import DGLDataset +from dgl.data.utils import download, save_graphs, save_info, load_graphs, load_info, \ + generate_mask_tensor, idx2mask + + +class HeCoDataset(DGLDataset): + """HeCo模型使用的数据集基类 + + 论文链接:https://arxiv.org/pdf/2105.09111 + + 类属性 + ----- + * num_classes: 类别数 + * metapaths: 使用的元路径 + * predict_ntype: 目标顶点类型 + * pos: (tensor(E_pos), tensor(E_pos)) 目标顶点正样本对,pos[1][i]是pos[0][i]的正样本 + """ + + def __init__(self, name, ntypes): + url = 'https://api.github.com/repos/liun-online/HeCo/zipball/main' + self._ntypes = {ntype[0]: ntype for ntype in ntypes} + super().__init__(name + '-heco', url) + + def download(self): + file_path = os.path.join(self.raw_dir, 'HeCo-main.zip') + if not os.path.exists(file_path): + download(self.url, path=file_path) + with zipfile.ZipFile(file_path, 'r') as f: + f.extractall(self.raw_dir) + shutil.copytree( + os.path.join(self.raw_dir, 'HeCo-main', 'data', self.name.split('-')[0]), + os.path.join(self.raw_path) + ) + + def save(self): + save_graphs(os.path.join(self.save_path, self.name + '_dgl_graph.bin'), [self.g]) + save_info(os.path.join(self.raw_path, self.name + '_pos.pkl'), {'pos_i': self.pos_i, 'pos_j': self.pos_j}) + + def load(self): + graphs, _ = load_graphs(os.path.join(self.save_path, self.name + '_dgl_graph.bin')) + self.g = graphs[0] + ntype = self.predict_ntype + self._num_classes = self.g.nodes[ntype].data['label'].max().item() + 1 + for k in ('train_mask', 'val_mask', 'test_mask'): + self.g.nodes[ntype].data[k] = self.g.nodes[ntype].data[k].bool() + info = load_info(os.path.join(self.raw_path, self.name + '_pos.pkl')) + self.pos_i, self.pos_j = info['pos_i'], info['pos_j'] + + def process(self): + self.g = dgl.heterograph(self._read_edges()) + + feats = self._read_feats() + for ntype, feat in feats.items(): + self.g.nodes[ntype].data['feat'] = feat + + labels = torch.from_numpy(np.load(os.path.join(self.raw_path, 'labels.npy'))).long() + self._num_classes = labels.max().item() + 1 + self.g.nodes[self.predict_ntype].data['label'] = labels + + n = self.g.num_nodes(self.predict_ntype) + for split in ('train', 'val', 'test'): + idx = np.load(os.path.join(self.raw_path, f'{split}_60.npy')) + mask = generate_mask_tensor(idx2mask(idx, n)) + self.g.nodes[self.predict_ntype].data[f'{split}_mask'] = mask + + pos_i, pos_j = sp.load_npz(os.path.join(self.raw_path, 'pos.npz')).nonzero() + self.pos_i, self.pos_j = torch.from_numpy(pos_i).long(), torch.from_numpy(pos_j).long() + + def _read_edges(self): + edges = {} + for file in os.listdir(self.raw_path): + name, ext = os.path.splitext(file) + if ext == '.txt': + u, v = name + e = pd.read_csv(os.path.join(self.raw_path, f'{u}{v}.txt'), sep='\t', names=[u, v]) + src = e[u].to_list() + dst = e[v].to_list() + edges[(self._ntypes[u], f'{u}{v}', self._ntypes[v])] = (src, dst) + edges[(self._ntypes[v], f'{v}{u}', self._ntypes[u])] = (dst, src) + return edges + + def _read_feats(self): + feats = {} + for u in self._ntypes: + file = os.path.join(self.raw_path, f'{u}_feat.npz') + if os.path.exists(file): + feats[self._ntypes[u]] = torch.from_numpy(sp.load_npz(file).toarray()).float() + return feats + + def has_cache(self): + return os.path.exists(os.path.join(self.save_path, self.name + '_dgl_graph.bin')) + + def __getitem__(self, idx): + if idx != 0: + raise IndexError('This dataset has only one graph') + return self.g + + def __len__(self): + return 1 + + @property + def num_classes(self): + return self._num_classes + + @property + def metapaths(self): + raise NotImplementedError + + @property + def predict_ntype(self): + raise NotImplementedError + + @property + def pos(self): + return self.pos_i, self.pos_j + + +class ACMDataset(HeCoDataset): + """ACM数据集 + + 统计数据 + ----- + * 顶点:4019 paper, 7167 author, 60 subject + * 边:13407 paper-author, 4019 paper-subject + * 目标顶点类型:paper + * 类别数:3 + * 顶点划分:180 train, 1000 valid, 1000 test + + paper顶点特征 + ----- + * feat: tensor(N_paper, 1902) + * label: tensor(N_paper) 0~2 + * train_mask, val_mask, test_mask: tensor(N_paper) + + author顶点特征 + ----- + * feat: tensor(7167, 1902) + """ + + def __init__(self): + super().__init__('acm', ['paper', 'author', 'subject']) + + @property + def metapaths(self): + return [['pa', 'ap'], ['ps', 'sp']] + + @property + def predict_ntype(self): + return 'paper' + + +class DBLPDataset(HeCoDataset): + """DBLP数据集 + + 统计数据 + ----- + * 顶点:4057 author, 14328 paper, 20 conference, 7723 term + * 边:19645 paper-author, 14328 paper-conference, 85810 paper-term + * 目标顶点类型:author + * 类别数:4 + * 顶点划分:240 train, 1000 valid, 1000 test + + author顶点特征 + ----- + * feat: tensor(N_author, 334) + * label: tensor(N_author) 0~3 + * train_mask, val_mask, test_mask: tensor(N_author) + + paper顶点特征 + ----- + * feat: tensor(14328, 4231) + + term顶点特征 + ----- + * feat: tensor(7723, 50) + """ + + def __init__(self): + super().__init__('dblp', ['author', 'paper', 'conference', 'term']) + + def _read_feats(self): + feats = {} + for u in 'ap': + file = os.path.join(self.raw_path, f'{u}_feat.npz') + feats[self._ntypes[u]] = torch.from_numpy(sp.load_npz(file).toarray()).float() + feats['term'] = torch.from_numpy(np.load(os.path.join(self.raw_path, 't_feat.npz'))).float() + return feats + + @property + def metapaths(self): + return [['ap', 'pa'], ['ap', 'pc', 'cp', 'pa'], ['ap', 'pt', 'tp', 'pa']] + + @property + def predict_ntype(self): + return 'author' diff --git a/gnnrec/hge/heco/__init__.py b/gnnrec/hge/heco/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/heco/model.py b/gnnrec/hge/heco/model.py new file mode 100644 index 0000000..769684d --- /dev/null +++ b/gnnrec/hge/heco/model.py @@ -0,0 +1,269 @@ +import dgl.function as fn +import torch +import torch.nn as nn +import torch.nn.functional as F +from dgl.nn import GraphConv +from dgl.ops import edge_softmax + + +class HeCoGATConv(nn.Module): + + def __init__(self, hidden_dim, attn_drop=0.0, negative_slope=0.01, activation=None): + """HeCo作者代码中使用的GAT + + :param hidden_dim: int 隐含特征维数 + :param attn_drop: float 注意力dropout + :param negative_slope: float, optional LeakyReLU负斜率,默认为0.01 + :param activation: callable, optional 激活函数,默认为None + """ + super().__init__() + self.attn_l = nn.Parameter(torch.FloatTensor(1, hidden_dim)) + self.attn_r = nn.Parameter(torch.FloatTensor(1, hidden_dim)) + self.attn_drop = nn.Dropout(attn_drop) + self.leaky_relu = nn.LeakyReLU(negative_slope) + self.activation = activation + self.reset_parameters() + + def reset_parameters(self): + gain = nn.init.calculate_gain('relu') + nn.init.xavier_normal_(self.attn_l, gain) + nn.init.xavier_normal_(self.attn_r, gain) + + def forward(self, g, feat_src, feat_dst): + """ + :param g: DGLGraph 邻居-目标顶点二分图 + :param feat_src: tensor(N_src, d) 邻居顶点输入特征 + :param feat_dst: tensor(N_dst, d) 目标顶点输入特征 + :return: tensor(N_dst, d) 目标顶点输出特征 + """ + with g.local_scope(): + # HeCo作者代码中使用attn_drop的方式与原始GAT不同,这样是不对的,却能顶点聚类提升性能…… + attn_l = self.attn_drop(self.attn_l) + attn_r = self.attn_drop(self.attn_r) + el = (feat_src * attn_l).sum(dim=-1).unsqueeze(dim=-1) # (N_src, 1) + er = (feat_dst * attn_r).sum(dim=-1).unsqueeze(dim=-1) # (N_dst, 1) + g.srcdata.update({'ft': feat_src, 'el': el}) + g.dstdata['er'] = er + g.apply_edges(fn.u_add_v('el', 'er', 'e')) + e = self.leaky_relu(g.edata.pop('e')) + g.edata['a'] = edge_softmax(g, e) # (E, 1) + + # 消息传递 + g.update_all(fn.u_mul_e('ft', 'a', 'm'), fn.sum('m', 'ft')) + ret = g.dstdata['ft'] + if self.activation: + ret = self.activation(ret) + return ret + + +class Attention(nn.Module): + + def __init__(self, hidden_dim, attn_drop): + """语义层次的注意力 + + :param hidden_dim: int 隐含特征维数 + :param attn_drop: float 注意力dropout + """ + super().__init__() + self.fc = nn.Linear(hidden_dim, hidden_dim) + self.attn = nn.Parameter(torch.FloatTensor(1, hidden_dim)) + self.attn_drop = nn.Dropout(attn_drop) + self.reset_parameters() + + def reset_parameters(self): + gain = nn.init.calculate_gain('relu') + nn.init.xavier_normal_(self.fc.weight, gain) + nn.init.xavier_normal_(self.attn, gain) + + def forward(self, h): + """ + :param h: tensor(N, M, d) 顶点基于不同元路径/类型的嵌入,N为顶点数,M为元路径/类型数 + :return: tensor(N, d) 顶点的最终嵌入 + """ + attn = self.attn_drop(self.attn) + # (N, M, d) -> (M, d) -> (M, 1) + w = torch.tanh(self.fc(h)).mean(dim=0).matmul(attn.t()) + beta = torch.softmax(w, dim=0) # (M, 1) + beta = beta.expand((h.shape[0],) + beta.shape) # (N, M, 1) + z = (beta * h).sum(dim=1) # (N, d) + return z + + +class NetworkSchemaEncoder(nn.Module): + + def __init__(self, hidden_dim, attn_drop, relations): + """网络结构视图编码器 + + :param hidden_dim: int 隐含特征维数 + :param attn_drop: float 注意力dropout + :param relations: List[(str, str, str)] 目标顶点关联的关系列表,长度为邻居类型数S + """ + super().__init__() + self.relations = relations + self.dtype = relations[0][2] + self.gats = nn.ModuleDict({ + r[0]: HeCoGATConv(hidden_dim, attn_drop, activation=F.elu) + for r in relations + }) + self.attn = Attention(hidden_dim, attn_drop) + + def forward(self, g, feats): + """ + :param g: DGLGraph 异构图 + :param feats: Dict[str, tensor(N_i, d)] 顶点类型到输入特征的映射 + :return: tensor(N_dst, d) 目标顶点的最终嵌入 + """ + feat_dst = feats[self.dtype][:g.num_dst_nodes(self.dtype)] + h = [] + for stype, etype, dtype in self.relations: + h.append(self.gats[stype](g[stype, etype, dtype], feats[stype], feat_dst)) + h = torch.stack(h, dim=1) # (N_dst, S, d) + z_sc = self.attn(h) # (N_dst, d) + return z_sc + + +class PositiveGraphEncoder(nn.Module): + + def __init__(self, num_metapaths, in_dim, hidden_dim, attn_drop): + """正样本视图编码器 + + :param num_metapaths: int 元路径数量M + :param hidden_dim: int 隐含特征维数 + :param attn_drop: float 注意力dropout + """ + super().__init__() + self.gcns = nn.ModuleList([ + GraphConv(in_dim, hidden_dim, norm='right', activation=nn.PReLU()) + for _ in range(num_metapaths) + ]) + self.attn = Attention(hidden_dim, attn_drop) + + def forward(self, mgs, feats): + """ + :param mgs: List[DGLGraph] 正样本图 + :param feats: List[tensor(N, d)] 输入顶点特征 + :return: tensor(N, d) 输出顶点特征 + """ + h = [gcn(mg, feat) for gcn, mg, feat in zip(self.gcns, mgs, feats)] + h = torch.stack(h, dim=1) # (N, M, d) + z_pg = self.attn(h) # (N, d) + return z_pg + + +class Contrast(nn.Module): + + def __init__(self, hidden_dim, tau, lambda_): + """对比损失模块 + + :param hidden_dim: int 隐含特征维数 + :param tau: float 温度参数 + :param lambda_: float 0~1之间,网络结构视图损失的系数(元路径视图损失的系数为1-λ) + """ + super().__init__() + self.proj = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.ELU(), + nn.Linear(hidden_dim, hidden_dim) + ) + self.tau = tau + self.lambda_ = lambda_ + self.reset_parameters() + + def reset_parameters(self): + gain = nn.init.calculate_gain('relu') + for model in self.proj: + if isinstance(model, nn.Linear): + nn.init.xavier_normal_(model.weight, gain) + + def sim(self, x, y): + """计算相似度矩阵 + + :param x: tensor(N, d) + :param y: tensor(N, d) + :return: tensor(N, N) S[i, j] = exp(cos(x[i], y[j])) + """ + x_norm = torch.norm(x, dim=1, keepdim=True) + y_norm = torch.norm(y, dim=1, keepdim=True) + numerator = torch.mm(x, y.t()) + denominator = torch.mm(x_norm, y_norm.t()) + return torch.exp(numerator / denominator / self.tau) + + def forward(self, z_sc, z_mp, pos): + """ + :param z_sc: tensor(N, d) 目标顶点在网络结构视图下的嵌入 + :param z_mp: tensor(N, d) 目标顶点在元路径视图下的嵌入 + :param pos: tensor(B, N) 0-1张量,每个目标顶点的正样本 + (B是batch大小,真正的目标顶点;N是B个目标顶点加上其正样本后的顶点数) + :return: float 对比损失 + """ + z_sc_proj = self.proj(z_sc) + z_mp_proj = self.proj(z_mp) + sim_sc2mp = self.sim(z_sc_proj, z_mp_proj) + sim_mp2sc = sim_sc2mp.t() + + batch = pos.shape[0] + sim_sc2mp = sim_sc2mp / (sim_sc2mp.sum(dim=1, keepdim=True) + 1e-8) # 不能改成/= + loss_sc = -torch.log(torch.sum(sim_sc2mp[:batch] * pos, dim=1)).mean() + + sim_mp2sc = sim_mp2sc / (sim_mp2sc.sum(dim=1, keepdim=True) + 1e-8) + loss_mp = -torch.log(torch.sum(sim_mp2sc[:batch] * pos, dim=1)).mean() + return self.lambda_ * loss_sc + (1 - self.lambda_) * loss_mp + + +class HeCo(nn.Module): + + def __init__(self, in_dims, hidden_dim, feat_drop, attn_drop, relations, tau, lambda_): + """HeCo模型 + + :param in_dims: Dict[str, int] 顶点类型到输入特征维数的映射 + :param hidden_dim: int 隐含特征维数 + :param feat_drop: float 输入特征dropout + :param attn_drop: float 注意力dropout + :param relations: List[(str, str, str)] 目标顶点关联的关系列表,长度为邻居类型数S + :param tau: float 温度参数 + :param lambda_: float 0~1之间,网络结构视图损失的系数(元路径视图损失的系数为1-λ) + """ + super().__init__() + self.dtype = relations[0][2] + self.fcs = nn.ModuleDict({ + ntype: nn.Linear(in_dim, hidden_dim) for ntype, in_dim in in_dims.items() + }) + self.feat_drop = nn.Dropout(feat_drop) + self.sc_encoder = NetworkSchemaEncoder(hidden_dim, attn_drop, relations) + self.mp_encoder = PositiveGraphEncoder(len(relations), hidden_dim, hidden_dim, attn_drop) + self.contrast = Contrast(hidden_dim, tau, lambda_) + self.reset_parameters() + + def reset_parameters(self): + gain = nn.init.calculate_gain('relu') + for ntype in self.fcs: + nn.init.xavier_normal_(self.fcs[ntype].weight, gain) + + def forward(self, g, feats, mgs, mg_feats, pos): + """ + :param g: DGLGraph 异构图 + :param feats: Dict[str, tensor(N_i, d_in)] 顶点类型到输入特征的映射 + :param mgs: List[DGLBlock] 正样本图,len(mgs)=元路径数量=目标顶点邻居类型数S≠模型层数 + :param mg_feats: List[tensor(N_pos_src, d_in)] 正样本图源顶点的输入特征 + :param pos: tensor(B, N) 布尔张量,每个顶点的正样本 + (B是batch大小,真正的目标顶点;N是B个目标顶点加上其正样本后的顶点数) + :return: float, tensor(B, d_hid) 对比损失,元路径编码器输出的目标顶点特征 + """ + h = {ntype: F.elu(self.feat_drop(self.fcs[ntype](feat))) for ntype, feat in feats.items()} + mg_h = [F.elu(self.feat_drop(self.fcs[self.dtype](mg_feat))) for mg_feat in mg_feats] + z_sc = self.sc_encoder(g, h) # (N, d_hid) + z_mp = self.mp_encoder(mgs, mg_h) # (N, d_hid) + loss = self.contrast(z_sc, z_mp, pos) + return loss, z_mp[:pos.shape[0]] + + @torch.no_grad() + def get_embeds(self, mgs, feats): + """计算目标顶点的最终嵌入(z_mp) + + :param mgs: List[DGLBlock] 正样本图 + :param feats: List[tensor(N_pos_src, d_in)] 正样本图源顶点的输入特征 + :return: tensor(N_tgt, d_hid) 目标顶点的最终嵌入 + """ + h = [F.elu(self.fcs[self.dtype](feat)) for feat in feats] + z_mp = self.mp_encoder(mgs, h) + return z_mp diff --git a/gnnrec/hge/heco/sampler.py b/gnnrec/hge/heco/sampler.py new file mode 100644 index 0000000..f39569d --- /dev/null +++ b/gnnrec/hge/heco/sampler.py @@ -0,0 +1,28 @@ +import torch +from dgl.dataloading import MultiLayerNeighborSampler + + +class PositiveSampler(MultiLayerNeighborSampler): + + def __init__(self, fanouts, pos): + """用于HeCo模型的邻居采样器 + + 对于每个batch的目标顶点,将其正样本添加到目标顶点并生成block + + :param fanouts: 每层的邻居采样数(见MultiLayerNeighborSampler) + :param pos: tensor(N, T_pos) 每个顶点的正样本id,N是目标顶点数 + """ + super().__init__(fanouts) + self.pos = pos + + def sample_blocks(self, g, seed_nodes, exclude_eids=None): + # 如果g是异构图则seed_nodes是字典,应当只有目标顶点类型 + if not g.is_homogeneous: + assert len(seed_nodes) == 1, 'PositiveSampler: 异构图只能指定目标顶点这一种类型' + ntype, seed_nodes = next(iter(seed_nodes.items())) + pos_samples = self.pos[seed_nodes].flatten() # (B, T_pos) -> (B*T_pos,) + added = list(set(pos_samples.tolist()) - set(seed_nodes.tolist())) + seed_nodes = torch.cat([seed_nodes, torch.tensor(added, device=seed_nodes.device)]) + if not g.is_homogeneous: + seed_nodes = {ntype: seed_nodes} + return super().sample_blocks(g, seed_nodes, exclude_eids) diff --git a/gnnrec/hge/heco/train.py b/gnnrec/hge/heco/train.py new file mode 100644 index 0000000..9897aed --- /dev/null +++ b/gnnrec/hge/heco/train.py @@ -0,0 +1,117 @@ +import argparse + +import dgl +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +from dgl.dataloading import NodeDataLoader +from torch.utils.data import DataLoader +from tqdm import tqdm, trange + +from gnnrec.hge.heco.model import HeCo +from gnnrec.hge.heco.sampler import PositiveSampler +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, accuracy, \ + calc_metrics, METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, evaluator = \ + load_data(args.dataset, device) + add_node_feat(g, 'pretrained', args.node_embed_path) + features = g.nodes[predict_ntype].data['feat'] + relations = [r for r in g.canonical_etypes if r[2] == predict_ntype] + + (*mgs, pos_g), _ = dgl.load_graphs(args.pos_graph_path) + mgs = [mg.to(device) for mg in mgs] + pos_g = pos_g.to(device) + pos = pos_g.in_edges(pos_g.nodes())[0].view(pos_g.num_nodes(), -1) # (N, T_pos) 每个目标顶点的正样本id + + id_loader = DataLoader(train_idx, batch_size=args.batch_size) + sampler = PositiveSampler([None], pos) + loader = NodeDataLoader(g, {predict_ntype: train_idx}, sampler, device=device, batch_size=args.batch_size) + mg_loaders = [ + NodeDataLoader(mg, train_idx, sampler, device=device, batch_size=args.batch_size) + for mg in mgs + ] + pos_loader = NodeDataLoader(pos_g, train_idx, sampler, device=device, batch_size=args.batch_size) + + model = HeCo( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, args.feat_drop, args.attn_drop, relations, args.tau, args.lambda_ + ).to(device) + optimizer = optim.Adam(model.parameters(), lr=args.lr) + for epoch in range(args.epochs): + model.train() + losses = [] + for (batch, (_, _, blocks), *mg_blocks, (_, _, pos_blocks)) in tqdm(zip(id_loader, loader, *mg_loaders, pos_loader)): + block = blocks[0] + mg_feats = [features[i] for i, _, _ in mg_blocks] + mg_blocks = [b[0] for _, _, b in mg_blocks] + pos_block = pos_blocks[0] + batch_pos = torch.zeros(pos_block.num_dst_nodes(), batch.shape[0], dtype=torch.int, device=device) + batch_pos[pos_block.in_edges(torch.arange(batch.shape[0], device=device))] = 1 + loss, _ = model(block, block.srcdata['feat'], mg_blocks, mg_feats, batch_pos.t()) + losses.append(loss.item()) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + torch.cuda.empty_cache() + print('Epoch {:d} | Loss {:.4f}'.format(epoch, sum(losses) / len(losses))) + if epoch % args.eval_every == 0 or epoch == args.epochs - 1: + print(METRICS_STR.format(*evaluate( + model, mgs, features, device, labels, data.num_classes, + train_idx, val_idx, test_idx, evaluator + ))) + + +def evaluate(model, mgs, feat, device, labels, num_classes, train_idx, val_idx, test_idx, evaluator): + model.eval() + embeds = model.get_embeds(mgs, [feat] * len(mgs)) + + clf = nn.Linear(embeds.shape[1], num_classes).to(device) + optimizer = optim.Adam(clf.parameters(), lr=0.05) + best_acc, best_logits = 0, None + for epoch in trange(200): + clf.train() + logits = clf(embeds) + loss = F.cross_entropy(logits[train_idx], labels[train_idx]) + optimizer.zero_grad() + loss.backward() + optimizer.step() + + with torch.no_grad(): + clf.eval() + logits = clf(embeds) + predict = logits.argmax(dim=1) + if accuracy(predict[val_idx], labels[val_idx]) > best_acc: + best_logits = logits + return calc_metrics(best_logits, labels, train_idx, val_idx, test_idx, evaluator) + + +def main(): + parser = argparse.ArgumentParser(description='训练HeCo模型') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['ogbn-mag', 'oag-venue'], default='ogbn-mag', help='数据集') + parser.add_argument('--num-hidden', type=int, default=64, help='隐藏层维数') + parser.add_argument('--feat-drop', type=float, default=0.3, help='特征dropout') + parser.add_argument('--attn-drop', type=float, default=0.5, help='注意力dropout') + parser.add_argument('--tau', type=float, default=0.8, help='温度参数') + parser.add_argument('--lambda', type=float, default=0.5, dest='lambda_', help='对比损失的平衡系数') + parser.add_argument('--epochs', type=int, default=200, help='训练epoch数') + parser.add_argument('--batch-size', type=int, default=1024, help='批大小') + parser.add_argument('--lr', type=float, default=0.0008, help='学习率') + parser.add_argument('--eval-every', type=int, default=10, help='每多少个epoch计算一次准确率') + parser.add_argument('node_embed_path', help='预训练顶点嵌入路径') + parser.add_argument('pos_graph_path', help='正样本图路径') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/hgconv/__init__.py b/gnnrec/hge/hgconv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/hgconv/model.py b/gnnrec/hge/hgconv/model.py new file mode 100644 index 0000000..2072e28 --- /dev/null +++ b/gnnrec/hge/hgconv/model.py @@ -0,0 +1,291 @@ +import dgl.function as fn +import torch +import torch.nn as nn +import torch.nn.functional as F +from dgl.dataloading import MultiLayerFullNeighborSampler, NodeDataLoader +from dgl.ops import edge_softmax +from dgl.utils import expand_as_pair +from tqdm import tqdm + + +class MicroConv(nn.Module): + + def __init__( + self, out_dim, num_heads, fc_src, fc_dst, attn_src, + feat_drop=0.0, negative_slope=0.2, activation=None): + """微观层次卷积 + + 针对一种关系(边类型)R=,聚集关系R下的邻居信息,得到关系R关于dtype类型顶点的表示 + (特征转换矩阵和注意力向量是与顶点类型相关的,除此之外与GAT完全相同) + + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param fc_src: nn.Linear(d_in, K*d_out) 源顶点特征转换模块 + :param fc_dst: nn.Linear(d_in, K*d_out) 目标顶点特征转换模块 + :param attn_src: nn.Parameter(K, 2d_out) 源顶点类型对应的注意力向量 + :param feat_drop: float, optional 输入特征Dropout概率,默认为0 + :param negative_slope: float, optional LeakyReLU负斜率,默认为0.2 + :param activation: callable, optional 用于输出特征的激活函数,默认为None + """ + super().__init__() + self.out_dim = out_dim + self.num_heads = num_heads + self.fc_src = fc_src + self.fc_dst = fc_dst + self.attn_src = attn_src + self.feat_drop = nn.Dropout(feat_drop) + self.leaky_relu = nn.LeakyReLU(negative_slope) + self.activation = activation + + def forward(self, g, feat): + """ + :param g: DGLGraph 二分图(只包含一种关系) + :param feat: tensor(N_src, d_in) or (tensor(N_src, d_in), tensor(N_dst, d_in)) 输入特征 + :return: tensor(N_dst, K*d_out) 该关系关于目标顶点的表示 + """ + with g.local_scope(): + feat_src, feat_dst = expand_as_pair(feat, g) + feat_src = self.fc_src(self.feat_drop(feat_src)).view(-1, self.num_heads, self.out_dim) + feat_dst = self.fc_dst(self.feat_drop(feat_dst)).view(-1, self.num_heads, self.out_dim) + + # a^T (z_u || z_v) = (a_l^T || a_r^T) (z_u || z_v) = a_l^T z_u + a_r^T z_v = el + er + el = (feat_src * self.attn_src[:, :self.out_dim]).sum(dim=-1, keepdim=True) # (N_src, K, 1) + er = (feat_dst * self.attn_src[:, self.out_dim:]).sum(dim=-1, keepdim=True) # (N_dst, K, 1) + g.srcdata.update({'ft': feat_src, 'el': el}) + g.dstdata['er'] = er + g.apply_edges(fn.u_add_v('el', 'er', 'e')) + e = self.leaky_relu(g.edata.pop('e')) + g.edata['a'] = edge_softmax(g, e) # (E, K, 1) + + # 消息传递 + g.update_all(fn.u_mul_e('ft', 'a', 'm'), fn.sum('m', 'ft')) + ret = g.dstdata['ft'].view(-1, self.num_heads * self.out_dim) + if self.activation: + ret = self.activation(ret) + return ret + + +class MacroConv(nn.Module): + + def __init__(self, out_dim, num_heads, fc_node, fc_rel, attn, dropout=0.0, negative_slope=0.2): + """宏观层次卷积 + + 针对所有关系(边类型),将每种类型的顶点关联的所有关系关于该类型顶点的表示组合起来 + + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param fc_node: Dict[str, nn.Linear(d_in, K*d_out)] 顶点类型到顶点特征转换模块的映射 + :param fc_rel: Dict[str, nn.Linear(K*d_out, K*d_out)] 关系到关系表示转换模块的映射 + :param attn: nn.Parameter(K, 2d_out) + :param dropout: float, optional Dropout概率,默认为0 + :param negative_slope: float, optional LeakyReLU负斜率,默认为0.2 + """ + super().__init__() + self.out_dim = out_dim + self.num_heads = num_heads + self.fc_node = fc_node + self.fc_rel = fc_rel + self.attn = attn + self.dropout = nn.Dropout(dropout) + self.leaky_relu = nn.LeakyReLU(negative_slope) + + def forward(self, node_feats, rel_feats): + """ + :param node_feats: Dict[str, tensor(N_i, d_in) 顶点类型到输入顶点特征的映射 + :param rel_feats: Dict[(str, str, str), tensor(N_i, K*d_out)] + 关系(stype, etype, dtype)到关系关于其终点类型的表示的映射 + :return: Dict[str, tensor(N_i, K*d_out)] 顶点类型到最终顶点嵌入的映射 + """ + node_feats = { + ntype: self.fc_node[ntype](feat).view(-1, self.num_heads, self.out_dim) + for ntype, feat in node_feats.items() + } + rel_feats = { + r: self.fc_rel[r[1]](feat).view(-1, self.num_heads, self.out_dim) + for r, feat in rel_feats.items() + } + out_feats = {} + for ntype, node_feat in node_feats.items(): + rel_node_feats = [feat for rel, feat in rel_feats.items() if rel[2] == ntype] + if not rel_node_feats: + continue + elif len(rel_node_feats) == 1: + out_feats[ntype] = rel_node_feats[0].view(-1, self.num_heads * self.out_dim) + else: + rel_node_feats = torch.stack(rel_node_feats, dim=0) # (R, N_i, K, d_out) + cat_feats = torch.cat( + (node_feat.repeat(rel_node_feats.shape[0], 1, 1, 1), rel_node_feats), dim=-1 + ) # (R, N_i, K, 2d_out) + attn_scores = self.leaky_relu((self.attn * cat_feats).sum(dim=-1, keepdim=True)) + attn_scores = F.softmax(attn_scores, dim=0) # (R, N_i, K, 1) + out_feat = (attn_scores * rel_node_feats).sum(dim=0) # (N_i, K, d_out) + out_feats[ntype] = self.dropout(out_feat.reshape(-1, self.num_heads * self.out_dim)) + return out_feats + + +class HGConvLayer(nn.Module): + + def __init__(self, in_dim, out_dim, num_heads, ntypes, etypes, dropout=0.0, residual=True): + """HGConv层 + + :param in_dim: int 输入特征维数 + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param ntypes: List[str] 顶点类型列表 + :param etypes: List[(str, str, str)] 规范边类型列表 + :param dropout: float, optional Dropout概率,默认为0 + :param residual: bool, optional 是否使用残差连接,默认True + """ + super().__init__() + # 微观层次卷积的参数 + micro_fc = {ntype: nn.Linear(in_dim, num_heads * out_dim, bias=False) for ntype in ntypes} + micro_attn = { + ntype: nn.Parameter(torch.FloatTensor(size=(num_heads, 2 * out_dim))) + for ntype in ntypes + } + + # 宏观层次卷积的参数 + macro_fc_node = nn.ModuleDict({ + ntype: nn.Linear(in_dim, num_heads * out_dim, bias=False) for ntype in ntypes + }) + macro_fc_rel = nn.ModuleDict({ + r[1]: nn.Linear(num_heads * out_dim, num_heads * out_dim, bias=False) + for r in etypes + }) + macro_attn = nn.Parameter(torch.FloatTensor(size=(num_heads, 2 * out_dim))) + + self.micro_conv = nn.ModuleDict({ + etype: MicroConv( + out_dim, num_heads, micro_fc[stype], + micro_fc[dtype], micro_attn[stype], dropout, activation=F.relu + ) for stype, etype, dtype in etypes + }) + self.macro_conv = MacroConv( + out_dim, num_heads, macro_fc_node, macro_fc_rel, macro_attn, dropout + ) + + self.residual = residual + if residual: + self.res_fc = nn.ModuleDict({ + ntype: nn.Linear(in_dim, num_heads * out_dim) for ntype in ntypes + }) + self.res_weight = nn.ParameterDict({ + ntype: nn.Parameter(torch.rand(1)) for ntype in ntypes + }) + self.reset_parameters(micro_fc, micro_attn, macro_fc_node, macro_fc_rel, macro_attn) + + def reset_parameters(self, micro_fc, micro_attn, macro_fc_node, macro_fc_rel, macro_attn): + gain = nn.init.calculate_gain('relu') + for ntype in micro_fc: + nn.init.xavier_normal_(micro_fc[ntype].weight, gain=gain) + nn.init.xavier_normal_(micro_attn[ntype], gain=gain) + nn.init.xavier_normal_(macro_fc_node[ntype].weight, gain=gain) + if self.residual: + nn.init.xavier_normal_(self.res_fc[ntype].weight, gain=gain) + for etype in macro_fc_rel: + nn.init.xavier_normal_(macro_fc_rel[etype].weight, gain=gain) + nn.init.xavier_normal_(macro_attn, gain=gain) + + def forward(self, g, feats): + """ + :param g: DGLGraph 异构图 + :param feats: Dict[str, tensor(N_i, d_in)] 顶点类型到输入顶点特征的映射 + :return: Dict[str, tensor(N_i, K*d_out)] 顶点类型到最终顶点嵌入的映射 + """ + if g.is_block: + feats_dst = {ntype: feats[ntype][:g.num_dst_nodes(ntype)] for ntype in feats} + else: + feats_dst = feats + rel_feats = { + (stype, etype, dtype): self.micro_conv[etype]( + g[stype, etype, dtype], (feats[stype], feats_dst[dtype]) + ) + for stype, etype, dtype in g.canonical_etypes + if g.num_edges((stype, etype, dtype)) > 0 + } # {rel: tensor(N_i, K*d_out)} + out_feats = self.macro_conv(feats_dst, rel_feats) # {ntype: tensor(N_i, K*d_out)} + if self.residual: + for ntype in out_feats: + alpha = torch.sigmoid(self.res_weight[ntype]) + inherit_feat = self.res_fc[ntype](feats_dst[ntype]) + out_feats[ntype] = alpha * out_feats[ntype] + (1 - alpha) * inherit_feat + return out_feats + + +class HGConv(nn.Module): + + def __init__( + self, in_dims, hidden_dim, out_dim, num_heads, ntypes, etypes, predict_ntype, + num_layers, dropout=0.0, residual=True): + """HGConv模型 + + :param in_dims: Dict[str, int] 顶点类型到输入特征维数的映射 + :param hidden_dim: int 隐含特征维数 + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param ntypes: List[str] 顶点类型列表 + :param etypes: List[(str, str, str)] 规范边类型列表 + :param predict_ntype: str 待预测顶点类型 + :param num_layers: int 层数 + :param dropout: float, optional Dropout概率,默认为0 + :param residual: bool, optional 是否使用残差连接,默认True + """ + super().__init__() + self.d = num_heads * hidden_dim + self.predict_ntype = predict_ntype + # 对齐输入特征维数 + self.fc_in = nn.ModuleDict({ + ntype: nn.Linear(in_dim, num_heads * hidden_dim) for ntype, in_dim in in_dims.items() + }) + self.layers = nn.ModuleList([ + HGConvLayer( + num_heads * hidden_dim, hidden_dim, num_heads, ntypes, etypes, dropout, residual + ) for _ in range(num_layers) + ]) + self.classifier = nn.Linear(num_heads * hidden_dim, out_dim) + + def forward(self, blocks, feats): + """ + :param blocks: List[DGLBlock] + :param feats: Dict[str, tensor(N_i, d_in_i)] 顶点类型到输入顶点特征的映射 + :return: tensor(N_i, d_out) 待预测顶点的最终嵌入 + """ + feats = {ntype: self.fc_in[ntype](feat) for ntype, feat in feats.items()} + for i in range(len(self.layers)): + feats = self.layers[i](blocks[i], feats) # {ntype: tensor(N_i, K*d_hid)} + return self.classifier(feats[self.predict_ntype]) + + @torch.no_grad() + def inference(self, g, feats, device, batch_size): + """离线推断所有顶点的最终嵌入(不使用邻居采样) + + :param g: DGLGraph 异构图 + :param feats: Dict[str, tensor(N_i, d_in_i)] 顶点类型到输入顶点特征的映射 + :param device: torch.device + :param batch_size: int 批大小 + :return: tensor(N_i, d_out) 待预测顶点的最终嵌入 + """ + g.ndata['emb'] = {ntype: self.fc_in[ntype](feat) for ntype, feat in feats.items()} + for layer in self.layers: + embeds = { + ntype: torch.zeros(g.num_nodes(ntype), self.d, device=device) + for ntype in g.ntypes + } + sampler = MultiLayerFullNeighborSampler(1) + loader = NodeDataLoader( + g, {ntype: g.nodes(ntype) for ntype in g.ntypes}, sampler, device=device, + batch_size=batch_size, shuffle=True + ) + for input_nodes, output_nodes, blocks in tqdm(loader): + block = blocks[0] + h = layer(block, block.srcdata['emb']) + for ntype in h: + embeds[ntype][output_nodes[ntype]] = h[ntype] + g.ndata['emb'] = embeds + return self.classifier(g.nodes[self.predict_ntype].data['emb']) + + +class HGConvFull(HGConv): + + def forward(self, g, feats): + return super().forward([g] * len(self.layers), feats) diff --git a/gnnrec/hge/hgconv/train.py b/gnnrec/hge/hgconv/train.py new file mode 100644 index 0000000..8c2f61b --- /dev/null +++ b/gnnrec/hge/hgconv/train.py @@ -0,0 +1,83 @@ +import argparse +import warnings + +import torch +import torch.nn.functional as F +import torch.optim as optim +from dgl.dataloading import MultiLayerNeighborSampler, NodeDataLoader +from tqdm import tqdm + +from gnnrec.hge.hgconv.model import HGConv +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, evaluate, \ + calc_metrics, METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, evaluator = \ + load_data(args.dataset, device) + add_node_feat(g, args.node_feat, args.node_embed_path) + + sampler = MultiLayerNeighborSampler([args.neighbor_size] * args.num_layers) + train_loader = NodeDataLoader(g, {predict_ntype: train_idx}, sampler, device=device, batch_size=args.batch_size) + loader = NodeDataLoader(g, {predict_ntype: g.nodes(predict_ntype)}, sampler, device=device, batch_size=args.batch_size) + + model = HGConv( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_heads, g.ntypes, g.canonical_etypes, + predict_ntype, args.num_layers, args.dropout, args.residual + ).to(device) + optimizer = optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay) + warnings.filterwarnings('ignore', 'Setting attributes on ParameterDict is not supported') + for epoch in range(args.epochs): + model.train() + losses = [] + for input_nodes, output_nodes, blocks in tqdm(train_loader): + batch_logits = model(blocks, blocks[0].srcdata['feat']) + batch_labels = labels[output_nodes[predict_ntype]] + loss = F.cross_entropy(batch_logits, batch_labels) + losses.append(loss.item()) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + torch.cuda.empty_cache() + print('Epoch {:d} | Loss {:.4f}'.format(epoch, sum(losses) / len(losses))) + if epoch % args.eval_every == 0 or epoch == args.epochs - 1: + print(METRICS_STR.format(*evaluate( + model, loader, g, labels, data.num_classes, predict_ntype, + train_idx, val_idx, test_idx, evaluator + ))) + embeds = model.inference(g, g.ndata['feat'], device, args.batch_size) + print(METRICS_STR.format(*calc_metrics(embeds, labels, train_idx, val_idx, test_idx, evaluator))) + + +def main(): + parser = argparse.ArgumentParser(description='训练HGConv模型') + parser.add_argument('--seed', type=int, default=8, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['ogbn-mag', 'oag-venue'], default='ogbn-mag', help='数据集') + parser.add_argument( + '--node-feat', choices=['average', 'pretrained'], default='pretrained', + help='如何获取无特征顶点的输入特征' + ) + parser.add_argument('--node-embed-path', help='预训练顶点嵌入路径') + parser.add_argument('--num-hidden', type=int, default=32, help='隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--no-residual', action='store_false', help='不使用残差连接', dest='residual') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--epochs', type=int, default=100, help='训练epoch数') + parser.add_argument('--batch-size', type=int, default=4096, help='批大小') + parser.add_argument('--neighbor-size', type=int, default=10, help='邻居采样数') + parser.add_argument('--lr', type=float, default=0.001, help='学习率') + parser.add_argument('--weight-decay', type=float, default=0.0, help='权重衰减') + parser.add_argument('--eval-every', type=int, default=10, help='每多少个epoch计算一次准确率') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/hgconv/train_full.py b/gnnrec/hge/hgconv/train_full.py new file mode 100644 index 0000000..d2d1f19 --- /dev/null +++ b/gnnrec/hge/hgconv/train_full.py @@ -0,0 +1,59 @@ +import argparse +import warnings + +import torch +import torch.nn.functional as F +import torch.optim as optim + +from gnnrec.hge.hgconv.model import HGConvFull +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, evaluate_full, \ + METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, _ = \ + load_data(args.dataset, device) + add_node_feat(g, 'one-hot') + + model = HGConvFull( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_heads, g.ntypes, g.canonical_etypes, + predict_ntype, args.num_layers, args.dropout, args.residual + ).to(device) + optimizer = optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay) + warnings.filterwarnings('ignore', 'Setting attributes on ParameterDict is not supported') + for epoch in range(args.epochs): + model.train() + logits = model(g, g.ndata['feat']) + loss = F.cross_entropy(logits[train_idx], labels[train_idx]) + optimizer.zero_grad() + loss.backward() + optimizer.step() + torch.cuda.empty_cache() + print(('Epoch {:d} | Loss {:.4f} | ' + METRICS_STR).format( + epoch, loss.item(), *evaluate_full(model, g, labels, train_idx, val_idx, test_idx) + )) + + +def main(): + parser = argparse.ArgumentParser(description='训练HGConv模型(full-batch)') + parser.add_argument('--seed', type=int, default=8, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['acm', 'dblp'], default='acm', help='数据集') + parser.add_argument('--num-hidden', type=int, default=32, help='隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--no-residual', action='store_false', help='不使用残差连接', dest='residual') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--epochs', type=int, default=10, help='训练epoch数') + parser.add_argument('--lr', type=float, default=0.001, help='学习率') + parser.add_argument('--weight-decay', type=float, default=0.0, help='权重衰减') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/hgt/__init__.py b/gnnrec/hge/hgt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/hgt/model.py b/gnnrec/hge/hgt/model.py new file mode 100644 index 0000000..c1bcf61 --- /dev/null +++ b/gnnrec/hge/hgt/model.py @@ -0,0 +1,180 @@ +import math + +import dgl.function as fn +import torch +import torch.nn as nn +import torch.nn.functional as F +from dgl.nn import HeteroGraphConv +from dgl.ops import edge_softmax +from dgl.utils import expand_as_pair + + +class HGTAttention(nn.Module): + + def __init__(self, out_dim, num_heads, k_linear, q_linear, v_linear, w_att, w_msg, mu): + """HGT注意力模块 + + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param k_linear: nn.Linear(d_in, d_out) + :param q_linear: nn.Linear(d_in, d_out) + :param v_linear: nn.Linear(d_in, d_out) + :param w_att: tensor(K, d_out/K, d_out/K) + :param w_msg: tensor(K, d_out/K, d_out/K) + :param mu: tensor(1) + """ + super().__init__() + self.out_dim = out_dim + self.num_heads = num_heads + self.d_k = out_dim // num_heads + self.k_linear = k_linear + self.q_linear = q_linear + self.v_linear = v_linear + self.w_att = w_att + self.w_msg = w_msg + self.mu = mu + + def forward(self, g, feat): + """ + :param g: DGLGraph 二分图(只包含一种关系) + :param feat: tensor(N_src, d_in) or (tensor(N_src, d_in), tensor(N_dst, d_in)) 输入特征 + :return: tensor(N_dst, d_out) 目标顶点该关于关系的表示 + """ + with g.local_scope(): + feat_src, feat_dst = expand_as_pair(feat, g) + # (N_src, d_in) -> (N_src, d_out) -> (N_src, K, d_out/K) + k = self.k_linear(feat_src).view(-1, self.num_heads, self.d_k) + v = self.v_linear(feat_src).view(-1, self.num_heads, self.d_k) + q = self.q_linear(feat_dst).view(-1, self.num_heads, self.d_k) + + # k[:, h] @= w_att[h] => k[n, h, j] = ∑(i) k[n, h, i] * w_att[h, i, j] + k = torch.einsum('nhi,hij->nhj', k, self.w_att) + v = torch.einsum('nhi,hij->nhj', v, self.w_msg) + + g.srcdata.update({'k': k, 'v': v}) + g.dstdata['q'] = q + g.apply_edges(fn.v_dot_u('q', 'k', 't')) # g.edata['t']: (E, K, 1) + attn = g.edata.pop('t').squeeze(dim=-1) * self.mu / math.sqrt(self.d_k) + attn = edge_softmax(g, attn) # (E, K) + self.attn = attn.detach() + g.edata['t'] = attn.unsqueeze(dim=-1) # (E, K, 1) + + g.update_all(fn.u_mul_e('v', 't', 'm'), fn.sum('m', 'h')) + out = g.dstdata['h'].view(-1, self.out_dim) # (N_dst, d_out) + return out + + +class HGTLayer(nn.Module): + + def __init__(self, in_dim, out_dim, num_heads, ntypes, etypes, dropout=0.2, use_norm=True): + """HGT层 + + :param in_dim: int 输入特征维数 + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param ntypes: List[str] 顶点类型列表 + :param etypes: List[(str, str, str)] 规范边类型列表 + :param dropout: dropout: float, optional Dropout概率,默认为0.2 + :param use_norm: bool, optional 是否使用层归一化,默认为True + """ + super().__init__() + d_k = out_dim // num_heads + k_linear = {ntype: nn.Linear(in_dim, out_dim) for ntype in ntypes} + q_linear = {ntype: nn.Linear(in_dim, out_dim) for ntype in ntypes} + v_linear = {ntype: nn.Linear(in_dim, out_dim) for ntype in ntypes} + w_att = {r[1]: nn.Parameter(torch.Tensor(num_heads, d_k, d_k)) for r in etypes} + w_msg = {r[1]: nn.Parameter(torch.Tensor(num_heads, d_k, d_k)) for r in etypes} + mu = {r[1]: nn.Parameter(torch.ones(num_heads)) for r in etypes} + self.reset_parameters(w_att, w_msg) + self.conv = HeteroGraphConv({ + etype: HGTAttention( + out_dim, num_heads, k_linear[stype], q_linear[dtype], v_linear[stype], + w_att[etype], w_msg[etype], mu[etype] + ) for stype, etype, dtype in etypes + }, 'mean') + + self.a_linear = nn.ModuleDict({ntype: nn.Linear(out_dim, out_dim) for ntype in ntypes}) + self.skip = nn.ParameterDict({ntype: nn.Parameter(torch.ones(1)) for ntype in ntypes}) + self.drop = nn.Dropout(dropout) + + self.use_norm = use_norm + if use_norm: + self.norms = nn.ModuleDict({ntype: nn.LayerNorm(out_dim) for ntype in ntypes}) + + def reset_parameters(self, w_att, w_msg): + for etype in w_att: + nn.init.xavier_uniform_(w_att[etype]) + nn.init.xavier_uniform_(w_msg[etype]) + + def forward(self, g, feats): + """ + :param g: DGLGraph 异构图 + :param feats: Dict[str, tensor(N_i, d_in)] 顶点类型到输入顶点特征的映射 + :return: Dict[str, tensor(N_i, d_out)] 顶点类型到输出特征的映射 + """ + if g.is_block: + feats_dst = {ntype: feats[ntype][:g.num_dst_nodes(ntype)] for ntype in feats} + else: + feats_dst = feats + with g.local_scope(): + # 第1步:异构互注意力+异构消息传递+目标相关的聚集 + hs = self.conv(g, feats) # {ntype: tensor(N_i, d_out)} + + # 第2步:残差连接 + out_feats = {} + for ntype in g.dsttypes: + if g.num_dst_nodes(ntype) == 0: + continue + alpha = torch.sigmoid(self.skip[ntype]) + trans_out = self.drop(self.a_linear[ntype](hs[ntype])) + out = alpha * trans_out + (1 - alpha) * feats_dst[ntype] + out_feats[ntype] = self.norms[ntype](out) if self.use_norm else out + return out_feats + + +class HGT(nn.Module): + + def __init__( + self, in_dims, hidden_dim, out_dim, num_heads, ntypes, etypes, + predict_ntype, num_layers, dropout=0.2, use_norm=True): + """HGT模型 + + :param in_dims: Dict[str, int] 顶点类型到输入特征维数的映射 + :param hidden_dim: int 隐含特征维数 + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param ntypes: List[str] 顶点类型列表 + :param etypes: List[(str, str, str)] 规范边类型列表 + :param predict_ntype: str 待预测顶点类型 + :param num_layers: int 层数 + :param dropout: dropout: float, optional Dropout概率,默认为0.2 + :param use_norm: bool, optional 是否使用层归一化,默认为True + """ + super().__init__() + self.predict_ntype = predict_ntype + self.adapt_fcs = nn.ModuleDict({ + ntype: nn.Linear(in_dim, hidden_dim) for ntype, in_dim in in_dims.items() + }) + self.layers = nn.ModuleList([ + HGTLayer(hidden_dim, hidden_dim, num_heads, ntypes, etypes, dropout, use_norm) + for _ in range(num_layers) + ]) + self.predict = nn.Linear(hidden_dim, out_dim) + + def forward(self, blocks, feats): + """ + :param blocks: List[DGLBlock] + :param feats: Dict[str, tensor(N_i, d_in)] 顶点类型到输入顶点特征的映射 + :return: tensor(N_i, d_out) 待预测顶点的最终嵌入 + """ + hs = {ntype: F.gelu(self.adapt_fcs[ntype](feats[ntype])) for ntype in feats} + for i in range(len(self.layers)): + hs = self.layers[i](blocks[i], hs) # {ntype: tensor(N_i, d_hid)} + out = self.predict(hs[self.predict_ntype]) # tensor(N_i, d_out) + return out + + +class HGTFull(HGT): + + def forward(self, g, feats): + return super().forward([g] * len(self.layers), feats) diff --git a/gnnrec/hge/hgt/train.py b/gnnrec/hge/hgt/train.py new file mode 100644 index 0000000..57df5b7 --- /dev/null +++ b/gnnrec/hge/hgt/train.py @@ -0,0 +1,88 @@ +import argparse +import warnings + +import torch +import torch.nn.functional as F +import torch.optim as optim +from dgl.dataloading import MultiLayerNeighborSampler, NodeDataLoader +from tqdm import tqdm + +from gnnrec.hge.hgt.model import HGT +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, evaluate, \ + METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, evaluator = \ + load_data(args.dataset, device) + add_node_feat(g, args.node_feat, args.node_embed_path) + + sampler = MultiLayerNeighborSampler([args.neighbor_size] * args.num_layers) + train_loader = NodeDataLoader(g, {predict_ntype: train_idx}, sampler, device=device, batch_size=args.batch_size) + loader = NodeDataLoader(g, {predict_ntype: g.nodes(predict_ntype)}, sampler, device=device, batch_size=args.batch_size) + + model = HGT( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_heads, g.ntypes, g.canonical_etypes, + predict_ntype, args.num_layers, args.dropout + ).to(device) + optimizer = optim.AdamW(model.parameters(), eps=1e-6) + scheduler = optim.lr_scheduler.OneCycleLR( + optimizer, args.max_lr, epochs=args.epochs, steps_per_epoch=len(train_loader), + pct_start=0.05, anneal_strategy='linear', final_div_factor=10.0 + ) + warnings.filterwarnings('ignore', 'Setting attributes on ParameterDict is not supported') + for epoch in range(args.epochs): + model.train() + losses = [] + for input_nodes, output_nodes, blocks in tqdm(train_loader): + batch_logits = model(blocks, blocks[0].srcdata['feat']) + batch_labels = labels[output_nodes[predict_ntype]] + loss = F.cross_entropy(batch_logits, batch_labels) + losses.append(loss.item()) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + torch.cuda.empty_cache() + print('Epoch {:d} | Loss {:.4f}'.format(epoch, sum(losses) / len(losses))) + if epoch % args.eval_every == 0 or epoch == args.epochs - 1: + print(METRICS_STR.format(*evaluate( + model, loader, g, labels, data.num_classes, predict_ntype, + train_idx, val_idx, test_idx, evaluator + ))) + if args.save_path: + torch.save(model.cpu().state_dict(), args.save_path) + print('模型已保存到', args.save_path) + + +def main(): + parser = argparse.ArgumentParser(description='训练HGT模型') + parser.add_argument('--seed', type=int, default=1, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['ogbn-mag', 'oag-cs-venue'], default='ogbn-mag', help='数据集') + parser.add_argument( + '--node-feat', choices=['average', 'pretrained'], default='pretrained', + help='如何获取无特征顶点的输入特征' + ) + parser.add_argument('--node-embed-path', help='预训练顶点嵌入路径') + parser.add_argument('--num-hidden', type=int, default=512, help='隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--epochs', type=int, default=100, help='训练epoch数') + parser.add_argument('--batch-size', type=int, default=2048, help='批大小') + parser.add_argument('--neighbor-size', type=int, default=10, help='邻居采样数') + parser.add_argument('--max-lr', type=float, default=5e-4, help='学习率上界') + parser.add_argument('--eval-every', type=int, default=10, help='每多少个epoch计算一次准确率') + parser.add_argument('--save-path', help='模型保存路径') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/hgt/train_full.py b/gnnrec/hge/hgt/train_full.py new file mode 100644 index 0000000..06be8bf --- /dev/null +++ b/gnnrec/hge/hgt/train_full.py @@ -0,0 +1,66 @@ +import argparse +import warnings + +import torch +import torch.nn.functional as F +import torch.optim as optim + +from gnnrec.hge.hgt.model import HGTFull +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, evaluate_full, \ + METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, _ = \ + load_data(args.dataset, device) + add_node_feat(g, 'one-hot') + + model = HGTFull( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_heads, g.ntypes, g.canonical_etypes, + predict_ntype, args.num_layers, args.dropout + ).to(device) + optimizer = optim.AdamW(model.parameters(), eps=1e-6) + scheduler = optim.lr_scheduler.OneCycleLR( + optimizer, args.max_lr, epochs=args.epochs, steps_per_epoch=1, + pct_start=0.05, anneal_strategy='linear', final_div_factor=10.0 + ) + warnings.filterwarnings('ignore', 'Setting attributes on ParameterDict is not supported') + for epoch in range(args.epochs): + model.train() + logits = model(g, g.ndata['feat']) + loss = F.cross_entropy(logits[train_idx], labels[train_idx]) + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + torch.cuda.empty_cache() + print(('Epoch {:d} | Loss {:.4f} | ' + METRICS_STR).format( + epoch, loss.item(), *evaluate_full(model, g, labels, train_idx, val_idx, test_idx) + )) + if args.save_path: + torch.save(model.cpu().state_dict(), args.save_path) + print('模型已保存到', args.save_path) + + +def main(): + parser = argparse.ArgumentParser(description='训练HGT模型(full-batch)') + parser.add_argument('--seed', type=int, default=1, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['acm', 'dblp'], default='acm', help='数据集') + parser.add_argument('--num-hidden', type=int, default=512, help='隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--epochs', type=int, default=10, help='训练epoch数') + parser.add_argument('--max-lr', type=float, default=5e-4, help='学习率上界') + parser.add_argument('--save-path', help='模型保存路径') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/metapath2vec/__init__.py b/gnnrec/hge/metapath2vec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/metapath2vec/random_walk.py b/gnnrec/hge/metapath2vec/random_walk.py new file mode 100644 index 0000000..eeca0ed --- /dev/null +++ b/gnnrec/hge/metapath2vec/random_walk.py @@ -0,0 +1,56 @@ +import argparse + +import dgl +import torch +from ogb.nodeproppred import DglNodePropPredDataset +from torch.utils.data import DataLoader +from tqdm import tqdm + +from gnnrec.config import DATA_DIR +from gnnrec.hge.utils import add_reverse_edges + + +def random_walk(g, metapaths, num_walks, walk_length, output_file): + """在异构图上按指定的元路径随机游走,将轨迹保存到指定文件 + + :param g: DGLGraph 异构图 + :param metapaths: Dict[str, List[str]] 起点类型到元路径的映射,元路径表示为边类型列表,起点和终点类型应该相同 + :param num_walks: int 每个顶点的游走次数 + :param walk_length: int 元路径重复次数 + :param output_file: str 输出文件名 + :return: + """ + with open(output_file, 'w') as f: + for ntype, metapath in metapaths.items(): + print(ntype) + loader = DataLoader(torch.arange(g.num_nodes(ntype)), batch_size=200) + for b in tqdm(loader): + nodes = torch.repeat_interleave(b, num_walks) + traces, types = dgl.sampling.random_walk(g, nodes, metapath=metapath * walk_length) + f.writelines([trace2name(g, trace, types) + '\n' for trace in traces]) + + +def trace2name(g, trace, types): + return ' '.join(g.ntypes[t] + '_' + str(int(n)) for n, t in zip(trace, types) if int(n) >= 0) + + +def main(): + parser = argparse.ArgumentParser(description='ogbn-mag数据集 metapath2vec基于元路径的随机游走') + parser.add_argument('--num-walks', type=int, default=5, help='每个顶点游走次数') + parser.add_argument('--walk-length', type=int, default=16, help='元路径重复次数') + parser.add_argument('output_file', help='输出文件名') + args = parser.parse_args() + + data = DglNodePropPredDataset('ogbn-mag', DATA_DIR) + g = add_reverse_edges(data[0][0]) + metapaths = { + 'author': ['writes', 'has_topic', 'has_topic_rev', 'writes_rev'], # APFPA + 'paper': ['writes_rev', 'writes', 'has_topic', 'has_topic_rev'], # PAPFP + 'field_of_study': ['has_topic_rev', 'writes_rev', 'writes', 'has_topic'], # FPAPF + 'institution': ['affiliated_with_rev', 'writes', 'writes_rev', 'affiliated_with'] # IAPAI + } + random_walk(g, metapaths, args.num_walks, args.walk_length, args.output_file) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/metapath2vec/train_word2vec.py b/gnnrec/hge/metapath2vec/train_word2vec.py new file mode 100644 index 0000000..2814f75 --- /dev/null +++ b/gnnrec/hge/metapath2vec/train_word2vec.py @@ -0,0 +1,27 @@ +import argparse +import logging + +from gensim.models import Word2Vec + +logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) + + +def main(): + parser = argparse.ArgumentParser(description='metapath2vec训练word2vec') + parser.add_argument('--size', type=int, default=128, help='词向量维数') + parser.add_argument('--workers', type=int, default=3, help='工作线程数') + parser.add_argument('--iter', type=int, default=10, help='迭代次数') + parser.add_argument('corpus_file', help='语料库文件路径') + parser.add_argument('save_path', help='保存word2vec模型文件名') + args = parser.parse_args() + print(args) + + model = Word2Vec( + corpus_file=args.corpus_file, size=args.size, min_count=1, + workers=args.workers, sg=1, iter=args.iter + ) + model.save(args.save_path) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/readme.md b/gnnrec/hge/readme.md new file mode 100644 index 0000000..128c04d --- /dev/null +++ b/gnnrec/hge/readme.md @@ -0,0 +1,129 @@ +# 异构图表示学习 +## 数据集 +* [ACM](https://github.com/liun-online/HeCo/tree/main/data/acm) - ACM学术网络数据集 +* [DBLP](https://github.com/liun-online/HeCo/tree/main/data/dblp) - DBLP学术网络数据集 +* [ogbn-mag](https://ogb.stanford.edu/docs/nodeprop/#ogbn-mag) - OGB提供的微软学术数据集 +* [oag-venue](../kgrec/data/venue.py) - oag-cs期刊分类数据集 + +| 数据集 | 顶点数 | 边数 | 目标顶点 | 类别数 | +| --- | --- | --- | --- | --- | +| ACM | 11246 | 34852 | paper | 3 | +| DBLP | 26128 | 239566 | author | 4 | +| ogbn-mag | 1939743 | 21111007 | paper | 349 | +| oag-venue | 4235169 | 34520417 | paper | 360 | + +## Baselines +* [R-GCN](https://arxiv.org/pdf/1703.06103) +* [HGT](https://arxiv.org/pdf/2003.01332) +* [HGConv](https://arxiv.org/pdf/2012.14722) +* [R-HGNN](https://arxiv.org/pdf/2105.11122) +* [C&S](https://arxiv.org/pdf/2010.13993) +* [HeCo](https://arxiv.org/pdf/2105.09111) + +### R-GCN (full batch) +```shell +python -m gnnrec.hge.rgcn.train --dataset=acm --epochs=10 +python -m gnnrec.hge.rgcn.train --dataset=dblp --epochs=10 +python -m gnnrec.hge.rgcn.train --dataset=ogbn-mag --num-hidden=48 +python -m gnnrec.hge.rgcn.train --dataset=oag-venue --num-hidden=48 --epochs=30 +``` +(使用minibatch训练准确率就是只有20%多,不知道为什么) + +### 预训练顶点嵌入 +使用metapath2vec(随机游走+word2vec)预训练顶点嵌入,作为GNN模型的顶点输入特征 +```shell +python -m gnnrec.hge.metapath2vec.random_walk model/word2vec/ogbn-mag_corpus.txt +python -m gnnrec.hge.metapath2vec.train_word2vec --size=128 --workers=8 model/word2vec/ogbn-mag_corpus.txt model/word2vec/ogbn-mag.model +``` + +### HGT +```shell +python -m gnnrec.hge.hgt.train_full --dataset=acm +python -m gnnrec.hge.hgt.train_full --dataset=dblp +python -m gnnrec.hge.hgt.train --dataset=ogbn-mag --node-embed-path=model/word2vec/ogbn-mag.model --epochs=40 +python -m gnnrec.hge.hgt.train --dataset=oag-venue --node-embed-path=model/word2vec/oag-cs.model --epochs=40 +``` + +### HGConv +```shell +python -m gnnrec.hge.hgconv.train_full --dataset=acm --epochs=5 +python -m gnnrec.hge.hgconv.train_full --dataset=dblp --epochs=20 +python -m gnnrec.hge.hgconv.train --dataset=ogbn-mag --node-embed-path=model/word2vec/ogbn-mag.model +python -m gnnrec.hge.hgconv.train --dataset=oag-venue --node-embed-path=model/word2vec/oag-cs.model +``` + +### R-HGNN +```shell +python -m gnnrec.hge.rhgnn.train_full --dataset=acm --num-layers=1 --epochs=15 +python -m gnnrec.hge.rhgnn.train_full --dataset=dblp --epochs=20 +python -m gnnrec.hge.rhgnn.train --dataset=ogbn-mag model/word2vec/ogbn-mag.model +python -m gnnrec.hge.rhgnn.train --dataset=oag-venue --epochs=50 model/word2vec/oag-cs.model +``` + +### C&S +```shell +python -m gnnrec.hge.cs.train --dataset=acm --epochs=5 +python -m gnnrec.hge.cs.train --dataset=dblp --epochs=5 +python -m gnnrec.hge.cs.train --dataset=ogbn-mag --prop-graph=data/graph/pos_graph_ogbn-mag_t5.bin +python -m gnnrec.hge.cs.train --dataset=oag-venue --prop-graph=data/graph/pos_graph_oag-venue_t5.bin +``` + +### HeCo +```shell +python -m gnnrec.hge.heco.train --dataset=ogbn-mag model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5.bin +python -m gnnrec.hge.heco.train --dataset=oag-venue model/word2vec/oag-cs.model data/graph/pos_graph_oag-venue_t5.bin +``` +(ACM和DBLP的数据来自 https://github.com/ZZy979/pytorch-tutorial/tree/master/gnn/heco ,准确率和Micro-F1相等) + +## RHCO +基于对比学习的关系感知异构图神经网络(Relation-aware Heterogeneous Graph Neural Network with Contrastive Learning, RHCO) + +在HeCo的基础上改进: +* 网络结构编码器中的注意力向量改为关系的表示(类似于R-HGNN) +* 正样本选择方式由元路径条数改为预训练的HGT计算的注意力权重、训练集使用真实标签 +* 元路径视图编码器改为正样本图编码器,适配mini-batch训练 +* Loss增加分类损失,训练方式由无监督改为半监督 +* 在最后增加C&S后处理步骤 + +ACM +```shell +python -m gnnrec.hge.hgt.train_full --dataset=acm --save-path=model/hgt/hgt_acm.pt +python -m gnnrec.hge.rhco.build_pos_graph_full --dataset=acm --num-samples=5 --use-label model/hgt/hgt_acm.pt data/graph/pos_graph_acm_t5l.bin +python -m gnnrec.hge.rhco.train_full --dataset=acm data/graph/pos_graph_acm_t5l.bin +``` + +DBLP +```shell +python -m gnnrec.hge.hgt.train_full --dataset=dblp --save-path=model/hgt/hgt_dblp.pt +python -m gnnrec.hge.rhco.build_pos_graph_full --dataset=dblp --num-samples=5 --use-label model/hgt/hgt_dblp.pt data/graph/pos_graph_dblp_t5l.bin +python -m gnnrec.hge.rhco.train_full --dataset=dblp --use-data-pos data/graph/pos_graph_dblp_t5l.bin +``` + +ogbn-mag(第3步如果中断可使用--load-path参数继续训练) +```shell +python -m gnnrec.hge.hgt.train --dataset=ogbn-mag --node-embed-path=model/word2vec/ogbn-mag.model --epochs=40 --save-path=model/hgt/hgt_ogbn-mag.pt +python -m gnnrec.hge.rhco.build_pos_graph --dataset=ogbn-mag --num-samples=5 --use-label model/word2vec/ogbn-mag.model model/hgt/hgt_ogbn-mag.pt data/graph/pos_graph_ogbn-mag_t5l.bin +python -m gnnrec.hge.rhco.train --dataset=ogbn-mag --num-hidden=64 --contrast-weight=0.9 model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5l.bin model/rhco_ogbn-mag_d64_a0.9_t5l.pt +python -m gnnrec.hge.rhco.smooth --dataset=ogbn-mag model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5l.bin model/rhco_ogbn-mag_d64_a0.9_t5l.pt +``` + +oag-venue +```shell +python -m gnnrec.hge.hgt.train --dataset=oag-venue --node-embed-path=model/word2vec/oag-cs.model --epochs=40 --save-path=model/hgt/hgt_oag-venue.pt +python -m gnnrec.hge.rhco.build_pos_graph --dataset=oag-venue --num-samples=5 --use-label model/word2vec/oag-cs.model model/hgt/hgt_oag-venue.pt data/graph/pos_graph_oag-venue_t5l.bin +python -m gnnrec.hge.rhco.train --dataset=oag-venue --num-hidden=64 --contrast-weight=0.9 model/word2vec/oag-cs.model data/graph/pos_graph_oag-venue_t5l.bin model/rhco_oag-venue.pt +python -m gnnrec.hge.rhco.smooth --dataset=oag-venue model/word2vec/oag-cs.model data/graph/pos_graph_oag-venue_t5l.bin model/rhco_oag-venue.pt +``` + +消融实验 +```shell +python -m gnnrec.hge.rhco.train --dataset=ogbn-mag --model=RHCO_sc model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5l.bin model/rhco_sc_ogbn-mag.pt +python -m gnnrec.hge.rhco.train --dataset=ogbn-mag --model=RHCO_pg model/word2vec/ogbn-mag.model data/graph/pos_graph_ogbn-mag_t5l.bin model/rhco_pg_ogbn-mag.pt +``` + +## 实验结果 +[顶点分类](result/node_classification.csv) + +[参数敏感性分析](result/param_analysis.csv) + +[消融实验](result/ablation_study.csv) diff --git a/gnnrec/hge/result/__init__.py b/gnnrec/hge/result/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/result/ablation_study.csv b/gnnrec/hge/result/ablation_study.csv new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/result/node_classification.csv b/gnnrec/hge/result/node_classification.csv new file mode 100644 index 0000000..1b4d429 --- /dev/null +++ b/gnnrec/hge/result/node_classification.csv @@ -0,0 +1,9 @@ +数据集,评价指标,R-GCN,HGT,HGConv,R-HGNN,C&S,HeCo,RHCO +ACM,准确率,0.7750,0.7660,0.7550,0.7220,0.7740,0.8850,0.7330->0.8200 +ACM,Macro-F1,0.7556,0.7670,0.6978,0.6409,0.7780,0.8830,0.7050->0.8086 +DBLP,准确率,0.9490,0.7860,0.9060,0.8680,0.7970,0.9070,0.8020->0.8840 +DBLP,Macro-F1,0.9433,0.7837,0.8951,0.8591,0.7799,0.9032,0.7900->0.8732 +ogbn-mag,准确率,0.3720,0.4497,0.4851,0.5201,0.3558,0.3043,0.5215->0.5662 +ogbn-mag,Macro-F1,0.1970,0.2853,0.3148,0.3164,0.1863,0.0985,0.3105->0.3433 +oag-venue,准确率,0.1577,0.8359,0.8144,0.9615,0.1392,0.1361,0.9607->0.9623 +oag-venue,Macro-F1,0.1088,0.7628,0.7486,0.9057,0.0878,0.0681,0.8995->0.9186 diff --git a/gnnrec/hge/result/param_analysis.csv b/gnnrec/hge/result/param_analysis.csv new file mode 100644 index 0000000..51fdb7b --- /dev/null +++ b/gnnrec/hge/result/param_analysis.csv @@ -0,0 +1,6 @@ +alpha,Accuracy_alpha,Macro-F1_alpha,Train-time_alpha(h),Tpos,Accuracy_Tpos,Macro-F1_Tpos,Train-time_Tpos(h),dimension,Accuracy_dimension,Macro-F1_dimension,Train-time_dimension(h) +0,0.5564,0.3434,24.8,3,0.5417,0.3210,16.1,16,0.5229,0.2612,14.6 +0.2,0.5643,0.3440,24.8,5,0.5662,0.3433,24.8,32,0.5546,0.3169,17.9 +0.5,0.5571,0.3415,24.8,10,0.5392,0.3181,40.2,64,0.5662,0.3433,24.8 +0.8,0.5659,0.3371,24.8,,,,,128,0.5457,0.3389,55.4 +0.9,0.5662,0.3433,24.8,,,,,,,, \ No newline at end of file diff --git a/gnnrec/hge/result/plot.py b/gnnrec/hge/result/plot.py new file mode 100644 index 0000000..0280ccc --- /dev/null +++ b/gnnrec/hge/result/plot.py @@ -0,0 +1,34 @@ +import matplotlib.pyplot as plt +import pandas as pd + +from gnnrec.config import BASE_DIR + +RESULT_DIR = BASE_DIR / 'gnnrec/hge/result' + + +def plot_param_analysis(): + df = pd.read_csv(RESULT_DIR / 'param_analysis.csv') + params = ['alpha', 'Tpos', 'dimension'] + + for p in params: + fig, ax = plt.subplots() + x = df[p].dropna().to_numpy() + ax.plot(x, df[f'Accuracy_{p}'].dropna().to_numpy(), '.-', label='Accuracy') + ax.plot(x, df[f'Macro-F1_{p}'].dropna().to_numpy(), '*--', label='Macro-F1') + ax.set_xlabel(p) + ax.set_ylabel('Accuracy / Macro-F1') + + ax2 = ax.twinx() + ax2.plot(x, df[f'Train-time_{p}(h)'].dropna().to_numpy(), 'x-.', label='Train time') + ax2.set_ylabel('Train time(h)') + + fig.legend(loc='upper center') + fig.savefig(RESULT_DIR / f'param_analysis_{p}.png') + + +def main(): + plot_param_analysis() + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/rgcn/__init__.py b/gnnrec/hge/rgcn/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/rgcn/model.py b/gnnrec/hge/rgcn/model.py new file mode 100644 index 0000000..0713460 --- /dev/null +++ b/gnnrec/hge/rgcn/model.py @@ -0,0 +1,95 @@ +import torch.nn as nn +import torch.nn.functional as F +from dgl.nn import HeteroGraphConv, GraphConv + + +class RelGraphConv(nn.Module): + + def __init__(self, in_dim, out_dim, ntypes, etypes, activation=None, dropout=0.0): + """R-GCN层(用于异构图) + + :param in_dim: 输入特征维数 + :param out_dim: 输出特征维数 + :param ntypes: List[str] 顶点类型列表 + :param etypes: List[str] 边类型列表 + :param activation: callable, optional 激活函数,默认为None + :param dropout: float, optional Dropout概率,默认为0 + """ + super().__init__() + self.activation = activation + self.dropout = nn.Dropout(dropout) + + self.conv = HeteroGraphConv({ + etype: GraphConv(in_dim, out_dim, norm='right', bias=False) + for etype in etypes + }, 'sum') + self.loop_weight = nn.ModuleDict({ + ntype: nn.Linear(in_dim, out_dim, bias=False) for ntype in ntypes + }) + + def forward(self, g, feats): + """ + :param g: DGLGraph 异构图 + :param feats: Dict[str, tensor(N_i, d_in)] 顶点类型到输入特征的映射 + :return: Dict[str, tensor(N_i, d_out)] 顶点类型到输出特征的映射 + """ + if g.is_block: + feats_dst = {ntype: feat[:g.num_dst_nodes(ntype)] for ntype, feat in feats.items()} + else: + feats_dst = feats + out = self.conv(g, (feats, feats_dst)) # Dict[ntype, (N_i, d_out)] + for ntype in out: + out[ntype] += self.loop_weight[ntype](feats_dst[ntype]) + if self.activation: + out[ntype] = self.activation(out[ntype]) + out[ntype] = self.dropout(out[ntype]) + return out + + +class RGCN(nn.Module): + + def __init__( + self, in_dim, hidden_dim, out_dim, input_ntypes, num_nodes, etypes, predict_ntype, + num_layers=2, dropout=0.0): + """R-GCN模型 + + :param in_dim: int 输入特征维数 + :param hidden_dim: int 隐含特征维数 + :param out_dim: int 输出特征维数 + :param input_ntypes: List[str] 有输入特征的顶点类型列表 + :param num_nodes: Dict[str, int] 顶点类型到顶点数的映射 + :param etypes: List[str] 边类型列表 + :param predict_ntype: str 待预测顶点类型 + :param num_layers: int, optional 层数,默认为2 + :param dropout: float, optional Dropout概率,默认为0 + """ + super().__init__() + self.embeds = nn.ModuleDict({ + ntype: nn.Embedding(num_nodes[ntype], in_dim) + for ntype in num_nodes if ntype not in input_ntypes + }) + ntypes = list(num_nodes) + self.layers = nn.ModuleList() + self.layers.append(RelGraphConv(in_dim, hidden_dim, ntypes, etypes, F.relu, dropout)) + for i in range(num_layers - 2): + self.layers.append(RelGraphConv(hidden_dim, hidden_dim, ntypes, etypes, F.relu, dropout)) + self.layers.append(RelGraphConv(hidden_dim, out_dim, ntypes, etypes)) + self.predict_ntype = predict_ntype + self.reset_parameters() + + def reset_parameters(self): + gain = nn.init.calculate_gain('relu') + for k in self.embeds: + nn.init.xavier_uniform_(self.embeds[k].weight, gain=gain) + + def forward(self, g, feats): + """ + :param g: DGLGraph 异构图 + :param feats: Dict[str, tensor(N_i, d_in_i)] (部分)顶点类型到输入特征的映射 + :return: Dict[str, tensor(N_i, d_out)] 顶点类型到顶点嵌入的映射 + """ + for k in self.embeds: + feats[k] = self.embeds[k].weight + for i in range(len(self.layers)): + feats = self.layers[i](g, feats) # Dict[ntype, (N_i, d_hid)] + return feats[self.predict_ntype] diff --git a/gnnrec/hge/rgcn/train.py b/gnnrec/hge/rgcn/train.py new file mode 100644 index 0000000..c595661 --- /dev/null +++ b/gnnrec/hge/rgcn/train.py @@ -0,0 +1,60 @@ +import argparse + +import torch +import torch.nn.functional as F +import torch.optim as optim + +from gnnrec.hge.rgcn.model import RGCN +from gnnrec.hge.utils import set_random_seed, get_device, load_data, calc_metrics, METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, features, labels, predict_ntype, train_idx, val_idx, test_idx, evaluator = \ + load_data(args.dataset, device, reverse_self=False) + + model = RGCN( + features.shape[1], args.num_hidden, data.num_classes, [predict_ntype], + {ntype: g.num_nodes(ntype) for ntype in g.ntypes}, g.etypes, + predict_ntype, args.num_layers, args.dropout + ).to(device) + optimizer = optim.Adam(model.parameters(), lr=args.lr) + features = {predict_ntype: features} + for epoch in range(args.epochs): + model.train() + logits = model(g, features) + loss = F.cross_entropy(logits[train_idx], labels[train_idx]) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(('Epoch {:d} | Loss {:.4f} | ' + METRICS_STR).format( + epoch, loss.item(), + *evaluate(model, g, features, labels, train_idx, val_idx, test_idx, evaluator) + )) + + +@torch.no_grad() +def evaluate(model, g, features, labels, train_idx, val_idx, test_idx, evaluator): + model.eval() + logits = model(g, features) + return calc_metrics(logits, labels, train_idx, val_idx, test_idx, evaluator) + + +def main(): + parser = argparse.ArgumentParser(description='训练R-GCN模型') + parser.add_argument('--seed', type=int, default=8, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['acm', 'dblp', 'ogbn-mag', 'oag-venue'], default='ogbn-mag', help='数据集') + parser.add_argument('--num-hidden', type=int, default=32, help='隐藏层维数') + parser.add_argument('--num-layers', type=int, default=2, help='模型层数') + parser.add_argument('--dropout', type=float, default=0.8, help='Dropout概率') + parser.add_argument('--epochs', type=int, default=50, help='训练epoch数') + parser.add_argument('--lr', type=float, default=0.01, help='学习率') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/rhco/__init__.py b/gnnrec/hge/rhco/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/rhco/build_pos_graph.py b/gnnrec/hge/rhco/build_pos_graph.py new file mode 100644 index 0000000..9461eb4 --- /dev/null +++ b/gnnrec/hge/rhco/build_pos_graph.py @@ -0,0 +1,138 @@ +import argparse +import random +from collections import defaultdict + +import dgl +import torch +from dgl.dataloading import MultiLayerNeighborSampler, NodeDataLoader +from tqdm import tqdm + +from gnnrec.hge.hgt.model import HGT +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat + + +def main(): + args = parse_args() + print(args) + set_random_seed(args.seed) + device = get_device(args.device) + + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, _ = load_data(args.dataset) + g = g.to(device) + labels = labels.tolist() + train_idx = torch.cat([train_idx, val_idx]) + add_node_feat(g, 'pretrained', args.node_embed_path) + + label_neigh = sample_label_neighbors(labels, args.num_samples) # (N, T_pos) + # List[tensor(N, T_pos)] HGT计算出的注意力权重,M条元路径+一个总体 + attn_pos = calc_attn_pos(g, data.num_classes, predict_ntype, args.num_samples, device, args) + + # 元路径对应的正样本图 + v = torch.repeat_interleave(g.nodes(predict_ntype), args.num_samples).cpu() + pos_graphs = [] + for p in attn_pos[:-1]: + u = p.view(1, -1).squeeze(dim=0) # (N*T_pos,) + pos_graphs.append(dgl.graph((u, v))) + + # 整体正样本图 + pos = attn_pos[-1] + if args.use_label: + pos[train_idx] = label_neigh[train_idx] + # pos[test_idx, 0] = label_neigh[test_idx, 0] + u = pos.view(1, -1).squeeze(dim=0) + pos_graphs.append(dgl.graph((u, v))) + + dgl.save_graphs(args.save_graph_path, pos_graphs) + print('正样本图已保存到', args.save_graph_path) + + +def calc_attn_pos(g, num_classes, predict_ntype, num_samples, device, args): + """使用预训练的HGT模型计算的注意力权重选择目标顶点的正样本。""" + # 第1层只保留AB边,第2层只保留BA边,其中A是目标顶点类型,B是中间顶点类型 + num_neighbors = [{}, {}] + # 形如ABA的元路径,其中A是目标顶点类型 + metapaths = [] + rev_etype = { + e: next(re for rs, re, rd in g.canonical_etypes if rs == d and rd == s and re != e) + for s, e, d in g.canonical_etypes + } + for s, e, d in g.canonical_etypes: + if d == predict_ntype: + re = rev_etype[e] + num_neighbors[0][re] = num_neighbors[1][e] = 10 + metapaths.append((re, e)) + for i in range(len(num_neighbors)): + d = dict.fromkeys(g.etypes, 0) + d.update(num_neighbors[i]) + num_neighbors[i] = d + sampler = MultiLayerNeighborSampler(num_neighbors) + loader = NodeDataLoader( + g, {predict_ntype: g.nodes(predict_ntype)}, sampler, + device=device, batch_size=args.batch_size + ) + + model = HGT( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, num_classes, args.num_heads, g.ntypes, g.canonical_etypes, + predict_ntype, 2, args.dropout + ).to(device) + model.load_state_dict(torch.load(args.hgt_model_path, map_location=device)) + + # 每条元路径ABA对应一个正样本图G_ABA,加一个总体正样本图G_pos + pos = [ + torch.zeros(g.num_nodes(predict_ntype), num_samples, dtype=torch.long, device=device) + for _ in range(len(metapaths) + 1) + ] + with torch.no_grad(): + for input_nodes, output_nodes, blocks in tqdm(loader): + _ = model(blocks, blocks[0].srcdata['feat']) + # List[tensor(N_src, N_dst)] + attn = [calc_attn(mp, model, blocks, device).t() for mp in metapaths] + for i in range(len(attn)): + _, nid = torch.topk(attn[i], num_samples) # (N_dst, T_pos) + # nid是blocks[0]中的源顶点id,将其转换为原异构图中的顶点id + pos[i][output_nodes[predict_ntype]] = input_nodes[predict_ntype][nid] + _, nid = torch.topk(sum(attn), num_samples) + pos[-1][output_nodes[predict_ntype]] = input_nodes[predict_ntype][nid] + return [p.cpu() for p in pos] + + +def calc_attn(metapath, model, blocks, device): + """计算通过指定元路径与目标顶点连接的同类型顶点的注意力权重。""" + re, e = metapath + s, _, d = blocks[0].to_canonical_etype(re) # s是目标顶点类型, d是中间顶点类型 + a0 = torch.zeros(blocks[0].num_src_nodes(s), blocks[0].num_dst_nodes(d), device=device) + a0[blocks[0].edges(etype=re)] = model.layers[0].conv.mods[re].attn.mean(dim=1) + a1 = torch.zeros(blocks[1].num_src_nodes(d), blocks[1].num_dst_nodes(s), device=device) + a1[blocks[1].edges(etype=e)] = model.layers[1].conv.mods[e].attn.mean(dim=1) + return torch.matmul(a0, a1) # (N_src, N_dst) + + +def sample_label_neighbors(labels, num_samples): + """为每个顶点采样相同标签的邻居。""" + label2id = defaultdict(list) + for i, y in enumerate(labels): + label2id[y].append(i) + return torch.tensor([random.sample(label2id[y], num_samples) for y in labels]) + + +def parse_args(): + parser = argparse.ArgumentParser(description='使用预训练的HGT计算的注意力权重构造正样本图') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['ogbn-mag', 'oag-venue'], default='ogbn-mag', help='数据集') + parser.add_argument('--num-hidden', type=int, default=512, help='隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--batch-size', type=int, default=256, help='批大小') + parser.add_argument('--num-samples', type=int, default=5, help='每个顶点采样的正样本数量') + parser.add_argument('--use-label', action='store_true', help='训练集使用真实标签') + parser.add_argument('node_embed_path', help='预训练顶点嵌入路径') + parser.add_argument('hgt_model_path', help='预训练的HGT模型保存路径') + parser.add_argument('save_graph_path', help='正样本图保存路径') + args = parser.parse_args() + return args + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/rhco/build_pos_graph_full.py b/gnnrec/hge/rhco/build_pos_graph_full.py new file mode 100644 index 0000000..ab476b0 --- /dev/null +++ b/gnnrec/hge/rhco/build_pos_graph_full.py @@ -0,0 +1,107 @@ +import argparse +import random +from collections import defaultdict + +import dgl +import torch + +from gnnrec.hge.hgt.model import HGTFull +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat + + +def main(): + args = parse_args() + print(args) + set_random_seed(args.seed) + device = get_device(args.device) + + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, _ = load_data(args.dataset) + g = g.to(device) + labels = labels.tolist() + train_idx = torch.cat([train_idx, val_idx]) + add_node_feat(g, 'one-hot') + + label_neigh = sample_label_neighbors(labels, args.num_samples) # (N, T_pos) + # List[tensor(N, T_pos)] HGT计算出的注意力权重,M条元路径+一个总体 + attn_pos = calc_attn_pos(g, data.num_classes, predict_ntype, args.num_samples, device, args) + + # 元路径对应的正样本图 + v = torch.repeat_interleave(g.nodes(predict_ntype), args.num_samples).cpu() + pos_graphs = [] + for p in attn_pos[:-1]: + u = p.view(1, -1).squeeze(dim=0) # (N*T_pos,) + pos_graphs.append(dgl.graph((u, v))) + + # 整体正样本图 + pos = attn_pos[-1] + if args.use_label: + pos[train_idx] = label_neigh[train_idx] + u = pos.view(1, -1).squeeze(dim=0) + pos_graphs.append(dgl.graph((u, v))) + + dgl.save_graphs(args.save_graph_path, pos_graphs) + print('正样本图已保存到', args.save_graph_path) + + +def calc_attn_pos(g, num_classes, predict_ntype, num_samples, device, args): + """使用预训练的HGT模型计算的注意力权重选择目标顶点的正样本。""" + # 形如ABA的元路径,其中A是目标顶点类型 + metapaths = [] + for s, e, d in g.canonical_etypes: + if d == predict_ntype: + re = next(re for rs, re, rd in g.canonical_etypes if rs == d and rd == s) + metapaths.append((re, e)) + + model = HGTFull( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, num_classes, args.num_heads, g.ntypes, g.canonical_etypes, + predict_ntype, 2, args.dropout + ).to(device) + model.load_state_dict(torch.load(args.hgt_model_path, map_location=device)) + + # 每条元路径ABA对应一个正样本图G_ABA,加一个总体正样本图G_pos + with torch.no_grad(): + _ = model(g, g.ndata['feat']) + attn = [calc_attn(mp, model, g, device).t() for mp in metapaths] # List[tensor(N, N)] + pos = [torch.topk(a, num_samples)[1] for a in attn] # List[tensor(N, T_pos)] + pos.append(torch.topk(sum(attn), num_samples)[1]) + return [p.cpu() for p in pos] + + +def calc_attn(metapath, model, g, device): + """计算通过指定元路径与目标顶点连接的同类型顶点的注意力权重。""" + re, e = metapath + s, _, d = g.to_canonical_etype(re) # s是目标顶点类型, d是中间顶点类型 + a0 = torch.zeros(g.num_nodes(s), g.num_nodes(d), device=device) + a0[g.edges(etype=re)] = model.layers[0].conv.mods[re].attn.mean(dim=1) + a1 = torch.zeros(g.num_nodes(d), g.num_nodes(s), device=device) + a1[g.edges(etype=e)] = model.layers[1].conv.mods[e].attn.mean(dim=1) + return torch.matmul(a0, a1) # (N, N) + + +def sample_label_neighbors(labels, num_samples): + """为每个顶点采样相同标签的邻居。""" + label2id = defaultdict(list) + for i, y in enumerate(labels): + label2id[y].append(i) + return torch.tensor([random.sample(label2id[y], num_samples) for y in labels]) + + +def parse_args(): + parser = argparse.ArgumentParser(description='使用预训练的HGT计算的注意力权重构造正样本图(full-batch)') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['acm', 'dblp'], default='acm', help='数据集') + parser.add_argument('--num-hidden', type=int, default=512, help='隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--num-samples', type=int, default=5, help='每个顶点采样的正样本数量') + parser.add_argument('--use-label', action='store_true', help='训练集使用真实标签') + parser.add_argument('hgt_model_path', help='预训练的HGT模型保存路径') + parser.add_argument('save_graph_path', help='正样本图保存路径') + args = parser.parse_args() + return args + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/rhco/model.py b/gnnrec/hge/rhco/model.py new file mode 100644 index 0000000..8e2b2d7 --- /dev/null +++ b/gnnrec/hge/rhco/model.py @@ -0,0 +1,126 @@ +import torch +import torch.nn as nn +from dgl.dataloading import MultiLayerFullNeighborSampler, NodeDataLoader + +from ..heco.model import PositiveGraphEncoder, Contrast +from ..rhgnn.model import RHGNN + + +class RHCO(nn.Module): + + def __init__( + self, in_dims, hidden_dim, out_dim, rel_hidden_dim, num_heads, + ntypes, etypes, predict_ntype, num_layers, dropout, num_pos_graphs, tau, lambda_): + """基于对比学习的关系感知异构图神经网络RHCO + + :param in_dims: Dict[str, int] 顶点类型到输入特征维数的映射 + :param hidden_dim: int 隐含特征维数 + :param out_dim: int 输出特征维数 + :param rel_hidden_dim: int 关系隐含特征维数 + :param num_heads: int 注意力头数K + :param ntypes: List[str] 顶点类型列表 + :param etypes – List[(str, str, str)] 规范边类型列表 + :param predict_ntype: str 目标顶点类型 + :param num_layers: int 网络结构编码器层数 + :param dropout: float 输入特征dropout + :param num_pos_graphs: int 正样本图个数M + :param tau: float 温度参数τ + :param lambda_: float 0~1之间,网络结构视图损失的系数λ(元路径视图损失的系数为1-λ) + """ + super().__init__() + self.hidden_dim = hidden_dim + self.predict_ntype = predict_ntype + self.sc_encoder = RHGNN( + in_dims, hidden_dim, hidden_dim, rel_hidden_dim, rel_hidden_dim, num_heads, + ntypes, etypes, predict_ntype, num_layers, dropout + ) + self.pg_encoder = PositiveGraphEncoder( + num_pos_graphs, in_dims[predict_ntype], hidden_dim, dropout + ) + self.contrast = Contrast(hidden_dim, tau, lambda_) + self.predict = nn.Linear(hidden_dim, out_dim) + self.reset_parameters() + + def reset_parameters(self): + gain = nn.init.calculate_gain('relu') + nn.init.xavier_normal_(self.predict.weight, gain) + + def forward(self, blocks, feats, mgs, mg_feats, pos): + """ + :param blocks: List[DGLBlock] + :param feats: Dict[str, tensor(N_i, d_in)] 顶点类型到输入特征的映射 + :param mgs: List[DGLBlock] 正样本图,len(mgs)=元路径数量=目标顶点邻居类型数S≠模型层数 + :param mg_feats: List[tensor(N_pos_src, d_in)] 正样本图源顶点的输入特征 + :param pos: tensor(B, N) 布尔张量,每个顶点的正样本 + (B是batch大小,真正的目标顶点;N是B个目标顶点加上其正样本后的顶点数) + :return: float, tensor(B, d_out) 对比损失,目标顶点输出特征 + """ + z_sc = self.sc_encoder(blocks, feats) # (N, d_hid) + z_pg = self.pg_encoder(mgs, mg_feats) # (N, d_hid) + loss = self.contrast(z_sc, z_pg, pos) + return loss, self.predict(z_sc[:pos.shape[0]]) + + @torch.no_grad() + def get_embeds(self, g, batch_size, device): + """计算目标顶点的最终嵌入(z_sc) + + :param g: DGLGraph 异构图 + :param batch_size: int 批大小 + :param device torch.device GPU设备 + :return: tensor(N_tgt, d_out) 目标顶点的最终嵌入 + """ + sampler = MultiLayerFullNeighborSampler(len(self.sc_encoder.layers)) + loader = NodeDataLoader( + g, {self.predict_ntype: g.nodes(self.predict_ntype)}, sampler, + device=device, batch_size=batch_size + ) + embeds = torch.zeros(g.num_nodes(self.predict_ntype), self.hidden_dim, device=device) + for input_nodes, output_nodes, blocks in loader: + z_sc = self.sc_encoder(blocks, blocks[0].srcdata['feat']) + embeds[output_nodes[self.predict_ntype]] = z_sc + return self.predict(embeds) + + +class RHCOFull(RHCO): + """Full-batch RHCO""" + + def forward(self, g, feats, mgs, mg_feat, pos): + return super().forward( + [g] * len(self.sc_encoder.layers), feats, mgs, [mg_feat] * len(mgs), pos + ) + + @torch.no_grad() + def get_embeds(self, g, *args): + return self.predict(self.sc_encoder([g] * len(self.sc_encoder.layers), g.ndata['feat'])) + + +class RHCOsc(RHCO): + """RHCO消融实验变体:仅使用网络结构编码器""" + + def forward(self, blocks, feats, mgs, mg_feats, pos): + z_sc = self.sc_encoder(blocks, feats) # (N, d_hid) + loss = self.contrast(z_sc, z_sc, pos) + return loss, self.predict(z_sc[:pos.shape[0]]) + + +class RHCOpg(RHCO): + """RHCO消融实验变体:仅使用正样本图编码器""" + + def forward(self, blocks, feats, mgs, mg_feats, pos): + z_pg = self.pg_encoder(mgs, mg_feats) # (N, d_hid) + loss = self.contrast(z_pg, z_pg, pos) + return loss, self.predict(z_pg[:pos.shape[0]]) + + def get_embeds(self, mgs, feat, batch_size, device): + sampler = MultiLayerFullNeighborSampler(1) + mg_loaders = [ + NodeDataLoader(mg, mg.nodes(self.predict_ntype), sampler, device=device, batch_size=batch_size) + for mg in mgs + ] + embeds = torch.zeros(mgs[0].num_nodes(self.predict_ntype), self.hidden_dim, device=device) + for mg_blocks in zip(*mg_loaders): + output_nodes = mg_blocks[0][1] + mg_feats = [feat[i] for i, _, _ in mg_blocks] + mg_blocks = [b[0] for _, _, b in mg_blocks] + embeds[output_nodes] = self.pg_encoder(mg_blocks, mg_feats) + return self.predict(embeds) diff --git a/gnnrec/hge/rhco/smooth.py b/gnnrec/hge/rhco/smooth.py new file mode 100644 index 0000000..d5d021e --- /dev/null +++ b/gnnrec/hge/rhco/smooth.py @@ -0,0 +1,75 @@ +import argparse + +import dgl +import torch +import torch.nn.functional as F + +from gnnrec.hge.cs.model import LabelPropagation +from gnnrec.hge.rhco.model import RHCO +from gnnrec.hge.utils import get_device, load_data, add_node_feat, calc_metrics + + +def smooth(base_pred, g, labels, mask, args): + cs = LabelPropagation(args.num_smooth_layers, args.smooth_alpha, args.smooth_norm) + labels = F.one_hot(labels).float() + base_pred[mask] = labels[mask] + return cs(g, base_pred) + + +def main(): + args = parse_args() + print(args) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, evaluator = \ + load_data(args.dataset, device) + add_node_feat(g, 'pretrained', args.node_embed_path, True) + if args.dataset == 'oag-venue': + labels[labels == -1] = 0 + (*mgs, pos_g), _ = dgl.load_graphs(args.pos_graph_path) + pos_g = pos_g.to(device) + + model = RHCO( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_rel_hidden, args.num_heads, + g.ntypes, g.canonical_etypes, predict_ntype, args.num_layers, args.dropout, + len(mgs), args.tau, args.lambda_ + ).to(device) + model.load_state_dict(torch.load(args.model_path, map_location=device)) + model.eval() + + base_pred = model.get_embeds(g, args.neighbor_size, args.batch_size, device) + mask = torch.cat([train_idx, val_idx]) + logits = smooth(base_pred, pos_g, labels, mask, args) + _, _, test_acc, _, _, test_f1 = calc_metrics(logits, labels, train_idx, val_idx, test_idx, evaluator) + print('After smoothing: Test Acc {:.4f} | Test Macro-F1 {:.4f}'.format(test_acc, test_f1)) + + +def parse_args(): + parser = argparse.ArgumentParser(description='RHCO+C&S(仅Smooth步骤)') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['ogbn-mag', 'oag-venue'], default='ogbn-mag', help='数据集') + # RHCO + parser.add_argument('--num-hidden', type=int, default=64, help='隐藏层维数') + parser.add_argument('--num-rel-hidden', type=int, default=8, help='关系表示的隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--tau', type=float, default=0.8, help='温度参数') + parser.add_argument('--lambda', type=float, default=0.5, dest='lambda_', help='对比损失的平衡系数') + parser.add_argument('--batch-size', type=int, default=1024, help='批大小') + parser.add_argument('--neighbor-size', type=int, default=10, help='邻居采样数') + parser.add_argument('node_embed_path', help='预训练顶点嵌入路径') + parser.add_argument('pos_graph_path', help='正样本图保存路径') + parser.add_argument('model_path', help='预训练的模型保存路径') + # C&S + parser.add_argument('--num-smooth-layers', type=int, default=50, help='Smooth步骤传播层数') + parser.add_argument('--smooth-alpha', type=float, default=0.5, help='Smooth步骤α值') + parser.add_argument( + '--smooth-norm', choices=['left', 'right', 'both'], default='right', + help='Smooth步骤归一化方式' + ) + return parser.parse_args() + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/rhco/train.py b/gnnrec/hge/rhco/train.py new file mode 100644 index 0000000..2989ab8 --- /dev/null +++ b/gnnrec/hge/rhco/train.py @@ -0,0 +1,127 @@ +import argparse + +import dgl +import torch +import torch.nn.functional as F +import torch.optim as optim +from dgl.dataloading import NodeDataLoader +from torch.utils.data import DataLoader +from tqdm import tqdm + +from gnnrec.hge.heco.sampler import PositiveSampler +from gnnrec.hge.rhco.model import RHCO, RHCOsc, RHCOpg +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, calc_metrics, \ + METRICS_STR + + +def get_model_class(model): + return RHCOsc if model == 'RHCO_sc' else RHCOpg if model == 'RHCO_pg' else RHCO + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, evaluator = \ + load_data(args.dataset, device) + add_node_feat(g, 'pretrained', args.node_embed_path, True) + features = g.nodes[predict_ntype].data['feat'] + + (*mgs, pos_g), _ = dgl.load_graphs(args.pos_graph_path) + mgs = [mg.to(device) for mg in mgs] + pos_g = pos_g.to(device) + pos = pos_g.in_edges(pos_g.nodes())[0].view(pos_g.num_nodes(), -1) # (N, T_pos) 每个目标顶点的正样本id + # 不能用pos_g.edges(),必须按终点id排序 + + id_loader = DataLoader(train_idx, batch_size=args.batch_size) + loader = NodeDataLoader( + g, {predict_ntype: train_idx}, PositiveSampler([args.neighbor_size] * args.num_layers, pos), + device=device, batch_size=args.batch_size + ) + sampler = PositiveSampler([None], pos) + mg_loaders = [ + NodeDataLoader(mg, train_idx, sampler, device=device, batch_size=args.batch_size) + for mg in mgs + ] + pos_loader = NodeDataLoader(pos_g, train_idx, sampler, device=device, batch_size=args.batch_size) + + model_class = get_model_class(args.model) + model = model_class( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_rel_hidden, args.num_heads, + g.ntypes, g.canonical_etypes, predict_ntype, args.num_layers, args.dropout, + len(mgs), args.tau, args.lambda_ + ).to(device) + if args.load_path: + model.load_state_dict(torch.load(args.load_path, map_location=device)) + optimizer = optim.Adam(model.parameters(), lr=args.lr) + scheduler = optim.lr_scheduler.CosineAnnealingLR( + optimizer, T_max=len(loader) * args.epochs, eta_min=args.lr / 100 + ) + alpha = args.contrast_weight + for epoch in range(args.epochs): + model.train() + losses = [] + for (batch, (_, _, blocks), *mg_blocks, (_, _, pos_blocks)) in tqdm(zip(id_loader, loader, *mg_loaders, pos_loader)): + mg_feats = [features[i] for i, _, _ in mg_blocks] + mg_blocks = [b[0] for _, _, b in mg_blocks] + pos_block = pos_blocks[0] + # pos_block.num_dst_nodes() = batch_size + 正样本数 + batch_pos = torch.zeros(pos_block.num_dst_nodes(), batch.shape[0], dtype=torch.int, device=device) + batch_pos[pos_block.in_edges(torch.arange(batch.shape[0], device=device))] = 1 + contrast_loss, logits = model(blocks, blocks[0].srcdata['feat'], mg_blocks, mg_feats, batch_pos.t()) + clf_loss = F.cross_entropy(logits, labels[batch]) + loss = alpha * contrast_loss + (1 - alpha) * clf_loss + losses.append(loss.item()) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + torch.cuda.empty_cache() + print('Epoch {:d} | Loss {:.4f}'.format(epoch, sum(losses) / len(losses))) + torch.save(model.state_dict(), args.save_path) + if epoch % args.eval_every == 0 or epoch == args.epochs - 1: + print(METRICS_STR.format(*evaluate( + model, g, args.batch_size, device, labels, train_idx, val_idx, test_idx, evaluator + ))) + torch.save(model.state_dict(), args.save_path) + print('模型已保存到', args.save_path) + + +@torch.no_grad() +def evaluate(model, g, batch_size, device, labels, train_idx, val_idx, test_idx, evaluator): + model.eval() + embeds = model.get_embeds(g, batch_size, device) + return calc_metrics(embeds, labels, train_idx, val_idx, test_idx, evaluator) + + +def main(): + parser = argparse.ArgumentParser(description='训练RHCO模型') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['ogbn-mag', 'oag-venue'], default='ogbn-mag', help='数据集') + parser.add_argument('--model', choices=['RHCO', 'RHCO_sc', 'RHCO_pg'], default='RHCO', help='模型名称(用于消融实验)') + parser.add_argument('--num-hidden', type=int, default=64, help='隐藏层维数') + parser.add_argument('--num-rel-hidden', type=int, default=8, help='关系表示的隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--tau', type=float, default=0.8, help='温度参数') + parser.add_argument('--lambda', type=float, default=0.5, dest='lambda_', help='对比损失的平衡系数') + parser.add_argument('--epochs', type=int, default=150, help='训练epoch数') + parser.add_argument('--batch-size', type=int, default=512, help='批大小') + parser.add_argument('--neighbor-size', type=int, default=10, help='邻居采样数') + parser.add_argument('--lr', type=float, default=0.001, help='学习率') + parser.add_argument('--contrast-weight', type=float, default=0.9, help='对比损失权重') + parser.add_argument('--eval-every', type=int, default=10, help='每多少个epoch计算一次准确率') + parser.add_argument('--load-path', help='模型加载路径,用于继续训练') + parser.add_argument('node_embed_path', help='预训练顶点嵌入路径') + parser.add_argument('pos_graph_path', help='正样本图路径') + parser.add_argument('save_path', help='模型保存路径') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/rhco/train_full.py b/gnnrec/hge/rhco/train_full.py new file mode 100644 index 0000000..faa07ae --- /dev/null +++ b/gnnrec/hge/rhco/train_full.py @@ -0,0 +1,100 @@ +import argparse +import warnings + +import dgl +import torch +import torch.nn.functional as F +import torch.optim as optim + +from gnnrec.hge.rhco.model import RHCOFull +from gnnrec.hge.rhco.smooth import smooth +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, calc_metrics, \ + METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, features, labels, predict_ntype, train_idx, val_idx, test_idx, _ = \ + load_data(args.dataset, device) + add_node_feat(g, 'one-hot') + + (*mgs, pos_g), _ = dgl.load_graphs(args.pos_graph_path) + mgs = [mg.to(device) for mg in mgs] + if args.use_data_pos: + pos_v, pos_u = data.pos + pos_g = dgl.graph((pos_u, pos_v), device=device) + pos = torch.zeros((g.num_nodes(predict_ntype), g.num_nodes(predict_ntype)), dtype=torch.int, device=device) + pos[data.pos] = 1 + + model = RHCOFull( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_rel_hidden, args.num_heads, + g.ntypes, g.canonical_etypes, predict_ntype, args.num_layers, args.dropout, + len(mgs), args.tau, args.lambda_ + ).to(device) + optimizer = optim.Adam(model.parameters(), lr=args.lr) + scheduler = optim.lr_scheduler.CosineAnnealingLR( + optimizer, T_max=args.epochs, eta_min=args.lr / 100 + ) + alpha = args.contrast_weight + warnings.filterwarnings('ignore', 'Setting attributes on ParameterDict is not supported') + for epoch in range(args.epochs): + model.train() + contrast_loss, logits = model(g, g.ndata['feat'], mgs, features, pos) + clf_loss = F.cross_entropy(logits[train_idx], labels[train_idx]) + loss = alpha * contrast_loss + (1 - alpha) * clf_loss + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + torch.cuda.empty_cache() + print(('Epoch {:d} | Loss {:.4f} | ' + METRICS_STR).format( + epoch, loss.item(), *evaluate(model, g, labels, train_idx, val_idx, test_idx) + )) + + model.eval() + _, base_pred = model(g, g.ndata['feat'], mgs, features, pos) + mask = torch.cat([train_idx, val_idx]) + logits = smooth(base_pred, pos_g, labels, mask, args) + _, _, test_acc, _, _, test_f1 = calc_metrics(logits, labels, train_idx, val_idx, test_idx) + print('After smoothing: Test Acc {:.4f} | Test Macro-F1 {:.4f}'.format(test_acc, test_f1)) + + +@torch.no_grad() +def evaluate(model, g, labels, train_idx, val_idx, test_idx): + model.eval() + embeds = model.get_embeds(g) + return calc_metrics(embeds, labels, train_idx, val_idx, test_idx) + + +def main(): + parser = argparse.ArgumentParser(description='训练RHCO模型(full-batch)') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['acm', 'dblp'], default='acm', help='数据集') + parser.add_argument('--num-hidden', type=int, default=64, help='隐藏层维数') + parser.add_argument('--num-rel-hidden', type=int, default=8, help='关系表示的隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--tau', type=float, default=0.8, help='温度参数') + parser.add_argument('--lambda', type=float, default=0.5, dest='lambda_', help='对比损失的平衡系数') + parser.add_argument('--epochs', type=int, default=10, help='训练epoch数') + parser.add_argument('--lr', type=float, default=0.001, help='学习率') + parser.add_argument('--contrast-weight', type=float, default=0.5, help='对比损失权重') + parser.add_argument('--num-smooth-layers', type=int, default=50, help='Smooth步骤传播层数') + parser.add_argument('--smooth-alpha', type=float, default=0.5, help='Smooth步骤α值') + parser.add_argument( + '--smooth-norm', choices=['left', 'right', 'both'], default='right', + help='Smooth步骤归一化方式' + ) + parser.add_argument('--use-data-pos', action='store_true', help='使用数据集中的正样本图作为标签传播图') + parser.add_argument('pos_graph_path', help='正样本图路径') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/rhgnn/__init__.py b/gnnrec/hge/rhgnn/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/hge/rhgnn/model.py b/gnnrec/hge/rhgnn/model.py new file mode 100644 index 0000000..1da5fb5 --- /dev/null +++ b/gnnrec/hge/rhgnn/model.py @@ -0,0 +1,370 @@ +import dgl.function as fn +import torch +import torch.nn as nn +import torch.nn.functional as F +from dgl.ops import edge_softmax +from dgl.utils import expand_as_pair + + +class RelationGraphConv(nn.Module): + + def __init__( + self, out_dim, num_heads, fc_src, fc_dst, fc_rel, + feat_drop=0.0, negative_slope=0.2, activation=None): + """特定关系的卷积 + + 针对一种关系(边类型)R=,聚集关系R下的邻居信息,得到dtype类型顶点在关系R下的表示, + 注意力向量使用关系R的表示 + + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param fc_src: nn.Linear(d_in, K*d_out) 源顶点特征转换模块 + :param fc_dst: nn.Linear(d_in, K*d_out) 目标顶点特征转换模块 + :param fc_rel: nn.Linear(d_rel, 2*K*d_out) 关系表示转换模块 + :param feat_drop: float, optional 输入特征Dropout概率,默认为0 + :param negative_slope: float, optional LeakyReLU负斜率,默认为0.2 + :param activation: callable, optional 用于输出特征的激活函数,默认为None + """ + super().__init__() + self.out_dim = out_dim + self.num_heads = num_heads + self.fc_src = fc_src + self.fc_dst = fc_dst + self.fc_rel = fc_rel + self.feat_drop = nn.Dropout(feat_drop) + self.leaky_relu = nn.LeakyReLU(negative_slope) + self.activation = activation + + def forward(self, g, feat, feat_rel): + """ + :param g: DGLGraph 二分图(只包含一种关系) + :param feat: tensor(N_src, d_in) or (tensor(N_src, d_in), tensor(N_dst, d_in)) 输入特征 + :param feat_rel: tensor(d_rel) 关系R的表示 + :return: tensor(N_dst, K*d_out) 目标顶点在关系R下的表示 + """ + with g.local_scope(): + feat_src, feat_dst = expand_as_pair(feat, g) + feat_src = self.fc_src(self.feat_drop(feat_src)).view(-1, self.num_heads, self.out_dim) + feat_dst = self.fc_dst(self.feat_drop(feat_dst)).view(-1, self.num_heads, self.out_dim) + attn = self.fc_rel(feat_rel).view(self.num_heads, 2 * self.out_dim) + + # a^T (z_u || z_v) = (a_l^T || a_r^T) (z_u || z_v) = a_l^T z_u + a_r^T z_v = el + er + el = (feat_src * attn[:, :self.out_dim]).sum(dim=-1, keepdim=True) # (N_src, K, 1) + er = (feat_dst * attn[:, self.out_dim:]).sum(dim=-1, keepdim=True) # (N_dst, K, 1) + g.srcdata.update({'ft': feat_src, 'el': el}) + g.dstdata['er'] = er + g.apply_edges(fn.u_add_v('el', 'er', 'e')) + e = self.leaky_relu(g.edata.pop('e')) + g.edata['a'] = edge_softmax(g, e) # (E, K, 1) + + # 消息传递 + g.update_all(fn.u_mul_e('ft', 'a', 'm'), fn.sum('m', 'ft')) + ret = g.dstdata['ft'].view(-1, self.num_heads * self.out_dim) + if self.activation: + ret = self.activation(ret) + return ret + + +class RelationCrossing(nn.Module): + + def __init__(self, out_dim, num_heads, rel_attn, dropout=0.0, negative_slope=0.2): + """跨关系消息传递 + + 针对一种关系R=,将dtype类型顶点在不同关系下的表示进行组合 + + :param out_dim: int 输出特征维数 + :param num_heads: int 注意力头数K + :param rel_attn: nn.Parameter(K, d) 关系R的注意力向量 + :param dropout: float, optional Dropout概率,默认为0 + :param negative_slope: float, optional LeakyReLU负斜率,默认为0.2 + """ + super().__init__() + self.out_dim = out_dim + self.num_heads = num_heads + self.rel_attn = rel_attn + self.dropout = nn.Dropout(dropout) + self.leaky_relu = nn.LeakyReLU(negative_slope) + + def forward(self, feats): + """ + :param feats: tensor(N_R, N, K*d) dtype类型顶点在不同关系下的表示 + :return: tensor(N, K*d) 跨关系消息传递后dtype类型顶点在关系R下的表示 + """ + num_rel = feats.shape[0] + if num_rel == 1: + return feats.squeeze(dim=0) + feats = feats.view(num_rel, -1, self.num_heads, self.out_dim) # (N_R, N, K, d) + attn_scores = (self.rel_attn * feats).sum(dim=-1, keepdim=True) + attn_scores = F.softmax(self.leaky_relu(attn_scores), dim=0) # (N_R, N, K, 1) + out = (attn_scores * feats).sum(dim=0) # (N, K, d) + out = self.dropout(out.view(-1, self.num_heads * self.out_dim)) # (N, K*d) + return out + + +class RelationFusing(nn.Module): + + def __init__( + self, node_hidden_dim, rel_hidden_dim, num_heads, + w_node, w_rel, dropout=0.0, negative_slope=0.2): + """关系混合 + + 针对一种顶点类型,将该类型顶点在不同关系下的表示进行组合 + + :param node_hidden_dim: int 顶点隐含特征维数 + :param rel_hidden_dim: int 关系隐含特征维数 + :param num_heads: int 注意力头数K + :param w_node: Dict[str, tensor(K, d_node, d_node)] 边类型到顶点关于该关系的特征转换矩阵的映射 + :param w_rel: Dict[str, tensor(K, d_rel, d_node)] 边类型到关系的特征转换矩阵的映射 + :param dropout: float, optional Dropout概率,默认为0 + :param negative_slope: float, optional LeakyReLU负斜率,默认为0.2 + """ + super().__init__() + self.node_hidden_dim = node_hidden_dim + self.rel_hidden_dim = rel_hidden_dim + self.num_heads = num_heads + self.w_node = nn.ParameterDict(w_node) + self.w_rel = nn.ParameterDict(w_rel) + self.dropout = nn.Dropout(dropout) + self.leaky_relu = nn.LeakyReLU(negative_slope) + + def forward(self, node_feats, rel_feats): + """ + :param node_feats: Dict[str, tensor(N, K*d_node)] 边类型到顶点在该关系下的表示的映射 + :param rel_feats: Dict[str, tensor(K*d_rel)] 边类型到关系的表示的映射 + :return: tensor(N, K*d_node) 该类型顶点的最终嵌入 + """ + etypes = list(node_feats.keys()) + num_rel = len(node_feats) + if num_rel == 1: + return node_feats[etypes[0]] + node_feats = torch.stack([node_feats[e] for e in etypes], dim=0) \ + .reshape(num_rel, -1, self.num_heads, self.node_hidden_dim) # (N_R, N, K, d_node) + rel_feats = torch.stack([rel_feats[e] for e in etypes], dim=0) \ + .reshape(num_rel, self.num_heads, self.rel_hidden_dim) # (N_R, K, d_rel) + w_node = torch.stack([self.w_node[e] for e in etypes], dim=0) # (N_R, K, d_node, d_node) + w_rel = torch.stack([self.w_rel[e] for e in etypes], dim=0) # (N_R, K, d_rel, d_node) + + # hn[r, n, h] @= wn[r, h] => hn[r, n, h, i] = ∑(k) hn[r, n, h, k] * wn[r, h, k, i] + node_feats = torch.einsum('rnhk,rhki->rnhi', node_feats, w_node) # (N_R, N, K, d_node) + # hr[r, h] @= wr[r, h] => hr[r, h, i] = ∑(k) hr[r, h, k] * wr[r, h, k, i] + rel_feats = torch.einsum('rhk,rhki->rhi', rel_feats, w_rel) # (N_R, K, d_node) + + attn_scores = (node_feats * rel_feats.unsqueeze(dim=1)).sum(dim=-1, keepdim=True) + attn_scores = F.softmax(self.leaky_relu(attn_scores), dim=0) # (N_R, N, K, 1) + out = (attn_scores * node_feats).sum(dim=0) # (N_R, N, K, d_node) + out = self.dropout(out.view(-1, self.num_heads * self.node_hidden_dim)) # (N, K*d_node) + return out + + +class RHGNNLayer(nn.Module): + + def __init__( + self, node_in_dim, node_out_dim, rel_in_dim, rel_out_dim, num_heads, + ntypes, etypes, dropout=0.0, negative_slope=0.2, residual=True): + """R-HGNN层 + + :param node_in_dim: int 顶点输入特征维数 + :param node_out_dim: int 顶点输出特征维数 + :param rel_in_dim: int 关系输入特征维数 + :param rel_out_dim: int 关系输出特征维数 + :param num_heads: int 注意力头数K + :param ntypes: List[str] 顶点类型列表 + :param etypes: List[(str, str, str)] 规范边类型列表 + :param dropout: float, optional Dropout概率,默认为0 + :param negative_slope: float, optional LeakyReLU负斜率,默认为0.2 + :param residual: bool, optional 是否使用残差连接,默认True + """ + super().__init__() + # 特定关系的卷积的参数 + fc_node = { + ntype: nn.Linear(node_in_dim, num_heads * node_out_dim, bias=False) + for ntype in ntypes + } + fc_rel = { + etype: nn.Linear(rel_in_dim, 2 * num_heads * node_out_dim, bias=False) + for _, etype, _ in etypes + } + self.rel_graph_conv = nn.ModuleDict({ + etype: RelationGraphConv( + node_out_dim, num_heads, fc_node[stype], fc_node[dtype], fc_rel[etype], + dropout, negative_slope, F.relu + ) for stype, etype, dtype in etypes + }) + + # 残差连接的参数 + self.residual = residual + if residual: + self.fc_res = nn.ModuleDict({ + ntype: nn.Linear(node_in_dim, num_heads * node_out_dim) for ntype in ntypes + }) + self.res_weight = nn.ParameterDict({ + ntype: nn.Parameter(torch.rand(1)) for ntype in ntypes + }) + + # 关系表示学习的参数 + self.fc_upd = nn.ModuleDict({ + etype: nn.Linear(rel_in_dim, num_heads * rel_out_dim) + for _, etype, _ in etypes + }) + + # 跨关系消息传递的参数 + rel_attn = { + etype: nn.Parameter(torch.FloatTensor(num_heads, node_out_dim)) + for _, etype, _ in etypes + } + self.rel_cross = nn.ModuleDict({ + etype: RelationCrossing( + node_out_dim, num_heads, rel_attn[etype], dropout, negative_slope + ) for _, etype, _ in etypes + }) + + self.rev_etype = { + e: next(re for rs, re, rd in etypes if rs == d and rd == s and re != e) + for s, e, d in etypes + } + self.reset_parameters(rel_attn) + + def reset_parameters(self, rel_attn): + gain = nn.init.calculate_gain('relu') + for etype in rel_attn: + nn.init.xavier_normal_(rel_attn[etype], gain=gain) + + def forward(self, g, feats, rel_feats): + """ + :param g: DGLGraph 异构图 + :param feats: Dict[(str, str, str), tensor(N_i, d_in)] 关系(三元组)到目标顶点输入特征的映射 + :param rel_feats: Dict[str, tensor(d_in_rel)] 边类型到输入关系特征的映射 + :return: Dict[(str, str, str), tensor(N_i, K*d_out)], Dict[str, tensor(K*d_out_rel)] + 关系(三元组)到目标顶点在该关系下的表示的映射、边类型到关系表示的映射 + """ + if g.is_block: + feats_dst = {r: feats[r][:g.num_dst_nodes(r[2])] for r in feats} + else: + feats_dst = feats + + node_rel_feats = { + (stype, etype, dtype): self.rel_graph_conv[etype]( + g[stype, etype, dtype], + (feats[(dtype, self.rev_etype[etype], stype)], feats_dst[(stype, etype, dtype)]), + rel_feats[etype] + ) for stype, etype, dtype in g.canonical_etypes + if g.num_edges((stype, etype, dtype)) > 0 + } # {rel: tensor(N_dst, K*d_out)} + + if self.residual: + for stype, etype, dtype in node_rel_feats: + alpha = torch.sigmoid(self.res_weight[dtype]) + inherit_feat = self.fc_res[dtype](feats_dst[(stype, etype, dtype)]) + node_rel_feats[(stype, etype, dtype)] = \ + alpha * node_rel_feats[(stype, etype, dtype)] + (1 - alpha) * inherit_feat + + out_feats = {} # {rel: tensor(N_dst, K*d_out)} + for stype, etype, dtype in node_rel_feats: + dst_node_rel_feats = torch.stack([ + node_rel_feats[r] for r in node_rel_feats if r[2] == dtype + ], dim=0) # (N_Ri, N_i, K*d_out) + out_feats[(stype, etype, dtype)] = self.rel_cross[etype](dst_node_rel_feats) + + rel_feats = {etype: self.fc_upd[etype](rel_feats[etype]) for etype in rel_feats} + return out_feats, rel_feats + + +class RHGNN(nn.Module): + + def __init__( + self, in_dims, hidden_dim, out_dim, rel_in_dim, rel_hidden_dim, num_heads, ntypes, + etypes, predict_ntype, num_layers, dropout=0.0, negative_slope=0.2, residual=True): + """R-HGNN模型 + + :param in_dims: Dict[str, int] 顶点类型到输入特征维数的映射 + :param hidden_dim: int 顶点隐含特征维数 + :param out_dim: int 顶点输出特征维数 + :param rel_in_dim: int 关系输入特征维数 + :param rel_hidden_dim: int 关系隐含特征维数 + :param num_heads: int 注意力头数K + :param ntypes: List[str] 顶点类型列表 + :param etypes: List[(str, str, str)] 规范边类型列表 + :param predict_ntype: str 待预测顶点类型 + :param num_layers: int 层数 + :param dropout: float, optional Dropout概率,默认为0 + :param negative_slope: float, optional LeakyReLU负斜率,默认为0.2 + :param residual: bool, optional 是否使用残差连接,默认True + """ + super().__init__() + self._d = num_heads * hidden_dim + self.etypes = etypes + self.predict_ntype = predict_ntype + # 对齐输入特征维数 + self.fc_in = nn.ModuleDict({ + ntype: nn.Linear(in_dim, num_heads * hidden_dim) for ntype, in_dim in in_dims.items() + }) + # 关系输入特征 + self.rel_embed = nn.ParameterDict({ + etype: nn.Parameter(torch.FloatTensor(1, rel_in_dim)) for _, etype, _ in etypes + }) + + self.layers = nn.ModuleList() + self.layers.append(RHGNNLayer( + num_heads * hidden_dim, hidden_dim, rel_in_dim, rel_hidden_dim, + num_heads, ntypes, etypes, dropout, negative_slope, residual + )) + for _ in range(1, num_layers): + self.layers.append(RHGNNLayer( + num_heads * hidden_dim, hidden_dim, num_heads * rel_hidden_dim, rel_hidden_dim, + num_heads, ntypes, etypes, dropout, negative_slope, residual + )) + + w_node = { + etype: nn.Parameter(torch.FloatTensor(num_heads, hidden_dim, hidden_dim)) + for _, etype, _ in etypes + } + w_rel = { + etype: nn.Parameter(torch.FloatTensor(num_heads, rel_hidden_dim, hidden_dim)) + for _, etype, _ in etypes + } + self.rel_fusing = nn.ModuleDict({ + ntype: RelationFusing( + hidden_dim, rel_hidden_dim, num_heads, + {e: w_node[e] for _, e, d in etypes if d == ntype}, + {e: w_rel[e] for _, e, d in etypes if d == ntype}, + dropout, negative_slope + ) for ntype in ntypes + }) + self.classifier = nn.Linear(num_heads * hidden_dim, out_dim) + self.reset_parameters(self.rel_embed, w_node, w_rel) + + def reset_parameters(self, rel_embed, w_node, w_rel): + gain = nn.init.calculate_gain('relu') + for etype in rel_embed: + nn.init.xavier_normal_(rel_embed[etype], gain=gain) + nn.init.xavier_normal_(w_node[etype], gain=gain) + nn.init.xavier_normal_(w_rel[etype], gain=gain) + + def forward(self, blocks, feats): + """ + :param blocks: blocks: List[DGLBlock] + :param feats: Dict[str, tensor(N_i, d_in_i)] 顶点类型到输入顶点特征的映射 + :return: tensor(N_i, d_out) 待预测顶点的最终嵌入 + """ + feats = { + (stype, etype, dtype): self.fc_in[dtype](feats[dtype]) + for stype, etype, dtype in self.etypes + } + rel_feats = {rel: emb.flatten() for rel, emb in self.rel_embed.items()} + for block, layer in zip(blocks, self.layers): + # {(stype, etype, dtype): tensor(N_i, K*d_hid)}, {etype: tensor(K*d_hid_rel)} + feats, rel_feats = layer(block, feats, rel_feats) + + out_feats = { + ntype: self.rel_fusing[ntype]( + {e: feats[(s, e, d)] for s, e, d in feats if d == ntype}, + {e: rel_feats[e] for s, e, d in feats if d == ntype} + ) for ntype in set(d for _, _, d in feats) + } # {ntype: tensor(N_i, K*d_hid)} + return self.classifier(out_feats[self.predict_ntype]) + + +class RHGNNFull(RHGNN): + + def forward(self, g, feats): + return super().forward([g] * len(self.layers), feats) diff --git a/gnnrec/hge/rhgnn/train.py b/gnnrec/hge/rhgnn/train.py new file mode 100644 index 0000000..da25189 --- /dev/null +++ b/gnnrec/hge/rhgnn/train.py @@ -0,0 +1,85 @@ +import argparse +import warnings + +import torch +import torch.nn.functional as F +import torch.optim as optim +from dgl.dataloading import MultiLayerNeighborSampler, NodeDataLoader +from tqdm import tqdm + +from gnnrec.hge.rhgnn.model import RHGNN +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, evaluate, \ + METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, evaluator = \ + load_data(args.dataset, device) + add_node_feat(g, 'pretrained', args.node_embed_path, True) + + sampler = MultiLayerNeighborSampler(list(range(args.neighbor_size, args.neighbor_size + args.num_layers))) + train_loader = NodeDataLoader(g, {predict_ntype: train_idx}, sampler, device=device, batch_size=args.batch_size) + loader = NodeDataLoader(g, {predict_ntype: g.nodes(predict_ntype)}, sampler, device=device, batch_size=args.batch_size) + + model = RHGNN( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_rel_hidden, args.num_rel_hidden, args.num_heads, + g.ntypes, g.canonical_etypes, predict_ntype, args.num_layers, args.dropout + ).to(device) + optimizer = optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay) + scheduler = optim.lr_scheduler.CosineAnnealingLR( + optimizer, T_max=len(train_loader) * args.epochs, eta_min=args.lr / 100 + ) + warnings.filterwarnings('ignore', 'Setting attributes on ParameterDict is not supported') + for epoch in range(args.epochs): + model.train() + losses = [] + for input_nodes, output_nodes, blocks in tqdm(train_loader): + batch_logits = model(blocks, blocks[0].srcdata['feat']) + batch_labels = labels[output_nodes[predict_ntype]] + loss = F.cross_entropy(batch_logits, batch_labels) + losses.append(loss.item()) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + torch.cuda.empty_cache() + print('Epoch {:d} | Loss {:.4f}'.format(epoch, sum(losses) / len(losses))) + if epoch % args.eval_every == 0 or epoch == args.epochs - 1: + print(METRICS_STR.format(*evaluate( + model, loader, g, labels, data.num_classes, predict_ntype, + train_idx, val_idx, test_idx, evaluator + ))) + if args.save_path: + torch.save(model.cpu().state_dict(), args.save_path) + print('模型已保存到', args.save_path) + + +def main(): + parser = argparse.ArgumentParser(description='训练R-HGNN模型') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['ogbn-mag', 'oag-venue'], default='ogbn-mag', help='数据集') + parser.add_argument('--num-hidden', type=int, default=64, help='隐藏层维数') + parser.add_argument('--num-rel-hidden', type=int, default=8, help='关系表示的隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--epochs', type=int, default=200, help='训练epoch数') + parser.add_argument('--batch-size', type=int, default=1024, help='批大小') + parser.add_argument('--neighbor-size', type=int, default=10, help='邻居采样数') + parser.add_argument('--lr', type=float, default=0.001, help='学习率') + parser.add_argument('--weight-decay', type=float, default=0.0, help='权重衰减') + parser.add_argument('--eval-every', type=int, default=10, help='每多少个epoch计算一次准确率') + parser.add_argument('--save-path', help='模型保存路径') + parser.add_argument('node_embed_path', help='预训练顶点嵌入路径') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/rhgnn/train_full.py b/gnnrec/hge/rhgnn/train_full.py new file mode 100644 index 0000000..0b1a2f4 --- /dev/null +++ b/gnnrec/hge/rhgnn/train_full.py @@ -0,0 +1,63 @@ +import argparse +import warnings + +import torch +import torch.nn.functional as F +import torch.optim as optim + +from gnnrec.hge.rhgnn.model import RHGNNFull +from gnnrec.hge.utils import set_random_seed, get_device, load_data, add_node_feat, evaluate_full, \ + METRICS_STR + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + data, g, _, labels, predict_ntype, train_idx, val_idx, test_idx, _ = \ + load_data(args.dataset, device) + add_node_feat(g, 'one-hot') + + model = RHGNNFull( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, data.num_classes, args.num_rel_hidden, args.num_rel_hidden, args.num_heads, + g.ntypes, g.canonical_etypes, predict_ntype, args.num_layers, args.dropout + ).to(device) + optimizer = optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay) + scheduler = optim.lr_scheduler.CosineAnnealingLR( + optimizer, T_max=args.epochs, eta_min=args.lr / 100 + ) + warnings.filterwarnings('ignore', 'Setting attributes on ParameterDict is not supported') + for epoch in range(args.epochs): + model.train() + logits = model(g, g.ndata['feat']) + loss = F.cross_entropy(logits[train_idx], labels[train_idx]) + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + torch.cuda.empty_cache() + print(('Epoch {:d} | Loss {:.4f} | ' + METRICS_STR).format( + epoch, loss.item(), *evaluate_full(model, g, labels, train_idx, val_idx, test_idx) + )) + + +def main(): + parser = argparse.ArgumentParser(description='训练R-HGNN模型(full-batch)') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + parser.add_argument('--dataset', choices=['acm', 'dblp'], default='acm', help='数据集') + parser.add_argument('--num-hidden', type=int, default=64, help='隐藏层维数') + parser.add_argument('--num-rel-hidden', type=int, default=8, help='关系表示的隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--epochs', type=int, default=10, help='训练epoch数') + parser.add_argument('--lr', type=float, default=0.001, help='学习率') + parser.add_argument('--weight-decay', type=float, default=0.0, help='权重衰减') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/hge/utils/__init__.py b/gnnrec/hge/utils/__init__.py new file mode 100644 index 0000000..3322951 --- /dev/null +++ b/gnnrec/hge/utils/__init__.py @@ -0,0 +1,28 @@ +import random + +import numpy as np + +from .data import * +from .metrics import * + + +def set_random_seed(seed): + """设置Python, numpy, PyTorch的随机数种子 + + :param seed: int 随机数种子 + """ + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed(seed) + dgl.seed(seed) + + +def get_device(device): + """返回指定的GPU设备 + + :param device: int GPU编号,-1表示CPU + :return: torch.device + """ + return torch.device(f'cuda:{device}' if device >= 0 and torch.cuda.is_available() else 'cpu') diff --git a/gnnrec/hge/utils/data.py b/gnnrec/hge/utils/data.py new file mode 100644 index 0000000..9a3b95d --- /dev/null +++ b/gnnrec/hge/utils/data.py @@ -0,0 +1,138 @@ +import dgl +import dgl.function as fn +import torch +from gensim.models import Word2Vec +from ogb.nodeproppred import DglNodePropPredDataset, Evaluator + +from gnnrec.config import DATA_DIR +from gnnrec.hge.data import ACMDataset, DBLPDataset +from gnnrec.kgrec.data import OAGVenueDataset + + +def load_data(name, device='cpu', add_reverse_edge=True, reverse_self=True): + """加载数据集 + + :param name: str 数据集名称 acm, dblp, ogbn-mag, oag-venue + :param device: torch.device, optional 将图和数据移动到指定的设备上,默认为CPU + :param add_reverse_edge: bool, optional 是否添加反向边,默认为True + :param reverse_self: bool, optional 起点和终点类型相同时是否添加反向边,默认为True + :return: dataset, g, features, labels, predict_ntype, train_mask, val_mask, test_mask, evaluator + """ + if name == 'ogbn-mag': + return load_ogbn_mag(device, add_reverse_edge, reverse_self) + elif name == 'acm': + data = ACMDataset() + elif name == 'dblp': + data = DBLPDataset() + elif name == 'oag-venue': + data = OAGVenueDataset() + else: + raise ValueError(f'load_data: 未知数据集{name}') + g = data[0] + predict_ntype = data.predict_ntype + # ACM和DBLP数据集已添加反向边 + if add_reverse_edge and name not in ('acm', 'dblp'): + g = add_reverse_edges(g, reverse_self) + g = g.to(device) + features = g.nodes[predict_ntype].data['feat'] + labels = g.nodes[predict_ntype].data['label'] + train_mask = g.nodes[predict_ntype].data['train_mask'].nonzero(as_tuple=True)[0] + val_mask = g.nodes[predict_ntype].data['val_mask'].nonzero(as_tuple=True)[0] + test_mask = g.nodes[predict_ntype].data['test_mask'].nonzero(as_tuple=True)[0] + return data, g, features, labels, predict_ntype, train_mask, val_mask, test_mask, None + + +def load_ogbn_mag(device, add_reverse_edge, reverse_self): + """加载ogbn-mag数据集 + + :param device: torch.device 将图和数据移动到指定的设备上,默认为CPU + :param add_reverse_edge: bool 是否添加反向边 + :param reverse_self: bool 起点和终点类型相同时是否添加反向边 + :return: dataset, g, features, labels, predict_ntype, train_mask, val_mask, test_mask, evaluator + """ + data = DglNodePropPredDataset('ogbn-mag', DATA_DIR) + g, labels = data[0] + if add_reverse_edge: + g = add_reverse_edges(g, reverse_self) + g = g.to(device) + features = g.nodes['paper'].data['feat'] + labels = labels['paper'].squeeze(dim=1).to(device) + split_idx = data.get_idx_split() + train_idx = split_idx['train']['paper'].to(device) + val_idx = split_idx['valid']['paper'].to(device) + test_idx = split_idx['test']['paper'].to(device) + evaluator = Evaluator(data.name) + return data, g, features, labels, 'paper', train_idx, val_idx, test_idx, evaluator + + +def add_reverse_edges(g, reverse_self=True): + """给异构图的每种边添加反向边,返回新的异构图 + + :param g: DGLGraph 异构图 + :param reverse_self: bool, optional 起点和终点类型相同时是否添加反向边,默认为True + :return: DGLGraph 添加反向边之后的异构图 + """ + data = {} + for stype, etype, dtype in g.canonical_etypes: + u, v = g.edges(etype=(stype, etype, dtype)) + data[(stype, etype, dtype)] = u, v + if stype != dtype or reverse_self: + data[(dtype, etype + '_rev', stype)] = v, u + new_g = dgl.heterograph(data, {ntype: g.num_nodes(ntype) for ntype in g.ntypes}) + for ntype in g.ntypes: + new_g.nodes[ntype].data.update(g.nodes[ntype].data) + for etype in g.canonical_etypes: + new_g.edges[etype].data.update(g.edges[etype].data) + return new_g + + +def one_hot_node_feat(g): + for ntype in g.ntypes: + if 'feat' not in g.nodes[ntype].data: + g.nodes[ntype].data['feat'] = torch.eye(g.num_nodes(ntype), device=g.device) + + +def average_node_feat(g): + """ogbn-mag数据集没有输入特征的顶点取邻居平均""" + message_func, reduce_func = fn.copy_u('feat', 'm'), fn.mean('m', 'feat') + g.multi_update_all({ + 'writes_rev': (message_func, reduce_func), + 'has_topic': (message_func, reduce_func) + }, 'sum') + g.multi_update_all({'affiliated_with': (message_func, reduce_func)}, 'sum') + + +def load_pretrained_node_embed(g, node_embed_path, concat=False): + """为没有输入特征的顶点加载预训练的顶点特征 + + :param g: DGLGraph 异构图 + :param node_embed_path: str 预训练的word2vec模型路径 + :param concat: bool, optional 如果为True则将预训练特征与原输入特征拼接 + """ + model = Word2Vec.load(node_embed_path) + for ntype in g.ntypes: + embed = torch.from_numpy(model.wv[[f'{ntype}_{i}' for i in range(g.num_nodes(ntype))]]) \ + .to(g.device) + if 'feat' in g.nodes[ntype].data: + if concat: + g.nodes[ntype].data['feat'] = torch.cat([g.nodes[ntype].data['feat'], embed], dim=1) + else: + g.nodes[ntype].data['feat'] = embed + + +def add_node_feat(g, method, node_embed_path=None, concat=False): + """为没有输入特征的顶点添加输入特征 + + :param g: DGLGraph 异构图 + :param method: str one-hot, average(仅用于ogbn-mag数据集), pretrained + :param node_embed_path: str 预训练的word2vec模型路径 + :param concat: bool, optional 如果为True则将预训练特征与原输入特征拼接 + """ + if method == 'one-hot': + one_hot_node_feat(g) + elif method == 'average': + average_node_feat(g) + elif method == 'pretrained': + load_pretrained_node_embed(g, node_embed_path, concat) + else: + raise ValueError(f'add_node_feat: 未知方法{method}') diff --git a/gnnrec/hge/utils/metrics.py b/gnnrec/hge/utils/metrics.py new file mode 100644 index 0000000..cbe1d00 --- /dev/null +++ b/gnnrec/hge/utils/metrics.py @@ -0,0 +1,87 @@ +import torch +from sklearn.metrics import f1_score + + +def accuracy(predict, labels, evaluator=None): + """计算准确率 + + :param predict: tensor(N) 预测标签 + :param labels: tensor(N) 正确标签 + :param evaluator: ogb.nodeproppred.Evaluator + :return: float 准确率 + """ + if evaluator is not None: + y_true, y_pred = labels.unsqueeze(dim=1), predict.unsqueeze(dim=1) + return evaluator.eval({'y_true': y_true, 'y_pred': y_pred})['acc'] + else: + return torch.sum(predict == labels).item() / labels.shape[0] + + +def macro_f1_score(predict, labels): + """计算Macro-F1得分 + + :param predict: tensor(N) 预测标签 + :param labels: tensor(N) 正确标签 + :return: float Macro-F1得分 + """ + return f1_score(labels.numpy(), predict.long().numpy(), average='macro') + + +@torch.no_grad() +def evaluate( + model, loader, g, labels, num_classes, predict_ntype, + train_idx, val_idx, test_idx, evaluator=None): + """评估模型性能 + + :param model: nn.Module GNN模型 + :param loader: NodeDataLoader 图数据加载器 + :param g: DGLGraph 图 + :param labels: tensor(N) 顶点标签 + :param num_classes: int 类别数 + :param predict_ntype: str 目标顶点类型 + :param train_idx: tensor(N_train) 训练集顶点id + :param val_idx: tensor(N_val) 验证集顶点id + :param test_idx: tensor(N_test) 测试集顶点id + :param evaluator: ogb.nodeproppred.Evaluator + :return: train_acc, val_acc, test_acc, train_f1, val_f1, test_f1 + """ + model.eval() + logits = torch.zeros(g.num_nodes(predict_ntype), num_classes, device=train_idx.device) + for input_nodes, output_nodes, blocks in loader: + logits[output_nodes[predict_ntype]] = model(blocks, blocks[0].srcdata['feat']) + return calc_metrics(logits, labels, train_idx, val_idx, test_idx, evaluator) + + +@torch.no_grad() +def evaluate_full(model, g, labels, train_idx, val_idx, test_idx): + """评估模型性能(full-batch) + + :param model: nn.Module GNN模型 + :param g: DGLGraph 图 + :param labels: tensor(N) 顶点标签 + :param train_idx: tensor(N_train) 训练集顶点id + :param val_idx: tensor(N_val) 验证集顶点id + :param test_idx: tensor(N_test) 测试集顶点id + :return: train_acc, val_acc, test_acc, train_f1, val_f1, test_f1 + """ + model.eval() + logits = model(g, g.ndata['feat']) + return calc_metrics(logits, labels, train_idx, val_idx, test_idx) + + +def calc_metrics(logits, labels, train_idx, val_idx, test_idx, evaluator=None): + predict = logits.detach().cpu().argmax(dim=1) + labels = labels.cpu() + train_acc = accuracy(predict[train_idx], labels[train_idx], evaluator) + val_acc = accuracy(predict[val_idx], labels[val_idx], evaluator) + test_acc = accuracy(predict[test_idx], labels[test_idx], evaluator) + train_f1 = macro_f1_score(predict[train_idx], labels[train_idx]) + val_f1 = macro_f1_score(predict[val_idx], labels[val_idx]) + test_f1 = macro_f1_score(predict[test_idx], labels[test_idx]) + return train_acc, val_acc, test_acc, train_f1, val_f1, test_f1 + + +METRICS_STR = ' | '.join( + f'{split} {metric} {{:.4f}}' + for metric in ('Acc', 'Macro-F1') for split in ('Train', 'Val', 'Test') +) diff --git a/gnnrec/kgrec/__init__.py b/gnnrec/kgrec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/kgrec/data/__init__.py b/gnnrec/kgrec/data/__init__.py new file mode 100644 index 0000000..263eaf1 --- /dev/null +++ b/gnnrec/kgrec/data/__init__.py @@ -0,0 +1,3 @@ +from .oagcs import OAGCSDataset +from .contrast import OAGCSContrastDataset +from .venue import OAGVenueDataset diff --git a/gnnrec/kgrec/data/config.py b/gnnrec/kgrec/data/config.py new file mode 100644 index 0000000..875a10d --- /dev/null +++ b/gnnrec/kgrec/data/config.py @@ -0,0 +1,38 @@ +CS = 'computer science' + +CS_FIELD_L2 = [ + 'algorithm', + 'artificial intelligence', + 'computational science', + 'computer architecture', + 'computer engineering', + 'computer graphics', + 'computer hardware', + 'computer network', + 'computer security', + 'computer vision', + 'data mining', + 'data science', + 'database', + 'distributed computing', + 'embedded system', + 'human computer interaction', + 'information retrieval', + 'internet privacy', + 'knowledge management', + 'library science', + 'machine learning', + 'multimedia', + 'natural language processing', + 'operating system', + 'parallel computing', + 'pattern recognition', + 'programming language', + 'real time computing', + 'simulation', + 'software engineering', + 'speech recognition', + 'telecommunications', + 'theoretical computer science', + 'world wide web', +] diff --git a/gnnrec/kgrec/data/contrast.py b/gnnrec/kgrec/data/contrast.py new file mode 100644 index 0000000..75ff56a --- /dev/null +++ b/gnnrec/kgrec/data/contrast.py @@ -0,0 +1,30 @@ +from torch.utils.data import Dataset + +from gnnrec.kgrec.utils import iter_json + + +class OAGCSContrastDataset(Dataset): + SPLIT_YEAR = 2016 + + def __init__(self, raw_file, split='train'): + """oag-cs论文标题-关键词对比学习数据集 + + 由于原始数据不包含关键词,因此使用研究领域(fos字段)作为关键词 + + :param raw_file: str 原始论文数据文件 + :param split: str "train", "valid", "all" + """ + self.titles = [] + self.keywords = [] + for p in iter_json(raw_file): + if split == 'train' and p['year'] <= self.SPLIT_YEAR \ + or split == 'valid' and p['year'] > self.SPLIT_YEAR \ + or split == 'all': + self.titles.append(p['title']) + self.keywords.append('; '.join(p['fos'])) + + def __getitem__(self, item): + return self.titles[item], self.keywords[item] + + def __len__(self): + return len(self.titles) diff --git a/gnnrec/kgrec/data/oagcs.py b/gnnrec/kgrec/data/oagcs.py new file mode 100644 index 0000000..306e27b --- /dev/null +++ b/gnnrec/kgrec/data/oagcs.py @@ -0,0 +1,153 @@ +import os + +import dgl +import pandas as pd +import torch +from dgl.data import DGLDataset, extract_archive +from dgl.data.utils import save_graphs, load_graphs + +from gnnrec.kgrec.utils import iter_json + + +class OAGCSDataset(DGLDataset): + """OAG MAG数据集(https://www.aminer.cn/oag-2-1)计算机领域的子集,只有一个异构图 + + 统计数据 + ----- + 顶点 + + * 2248205 author + * 1852225 paper + * 11177 venue + * 13747 institution + * 120992 field + + 边 + + * 6349317 author-writes->paper + * 1852225 paper-published_at->venue + * 17250107 paper-has_field->field + * 9194781 paper-cites->paper + * 1726212 author-affiliated_with->institution + + paper顶点属性 + ----- + * feat: tensor(N_paper, 128) 预训练的标题和摘要词向量 + * year: tensor(N_paper) 发表年份(2010~2021) + * citation: tensor(N_paper) 引用数 + * 不包含标签 + + field顶点属性 + ----- + * feat: tensor(N_field, 128) 预训练的领域向量 + + writes边属性 + ----- + * order: tensor(N_writes) 作者顺序(从1开始) + """ + + def __init__(self, **kwargs): + super().__init__('oag-cs', 'https://pan.baidu.com/s/1ayH3tQxsiDDnqPoXhR0Ekg', **kwargs) + + def download(self): + zip_file_path = os.path.join(self.raw_dir, 'oag-cs.zip') + if not os.path.exists(zip_file_path): + raise FileNotFoundError('请手动下载文件 {} 提取码:2ylp 并保存为 {}'.format( + self.url, zip_file_path + )) + extract_archive(zip_file_path, self.raw_path) + + def save(self): + save_graphs(os.path.join(self.save_path, self.name + '_dgl_graph.bin'), [self.g]) + + def load(self): + self.g = load_graphs(os.path.join(self.save_path, self.name + '_dgl_graph.bin'))[0][0] + + def process(self): + self._vid_map = self._read_venues() # {原始id: 顶点id} + self._oid_map = self._read_institutions() # {原始id: 顶点id} + self._fid_map = self._read_fields() # {领域名称: 顶点id} + self._aid_map, author_inst = self._read_authors() # {原始id: 顶点id}, R(aid, oid) + # PA(pid, aid), PV(pid, vid), PF(pid, fid), PP(pid, rid), [年份], [引用数] + paper_author, paper_venue, paper_field, paper_ref, paper_year, paper_citation = self._read_papers() + self.g = self._build_graph(paper_author, paper_venue, paper_field, paper_ref, author_inst, paper_year, paper_citation) + + def _iter_json(self, filename): + yield from iter_json(os.path.join(self.raw_path, filename)) + + def _read_venues(self): + print('正在读取期刊数据...') + # 行号=索引=顶点id + return {v['id']: i for i, v in enumerate(self._iter_json('mag_venues.txt'))} + + def _read_institutions(self): + print('正在读取机构数据...') + return {o['id']: i for i, o in enumerate(self._iter_json('mag_institutions.txt'))} + + def _read_fields(self): + print('正在读取领域数据...') + return {f['name']: f['id'] for f in self._iter_json('mag_fields.txt')} + + def _read_authors(self): + print('正在读取学者数据...') + author_id_map, author_inst = {}, [] + for i, a in enumerate(self._iter_json('mag_authors.txt')): + author_id_map[a['id']] = i + if a['org'] is not None: + author_inst.append([i, self._oid_map[a['org']]]) + return author_id_map, pd.DataFrame(author_inst, columns=['aid', 'oid']) + + def _read_papers(self): + print('正在读取论文数据...') + paper_id_map, paper_author, paper_venue, paper_field = {}, [], [], [] + paper_year, paper_citation = [], [] + for i, p in enumerate(self._iter_json('mag_papers.txt')): + paper_id_map[p['id']] = i + paper_author.extend([i, self._aid_map[a], r + 1] for r, a in enumerate(p['authors'])) + paper_venue.append([i, self._vid_map[p['venue']]]) + paper_field.extend([i, self._fid_map[f]] for f in p['fos'] if f in self._fid_map) + paper_year.append(p['year']) + paper_citation.append(p['n_citation']) + + paper_ref = [] + for i, p in enumerate(self._iter_json('mag_papers.txt')): + paper_ref.extend([i, paper_id_map[r]] for r in p['references'] if r in paper_id_map) + return ( + pd.DataFrame(paper_author, columns=['pid', 'aid', 'order']).drop_duplicates(subset=['pid', 'aid']), + pd.DataFrame(paper_venue, columns=['pid', 'vid']), + pd.DataFrame(paper_field, columns=['pid', 'fid']), + pd.DataFrame(paper_ref, columns=['pid', 'rid']), + paper_year, paper_citation + ) + + def _build_graph(self, paper_author, paper_venue, paper_field, paper_ref, author_inst, paper_year, paper_citation): + print('正在构造异构图...') + pa_p, pa_a = paper_author['pid'].to_list(), paper_author['aid'].to_list() + pv_p, pv_v = paper_venue['pid'].to_list(), paper_venue['vid'].to_list() + pf_p, pf_f = paper_field['pid'].to_list(), paper_field['fid'].to_list() + pp_p, pp_r = paper_ref['pid'].to_list(), paper_ref['rid'].to_list() + ai_a, ai_i = author_inst['aid'].to_list(), author_inst['oid'].to_list() + g = dgl.heterograph({ + ('author', 'writes', 'paper'): (pa_a, pa_p), + ('paper', 'published_at', 'venue'): (pv_p, pv_v), + ('paper', 'has_field', 'field'): (pf_p, pf_f), + ('paper', 'cites', 'paper'): (pp_p, pp_r), + ('author', 'affiliated_with', 'institution'): (ai_a, ai_i) + }) + g.nodes['paper'].data['feat'] = torch.load(os.path.join(self.raw_path, 'paper_feat.pkl')) + g.nodes['paper'].data['year'] = torch.tensor(paper_year) + g.nodes['paper'].data['citation'] = torch.tensor(paper_citation) + g.nodes['field'].data['feat'] = torch.load(os.path.join(self.raw_path, 'field_feat.pkl')) + g.edges['writes'].data['order'] = torch.tensor(paper_author['order'].to_list()) + return g + + def has_cache(self): + return os.path.exists(os.path.join(self.save_path, self.name + '_dgl_graph.bin')) + + def __getitem__(self, idx): + if idx != 0: + raise IndexError('This dataset has only one graph') + return self.g + + def __len__(self): + return 1 diff --git a/gnnrec/kgrec/data/preprocess/__init__.py b/gnnrec/kgrec/data/preprocess/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnnrec/kgrec/data/preprocess/ai2000_crawler.py b/gnnrec/kgrec/data/preprocess/ai2000_crawler.py new file mode 100644 index 0000000..60cc1a7 --- /dev/null +++ b/gnnrec/kgrec/data/preprocess/ai2000_crawler.py @@ -0,0 +1,72 @@ +import json +import os +from collections import defaultdict + +import scrapy +from itemadapter import ItemAdapter + + +class ScholarItem(scrapy.Item): + name = scrapy.Field() + org = scrapy.Field() + field = scrapy.Field() + rank = scrapy.Field() + + +class AI2000Spider(scrapy.Spider): + name = 'ai2000' + allowed_domains = ['aminer.cn'] + custom_settings = { + 'DEFAULT_REQUEST_HEADERS': { + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + }, + 'DOWNLOAD_DELAY': 20, + 'ITEM_PIPELINES': {'ai2000_crawler.JsonWriterPipeline': 0} + } + + def __init__(self, save_path, *args, **kwargs): + super().__init__(*args, **kwargs) + self.save_path = save_path + + def start_requests(self): + return [scrapy.Request( + 'https://apiv2.aminer.cn/magic?a=__mostinfluentialscholars.GetDomainList___', + callback=self.parse_domain_list, method='POST', + body='[{"action":"mostinfluentialscholars.GetDomainList","parameters":{"year":2019}}]' + )] + + def parse_domain_list(self, response): + domains = json.loads(response.body)['data'][0]['item'] + body_fmt = '[{"action":"ai2000v2.GetDomainTopScholars","parameters":{"year_filter":2020,"domain":"%s","top_n":100,"type":"AI 2000"}}]' + for domain in domains: + yield scrapy.Request( + 'https://apiv2.aminer.cn/magic?a=__ai2000v2.GetDomainTopScholars___', + method='POST', body=body_fmt % domain['id'], + cb_kwargs={'domain_name': domain['name']} + ) + + def parse(self, response, **kwargs): + domain_name = kwargs['domain_name'] + scholars = json.loads(response.body)['data'][0]['data'] + for i, scholar in enumerate(scholars[:100]): + yield ScholarItem( + name=scholar['person']['name'], org=scholar['org_en'], + field=domain_name, rank=i + ) + + +class JsonWriterPipeline: + + def open_spider(self, spider): + self.scholar_rank = defaultdict(lambda: [None] * 100) + self.save_path = spider.save_path + + def process_item(self, item, spider): + scholar = ItemAdapter(item).asdict() + self.scholar_rank[scholar.pop('field')][scholar.pop('rank')] = scholar + return item + + def close_spider(self, spider): + with open(os.path.join(self.save_path), 'w', encoding='utf8') as f: + json.dump(self.scholar_rank, f, ensure_ascii=False) diff --git a/gnnrec/kgrec/data/preprocess/analyze.py b/gnnrec/kgrec/data/preprocess/analyze.py new file mode 100644 index 0000000..b798e55 --- /dev/null +++ b/gnnrec/kgrec/data/preprocess/analyze.py @@ -0,0 +1,41 @@ +import argparse +from collections import Counter + +from gnnrec.kgrec.data.preprocess.utils import iter_lines + + +def analyze(args): + total = 0 + max_fields = set() + min_fields = None + field_count = Counter() + sample = None + for d in iter_lines(args.raw_path, args.type): + total += 1 + keys = [k for k in d if d[k]] + max_fields.update(keys) + if min_fields is None: + min_fields = set(keys) + else: + min_fields.intersection_update(keys) + field_count.update(keys) + if len(keys) == len(max_fields): + sample = d + print('数据类型:', args.type) + print('总量:', total) + print('最大字段集合:', max_fields) + print('最小字段集合:', min_fields) + print('字段出现比例:', {k: v / total for k, v in field_count.items()}) + print('示例:', sample) + + +def main(): + parser = argparse.ArgumentParser(description='分析OAG MAG数据集的字段') + parser.add_argument('type', choices=['author', 'paper', 'venue', 'affiliation'], help='数据类型') + parser.add_argument('raw_path', help='原始zip文件所在目录') + args = parser.parse_args() + analyze(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/kgrec/data/preprocess/build_author_rank.py b/gnnrec/kgrec/data/preprocess/build_author_rank.py new file mode 100644 index 0000000..8ffac78 --- /dev/null +++ b/gnnrec/kgrec/data/preprocess/build_author_rank.py @@ -0,0 +1,202 @@ +import argparse +import json +import math +import random + +import dgl +import dgl.function as fn +import django +import numpy as np +import torch +from dgl.ops import edge_softmax +from sklearn.metrics import ndcg_score +from tqdm import tqdm + +from gnnrec.config import DATA_DIR +from gnnrec.hge.utils import set_random_seed, add_reverse_edges +from gnnrec.kgrec.data import OAGCSDataset +from gnnrec.kgrec.utils import iter_json, precision_at_k, recall_at_k + + +def build_ground_truth_valid(args): + """从AI 2000抓取的学者排名数据匹配学者id,作为学者排名ground truth验证集。""" + field_map = { + 'AAAI/IJCAI': 'artificial intelligence', + 'Machine Learning': 'machine learning', + 'Computer Vision': 'computer vision', + 'Natural Language Processing': 'natural language processing', + 'Robotics': 'robotics', + 'Knowledge Engineering': 'knowledge engineering', + 'Speech Recognition': 'speech recognition', + 'Data Mining': 'data mining', + 'Information Retrieval and Recommendation': 'information retrieval', + 'Database': 'database', + 'Human-Computer Interaction': 'human computer interaction', + 'Computer Graphics': 'computer graphics', + 'Multimedia': 'multimedia', + 'Visualization': 'visualization', + 'Security and Privacy': 'security privacy', + 'Computer Networking': 'computer network', + 'Computer Systems': 'operating system', + 'Theory': 'theory', + 'Chip Technology': 'chip', + 'Internet of Things': 'internet of things', + } + with open(DATA_DIR / 'rank/ai2000.json', encoding='utf8') as f: + ai2000_author_rank = json.load(f) + + django.setup() + from rank.models import Author + + author_rank = {} + for field, scholars in ai2000_author_rank.items(): + aid = [] + for s in scholars: + qs = Author.objects.filter(name=s['name'], institution__name=s['org']).order_by('-n_citation') + if qs.exists(): + aid.append(qs[0].id) + else: + qs = Author.objects.filter(name=s['name']).order_by('-n_citation') + aid.append(qs[0].id if qs.exists() else -1) + author_rank[field_map[field]] = aid + if not args.use_field_name: + field2id = {f['name']: i for i, f in enumerate(iter_json(DATA_DIR / 'oag/cs/mag_fields.txt'))} + author_rank = {field2id[f]: aid for f, aid in author_rank.items()} + + with open(DATA_DIR / 'rank/author_rank_val.json', 'w') as f: + json.dump(author_rank, f) + print('结果已保存到', f.name) + + +def build_ground_truth_train(args): + """根据某个领域的论文引用数加权求和构造学者排名,作为ground truth训练集。""" + data = OAGCSDataset() + g = data[0] + g.nodes['paper'].data['citation'] = g.nodes['paper'].data['citation'].float().log1p() + g.edges['writes'].data['order'] = g.edges['writes'].data['order'].float() + apg = g['author', 'writes', 'paper'] + + # 1.筛选论文数>=num_papers的领域 + field_in_degree, fid = g.in_degrees(g.nodes('field'), etype='has_field').sort(descending=True) + fid = fid[field_in_degree >= args.num_papers].tolist() + + # 2.对每个领域召回论文,构造学者-论文子图,通过论文引用数之和对学者排名 + author_rank = {} + for i in tqdm(fid): + pid, _ = g.in_edges(i, etype='has_field') + sg = add_reverse_edges(dgl.in_subgraph(apg, {'paper': pid}, relabel_nodes=True)) + + # 第k作者的权重为1/k,最后一个视为通讯作者,权重为1/2 + sg.edges['writes'].data['w'] = 1.0 / sg.edges['writes'].data['order'] + sg.update_all(fn.copy_e('w', 'w'), fn.min('w', 'mw'), etype='writes') + sg.apply_edges(fn.copy_u('mw', 'mw'), etype='writes_rev') + w, mw = sg.edges['writes'].data.pop('w'), sg.edges['writes_rev'].data.pop('mw') + w[w == mw] = 0.5 + + # 每篇论文所有作者的权重归一化,每个学者所有论文的引用数加权求和 + p = edge_softmax(sg['author', 'writes', 'paper'], torch.log(w).unsqueeze(dim=1)) + sg.edges['writes_rev'].data['p'] = p.squeeze(dim=1) + sg.update_all(fn.u_mul_e('citation', 'p', 'c'), fn.sum('c', 'c'), etype='writes_rev') + author_citation = sg.nodes['author'].data['c'] + + _, aid = author_citation.topk(args.num_authors) + aid = sg.nodes['author'].data[dgl.NID][aid] + author_rank[i] = aid.tolist() + if args.use_field_name: + fields = [f['name'] for f in iter_json(DATA_DIR / 'oag/cs/mag_fields.txt')] + author_rank = {fields[i]: aid for i, aid in author_rank.items()} + + with open(DATA_DIR / 'rank/author_rank_train.json', 'w') as f: + json.dump(author_rank, f) + print('结果已保存到', f.name) + + +def evaluate_ground_truth(args): + """评估ground truth训练集的质量。""" + with open(DATA_DIR / 'rank/author_rank_val.json') as f: + author_rank_val = json.load(f) + with open(DATA_DIR / 'rank/author_rank_train.json') as f: + author_rank_train = json.load(f) + fields = list(set(author_rank_val) & set(author_rank_train)) + author_rank_val = {k: v for k, v in author_rank_val.items() if k in fields} + author_rank_train = {k: v for k, v in author_rank_train.items() if k in fields} + + num_authors = OAGCSDataset()[0].num_nodes('author') + true_relevance = np.zeros((len(fields), num_authors), dtype=np.int32) + scores = np.zeros_like(true_relevance) + for i, f in enumerate(fields): + for r, a in enumerate(author_rank_val[f]): + if a != -1: + true_relevance[i, a] = math.ceil((100 - r) / 10) + for r, a in enumerate(author_rank_train[f]): + scores[i, a] = len(author_rank_train[f]) - r + + for k in (100, 50, 20, 10, 5): + print('nDGC@{0}={1:.4f}\tPrecision@{0}={2:.4f}\tRecall@{0}={3:.4f}'.format( + k, ndcg_score(true_relevance, scores, k=k, ignore_ties=True), + sum(precision_at_k(author_rank_val[f], author_rank_train[f], k) for f in fields) / len(fields), + sum(recall_at_k(author_rank_val[f], author_rank_train[f], k) for f in fields) / len(fields) + )) + + +def sample_triplets(args): + set_random_seed(args.seed) + with open(DATA_DIR / 'rank/author_rank_train.json') as f: + author_rank = json.load(f) + + # 三元组:(t, ap, an),表示对于领域t,学者ap的排名在an之前 + triplets = [] + for fid, aid in author_rank.items(): + fid = int(fid) + n = len(aid) + easy_margin, hard_margin = int(n * args.easy_margin), int(n * args.hard_margin) + num_triplets = min(args.max_num, 2 * n - easy_margin - hard_margin) + num_hard = int(num_triplets * args.hard_ratio) + num_easy = num_triplets - num_hard + triplets.extend( + (fid, aid[i], aid[i + easy_margin]) + for i in random.sample(range(n - easy_margin), num_easy) + ) + triplets.extend( + (fid, aid[i], aid[i + hard_margin]) + for i in random.sample(range(n - hard_margin), num_hard) + ) + + with open(DATA_DIR / 'rank/author_rank_triplets.txt', 'w') as f: + for t, ap, an in triplets: + f.write(f'{t} {ap} {an}\n') + print('结果已保存到', f.name) + + +def main(): + parser = argparse.ArgumentParser(description='基于oag-cs数据集构造学者排名数据集') + subparsers = parser.add_subparsers() + + build_val_parser = subparsers.add_parser('build-val', help='构造学者排名验证集') + build_val_parser.add_argument('--use-field-name', action='store_true', help='使用领域名称(用于调试)') + build_val_parser.set_defaults(func=build_ground_truth_valid) + + build_train_parser = subparsers.add_parser('build-train', help='构造学者排名训练集') + build_train_parser.add_argument('--num-papers', type=int, default=5000, help='筛选领域的论文数阈值') + build_train_parser.add_argument('--num-authors', type=int, default=100, help='每个领域取top k的学者数量') + build_train_parser.add_argument('--use-field-name', action='store_true', help='使用领域名称(用于调试)') + build_train_parser.set_defaults(func=build_ground_truth_train) + + evaluate_parser = subparsers.add_parser('eval', help='评估ground truth训练集的质量') + evaluate_parser.set_defaults(func=evaluate_ground_truth) + + sample_parser = subparsers.add_parser('sample', help='采样三元组') + sample_parser.add_argument('--seed', type=int, default=0, help='随机数种子') + sample_parser.add_argument('--max-num', type=int, default=100, help='每个领域采样三元组最大数量') + sample_parser.add_argument('--easy-margin', type=float, default=0.2, help='简单样本间隔(百分比)') + sample_parser.add_argument('--hard-margin', type=float, default=0.05, help='困难样本间隔(百分比)') + sample_parser.add_argument('--hard-ratio', type=float, default=0.5, help='困难样本比例') + sample_parser.set_defaults(func=sample_triplets) + + args = parser.parse_args() + print(args) + args.func(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/kgrec/data/preprocess/extract_cs.py b/gnnrec/kgrec/data/preprocess/extract_cs.py new file mode 100644 index 0000000..bec05e6 --- /dev/null +++ b/gnnrec/kgrec/data/preprocess/extract_cs.py @@ -0,0 +1,129 @@ +import argparse +import json + +from gnnrec.config import DATA_DIR +from gnnrec.kgrec.data.config import CS, CS_FIELD_L2 +from gnnrec.kgrec.data.preprocess.utils import iter_lines + + +def extract_papers(raw_path): + valid_keys = ['title', 'authors', 'venue', 'year', 'indexed_abstract', 'fos', 'references'] + cs_fields = set(CS_FIELD_L2) + for p in iter_lines(raw_path, 'paper'): + if not all(p.get(k) for k in valid_keys): + continue + fos = {f['name'] for f in p['fos']} + abstract = parse_abstract(p['indexed_abstract']) + if CS in fos and not fos.isdisjoint(cs_fields) \ + and 2010 <= p['year'] <= 2021 \ + and len(p['title']) <= 200 and len(abstract) <= 4000 \ + and 1 <= len(p['authors']) <= 20 and 1 <= len(p['references']) <= 100: + try: + yield { + 'id': p['id'], + 'title': p['title'], + 'authors': [a['id'] for a in p['authors']], + 'venue': p['venue']['id'], + 'year': p['year'], + 'abstract': abstract, + 'fos': list(fos), + 'references': p['references'], + 'n_citation': p.get('n_citation', 0), + } + except KeyError: + pass + + +def parse_abstract(indexed_abstract): + try: + abstract = json.loads(indexed_abstract) + words = [''] * abstract['IndexLength'] + for w, idx in abstract['InvertedIndex'].items(): + for i in idx: + words[i] = w + return ' '.join(words) + except json.JSONDecodeError: + return '' + + +def extract_authors(raw_path, author_ids): + for a in iter_lines(raw_path, 'author'): + if a['id'] in author_ids: + yield { + 'id': a['id'], + 'name': a['name'], + 'org': int(a['last_known_aff_id']) if 'last_known_aff_id' in a else None + } + + +def extract_venues(raw_path, venue_ids): + for v in iter_lines(raw_path, 'venue'): + if v['id'] in venue_ids: + yield {'id': v['id'], 'name': v['DisplayName']} + + +def extract_institutions(raw_path, institution_ids): + for i in iter_lines(raw_path, 'affiliation'): + if i['id'] in institution_ids: + yield {'id': i['id'], 'name': i['DisplayName']} + + +def extract(args): + print('正在抽取计算机领域的论文...') + paper_ids, author_ids, venue_ids, fields = set(), set(), set(), set() + output_path = DATA_DIR / 'oag/cs' + with open(output_path / 'mag_papers.txt', 'w', encoding='utf8') as f: + for p in extract_papers(args.raw_path): + paper_ids.add(p['id']) + author_ids.update(p['authors']) + venue_ids.add(p['venue']) + fields.update(p['fos']) + json.dump(p, f, ensure_ascii=False) + f.write('\n') + print(f'论文抽取完成,已保存到{f.name}') + print(f'论文数{len(paper_ids)},学者数{len(author_ids)},期刊数{len(venue_ids)},领域数{len(fields)}') + + print('正在抽取学者...') + institution_ids = set() + with open(output_path / 'mag_authors.txt', 'w', encoding='utf8') as f: + for a in extract_authors(args.raw_path, author_ids): + if a['org']: + institution_ids.add(a['org']) + json.dump(a, f, ensure_ascii=False) + f.write('\n') + print(f'学者抽取完成,已保存到{f.name}') + print(f'机构数{len(institution_ids)}') + + print('正在抽取期刊...') + with open(output_path / 'mag_venues.txt', 'w', encoding='utf8') as f: + for v in extract_venues(args.raw_path, venue_ids): + json.dump(v, f, ensure_ascii=False) + f.write('\n') + print(f'期刊抽取完成,已保存到{f.name}') + + print('正在抽取机构...') + with open(output_path / 'mag_institutions.txt', 'w', encoding='utf8') as f: + for i in extract_institutions(args.raw_path, institution_ids): + json.dump(i, f, ensure_ascii=False) + f.write('\n') + print(f'机构抽取完成,已保存到{f.name}') + + print('正在抽取领域...') + fields.remove(CS) + fields = sorted(fields) + with open(output_path / 'mag_fields.txt', 'w', encoding='utf8') as f: + for i, field in enumerate(fields): + json.dump({'id': i, 'name': field}, f, ensure_ascii=False) + f.write('\n') + print(f'领域抽取完成,已保存到{f.name}') + + +def main(): + parser = argparse.ArgumentParser(description='抽取OAG数据集计算机领域的子集') + parser.add_argument('raw_path', help='原始zip文件所在目录') + args = parser.parse_args() + extract(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/kgrec/data/preprocess/fine_tune.py b/gnnrec/kgrec/data/preprocess/fine_tune.py new file mode 100644 index 0000000..b544e32 --- /dev/null +++ b/gnnrec/kgrec/data/preprocess/fine_tune.py @@ -0,0 +1,131 @@ +import argparse + +import torch +import torch.optim as optim +from torch.utils.data import DataLoader +from tqdm import tqdm +from transformers import get_linear_schedule_with_warmup + +from gnnrec.config import DATA_DIR, MODEL_DIR +from gnnrec.hge.utils import set_random_seed, get_device, accuracy +from gnnrec.kgrec.data import OAGCSContrastDataset +from gnnrec.kgrec.scibert import ContrastiveSciBERT +from gnnrec.kgrec.utils import iter_json + + +def collate(samples): + return map(list, zip(*samples)) + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + + raw_file = DATA_DIR / 'oag/cs/mag_papers.txt' + train_dataset = OAGCSContrastDataset(raw_file, split='train') + train_loader = DataLoader(train_dataset, batch_size=args.batch_size, shuffle=True, collate_fn=collate) + valid_dataset = OAGCSContrastDataset(raw_file, split='valid') + valid_loader = DataLoader(valid_dataset, batch_size=args.batch_size, shuffle=True, collate_fn=collate) + + model = ContrastiveSciBERT(args.num_hidden, args.tau, device).to(device) + optimizer = optim.AdamW(model.parameters(), lr=args.lr) + total_steps = len(train_loader) * args.epochs + scheduler = get_linear_schedule_with_warmup( + optimizer, num_warmup_steps=total_steps * 0.1, num_training_steps=total_steps + ) + for epoch in range(args.epochs): + model.train() + losses, scores = [], [] + for titles, keywords in tqdm(train_loader): + logits, loss = model(titles, keywords) + labels = torch.arange(len(titles), device=device) + losses.append(loss.item()) + scores.append(score(logits, labels)) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + val_score = evaluate(valid_loader, model, device) + print('Epoch {:d} | Loss {:.4f} | Train Acc {:.4f} | Val Acc {:.4f}'.format( + epoch, sum(losses) / len(losses), sum(scores) / len(scores), val_score + )) + model_save_path = MODEL_DIR / 'scibert.pt' + torch.save(model.state_dict(), model_save_path) + print('模型已保存到', model_save_path) + + +@torch.no_grad() +def evaluate(loader, model, device): + model.eval() + scores = [] + for titles, keywords in tqdm(loader): + logits = model.calc_sim(titles, keywords) + labels = torch.arange(len(titles), device=device) + scores.append(score(logits, labels)) + return sum(scores) / len(scores) + + +def score(logits, labels): + return (accuracy(logits.argmax(dim=1), labels) + accuracy(logits.argmax(dim=0), labels)) / 2 + + +@torch.no_grad() +def infer(args): + device = get_device(args.device) + model = ContrastiveSciBERT(args.num_hidden, args.tau, device).to(device) + model.load_state_dict(torch.load(MODEL_DIR / 'scibert.pt', map_location=device)) + model.eval() + + raw_path = DATA_DIR / 'oag/cs' + dataset = OAGCSContrastDataset(raw_path / 'mag_papers.txt', split='all') + loader = DataLoader(dataset, batch_size=args.batch_size, collate_fn=collate) + print('正在推断论文向量...') + h = [] + for titles, _ in tqdm(loader): + h.append(model.get_embeds(titles).detach().cpu()) + h = torch.cat(h) # (N_paper, d_hid) + h = h / h.norm(dim=1, keepdim=True) + torch.save(h, raw_path / 'paper_feat.pkl') + print('论文向量已保存到', raw_path / 'paper_feat.pkl') + + fields = [f['name'] for f in iter_json(raw_path / 'mag_fields.txt')] + loader = DataLoader(fields, batch_size=args.batch_size) + print('正在推断领域向量...') + h = [] + for fields in tqdm(loader): + h.append(model.get_embeds(fields).detach().cpu()) + h = torch.cat(h) # (N_field, d_hid) + h = h / h.norm(dim=1, keepdim=True) + torch.save(h, raw_path / 'field_feat.pkl') + print('领域向量已保存到', raw_path / 'field_feat.pkl') + + +def main(): + parser = argparse.ArgumentParser(description='通过论文标题和关键词的对比学习对SciBERT模型进行fine-tune') + subparsers = parser.add_subparsers() + + train_parser = subparsers.add_parser('train', help='训练') + train_parser.add_argument('--seed', type=int, default=42, help='随机数种子') + train_parser.add_argument('--device', type=int, default=0, help='GPU设备') + train_parser.add_argument('--num-hidden', type=int, default=128, help='隐藏层维数') + train_parser.add_argument('--tau', type=float, default=0.07, help='温度参数') + train_parser.add_argument('--epochs', type=int, default=5, help='训练epoch数') + train_parser.add_argument('--batch-size', type=int, default=64, help='批大小') + train_parser.add_argument('--lr', type=float, default=5e-5, help='学习率') + train_parser.set_defaults(func=train) + + infer_parser = subparsers.add_parser('infer', help='推断') + infer_parser.add_argument('--device', type=int, default=0, help='GPU设备') + infer_parser.add_argument('--num-hidden', type=int, default=128, help='隐藏层维数') + infer_parser.add_argument('--tau', type=float, default=0.07, help='温度参数') + infer_parser.add_argument('--batch-size', type=int, default=64, help='批大小') + infer_parser.set_defaults(func=infer) + + args = parser.parse_args() + print(args) + args.func(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/kgrec/data/preprocess/utils.py b/gnnrec/kgrec/data/preprocess/utils.py new file mode 100644 index 0000000..fa9508d --- /dev/null +++ b/gnnrec/kgrec/data/preprocess/utils.py @@ -0,0 +1,23 @@ +import os +import tempfile +import zipfile + +from gnnrec.kgrec.utils import iter_json + + +def iter_lines(raw_path, data_type): + """依次迭代OAG数据集某种类型数据所有txt文件的每一行并将JSON解析为字典 + + :param raw_path: str 原始zip文件所在目录 + :param data_type: str 数据类型,author, paper, venue, affiliation之一 + :return: Iterable[dict] + """ + with tempfile.TemporaryDirectory() as tmp: + for zip_file in os.listdir(raw_path): + if zip_file.startswith(f'mag_{data_type}s'): + with zipfile.ZipFile(os.path.join(raw_path, zip_file)) as z: + for txt in z.namelist(): + print(f'{zip_file}\\{txt}') + txt_file = z.extract(txt, tmp) + yield from iter_json(txt_file) + os.remove(txt_file) diff --git a/gnnrec/kgrec/data/readme.md b/gnnrec/kgrec/data/readme.md new file mode 100644 index 0000000..da614b3 --- /dev/null +++ b/gnnrec/kgrec/data/readme.md @@ -0,0 +1,157 @@ +# oag-cs数据集 +## 原始数据 +[Open Academic Graph 2.1](https://www.aminer.cn/oag-2-1) + +使用其中的微软学术(MAG)数据,总大小169 GB + +| 类型 | 文件 | 总量 | +| --- | --- | --- | +| author | mag_authors_{0-1}.zip | 243477150 | +| paper | mag_papers_{0-16}.zip | 240255240 | +| venue | mag_venues.zip | 53422 | +| affiliation | mag_affiliations.zip | 25776 | + +## 字段分析 +假设原始zip文件所在目录为data/oag/mag/ +```shell +python -m gnnrec.kgrec.data.preprocess.analyze author data/oag/mag/ +python -m gnnrec.kgrec.data.preprocess.analyze paper data/oag/mag/ +python -m gnnrec.kgrec.data.preprocess.analyze venue data/oag/mag/ +python -m gnnrec.kgrec.data.preprocess.analyze affiliation data/oag/mag/ +``` + +``` +数据类型: venue +总量: 53422 +最大字段集合: {'JournalId', 'NormalizedName', 'id', 'ConferenceId', 'DisplayName'} +最小字段集合: {'NormalizedName', 'DisplayName', 'id'} +字段出现比例: {'id': 1.0, 'JournalId': 0.9162891692561117, 'DisplayName': 1.0, 'NormalizedName': 1.0, 'ConferenceId': 0.08371083074388828} +示例: {'id': 2898614270, 'JournalId': 2898614270, 'DisplayName': 'Revista de Psiquiatría y Salud Mental', 'NormalizedName': 'revista de psiquiatria y salud mental'} +``` + +``` +数据类型: affiliation +总量: 25776 +最大字段集合: {'id', 'NormalizedName', 'url', 'Latitude', 'Longitude', 'WikiPage', 'DisplayName'} +最小字段集合: {'id', 'NormalizedName', 'Latitude', 'Longitude', 'DisplayName'} +字段出现比例: {'id': 1.0, 'DisplayName': 1.0, 'NormalizedName': 1.0, 'WikiPage': 0.9887880198634389, 'Latitude': 1.0, 'Longitude': 1.0, 'url': 0.6649984481688392} +示例: {'id': 3032752892, 'DisplayName': 'Universidad Internacional de La Rioja', 'NormalizedName': 'universidad internacional de la rioja', 'WikiPage': 'https://en.wikipedia.org/wiki/International_University_of_La_Rioja', 'Latitude': '42.46270', 'Longitude': '2.45500', 'url': 'https://en.unir.net/'} +``` + +``` +数据类型: author +总量: 243477150 +最大字段集合: {'normalized_name', 'name', 'pubs', 'n_pubs', 'n_citation', 'last_known_aff_id', 'id'} +最小字段集合: {'normalized_name', 'name', 'n_pubs', 'pubs', 'id'} +字段出现比例: {'id': 1.0, 'name': 1.0, 'normalized_name': 1.0, 'last_known_aff_id': 0.17816547055853085, 'pubs': 1.0, 'n_pubs': 1.0, 'n_citation': 0.39566894470384595} +示例: {'id': 3040689058, 'name': 'Jeong Hoe Heo', 'normalized_name': 'jeong hoe heo', 'last_known_aff_id': '59412607', 'pubs': [{'i': 2770054759, 'r': 10}], 'n_pubs': 1, 'n_citation': 44} +``` + +``` +数据类型: paper +总量: 240255240 +最大字段集合: {'issue', 'authors', 'page_start', 'publisher', 'doc_type', 'title', 'id', 'doi', 'references', 'volume', 'fos', 'n_citation', 'venue', 'page_end', 'year', 'indexed_abstract', 'url'} +最小字段集合: {'id'} +字段出现比例: {'id': 1.0, 'title': 0.9999999958377599, 'authors': 0.9998381970774082, 'venue': 0.5978255167296247, 'year': 0.9999750931550963, 'page_start': 0.5085962370685443, 'page_end': 0.4468983111460961, 'publisher': 0.5283799512551735, 'issue': 0.41517357124031923, 'url': 0.9414517743712895, 'doi': 0.37333226530251745, 'indexed_abstract': 0.5832887141192009, 'fos': 0.8758779954185391, 'n_citation': 0.3795505812901313, 'doc_type': 0.6272126634990355, 'volume': 0.43235134434528877, 'references': 0.3283648464857624} +示例: { + 'id': 2507145174, + 'title': 'Structure-Activity Relationships and Kinetic Studies of Peptidic Antagonists of CBX Chromodomains.', + 'authors': [{'name': 'Jacob I. Stuckey', 'id': 2277886111, 'org': 'Center for Integrative Chemical Biology and Drug Discovery, Division of Chemical Biology and Medicinal Chemistry, UNC Eshelman School of Pharmacy, University of North Carolina at Chapel Hill , Chapel Hill, North Carolina 27599, United States.\r', 'org_id': 114027177}, {'name': 'Catherine Simpson', 'id': 2098592917, 'org': 'Center for Integrative Chemical Biology and Drug Discovery, Division of Chemical Biology and Medicinal Chemistry, UNC Eshelman School of Pharmacy, University of North Carolina at Chapel Hill , Chapel Hill, North Carolina 27599, United States.\r', 'org_id': 114027177}, ...], + 'venue': {'name': 'Journal of Medicinal Chemistry', 'id': 162030435}, + 'year': 2016, 'n_citation': 13, 'page_start': '8913', 'page_end': '8923', 'doc_type': 'Journal', 'publisher': 'American Chemical Society', 'volume': '59', 'issue': '19', 'doi': '10.1021/ACS.JMEDCHEM.6B00801', + 'references': [1976962550, 1982791788, 1988515229, 2000127174, 2002698073, 2025496265, 2032915605, 2050256263, 2059999434, 2076333986, 2077957449, 2082815186, 2105928678, 2116982909, 2120121380, 2146641795, 2149566960, 2156518222, 2160723017, 2170079272, 2207535250, 2270756322, 2326025506, 2327795699, 2332365177, 2346619380, 2466657786], + 'indexed_abstract': '{"IndexLength":108,"InvertedIndex":{"To":[0],"better":[1],"understand":[2],"the":[3,19,54,70,80,95],"contribution":[4],"of":[5,21,31,47,56,82,90,98],"methyl-lysine":[6],"(Kme)":[7],"binding":[8,33,96],"proteins":[9],"to":[10,79],"various":[11],"disease":[12],"states,":[13],"we":[14,68],"recently":[15],"developed":[16],"and":[17,36,43,63,73,84],"reported":[18],"discovery":[20,46],"1":[22,48,83],"(UNC3866),":[23],"a":[24],"chemical":[25],"probe":[26],"that":[27,77],"targets":[28],"two":[29],"families":[30],"Kme":[32],"proteins,":[34],"CBX":[35],"CDY":[37],"chromodomains,":[38],"with":[39,61,101],"selectivity":[40],"for":[41,87],"CBX4":[42],"-7.":[44],"The":[45],"was":[49],"enabled":[50],"in":[51],"part":[52],"by":[53,93,105],"use":[55],"molecular":[57],"dynamics":[58],"simulations":[59],"performed":[60],"CBX7":[62,102],"its":[64],"endogenous":[65],"substrate.":[66],"Herein,":[67],"describe":[69],"design,":[71],"synthesis,":[72],"structure–activity":[74],"relationship":[75],"studies":[76],"led":[78],"development":[81],"provide":[85],"support":[86],"our":[88,99],"model":[89],"CBX7–ligand":[91],"recognition":[92],"examining":[94],"kinetics":[97],"antagonists":[100],"as":[103],"determined":[104],"surface-plasmon":[106],"resonance.":[107]}}', + 'fos': [{'name': 'chemistry', 'w': 0.36301}, {'name': 'chemical probe', 'w': 0.0}, {'name': 'receptor ligand kinetics', 'w': 0.46173}, {'name': 'dna binding protein', 'w': 0.42292}, {'name': 'biochemistry', 'w': 0.39304}], + 'url': ['https://pubs.acs.org/doi/full/10.1021/acs.jmedchem.6b00801', 'https://www.ncbi.nlm.nih.gov/pubmed/27571219', 'http://pubsdc3.acs.org/doi/abs/10.1021/acs.jmedchem.6b00801'] +} +``` + +## 第1步:抽取计算机领域的子集 +```shell +python -m gnnrec.kgrec.data.preprocess.extract_cs data/oag/mag/ +``` + +筛选近10年计算机领域的论文,从微软学术抓取了计算机科学下的34个二级领域作为领域字段过滤条件,过滤掉主要字段为空的论文 + +二级领域列表:[CS_FIELD_L2](config.py) + +输出5个文件: + +(1)学者:mag_authors.txt + +`{"id": aid, "name": "author name", "org": oid}` + +(2)论文:mag_papers.txt + +``` +{ + "id": pid, + "title": "paper title", + "authors": [aid], + "venue": vid, + "year": year, + "abstract": "abstract", + "fos": ["field"], + "references": [pid], + "n_citation": n_citation +} +``` + +(3)期刊:mag_venues.txt + +`{"id": vid, "name": "venue name"}` + +(4)机构:mag_institutions.txt + +`{"id": oid, "name": "org name"}` + +(5)领域:mag_fields.txt + +`{"id": fid, "name": "field name"}` + +## 第2步:预训练论文和领域向量 +通过论文标题和关键词的**对比学习**对预训练的SciBERT模型进行fine-tune,之后将隐藏层输出的128维向量作为paper和field顶点的输入特征 + +预训练的SciBERT模型来自Transformers [allenai/scibert_scivocab_uncased](https://huggingface.co/allenai/scibert_scivocab_uncased) + +注:由于原始数据不包含关键词,因此使用研究领域(fos字段)作为关键词 + +1. fine-tune +```shell +python -m gnnrec.kgrec.data.preprocess.fine_tune train +``` + +``` +Epoch 0 | Loss 0.3470 | Train Acc 0.9105 | Val Acc 0.9426 +Epoch 1 | Loss 0.1609 | Train Acc 0.9599 | Val Acc 0.9535 +Epoch 2 | Loss 0.1065 | Train Acc 0.9753 | Val Acc 0.9573 +Epoch 3 | Loss 0.0741 | Train Acc 0.9846 | Val Acc 0.9606 +Epoch 4 | Loss 0.0551 | Train Acc 0.9898 | Val Acc 0.9614 +``` + +2. 推断 +```shell +python -m gnnrec.kgrec.data.preprocess.fine_tune infer +``` + +预训练的论文和领域向量分别保存到paper_feat.pkl和field_feat.pkl文件(已归一化), +该向量既可用于GNN模型的输入特征,也可用于计算相似度召回论文 + +## 第3步:构造图数据集 +将以上5个txt和2个pkl文件压缩为oag-cs.zip,得到oag-cs数据集的原始数据 + +将oag-cs.zip文件放到`$DGL_DOWNLOAD_DIR`目录下(环境变量`DGL_DOWNLOAD_DIR`默认为`~/.dgl/`) + +```python +from gnnrec.kgrec.data import OAGCSDataset + +data = OAGCSDataset() +g = data[0] +``` + +统计数据见 [OAGCSDataset](oagcs.py) 的文档字符串 + +## 下载地址 +下载地址:,提取码:2ylp + +大小:1.91 GB,解压后大小:3.93 GB diff --git a/gnnrec/kgrec/data/venue.py b/gnnrec/kgrec/data/venue.py new file mode 100644 index 0000000..857af8f --- /dev/null +++ b/gnnrec/kgrec/data/venue.py @@ -0,0 +1,67 @@ +import dgl +import torch + +from .oagcs import OAGCSDataset + + +class OAGVenueDataset(OAGCSDataset): + """oag-cs期刊分类数据集,删除了venue顶点,作为paper顶点的标签 + + 属性 + ----- + * num_classes: 类别数 + * predict_ntype: 目标顶点类型 + + 增加的paper顶点属性 + ----- + * label: tensor(N_paper) 论文所属期刊(-1~176) + * train_mask, val_mask, test_mask: tensor(N_paper) 数量分别为402457, 280762, 255387,划分方式:年份 + """ + + def load(self): + super().load() + for k in ('train_mask', 'val_mask', 'test_mask'): + self.g.nodes['paper'].data[k] = self.g.nodes['paper'].data[k].bool() + + def process(self): + super().process() + venue_in_degrees = self.g.in_degrees(etype='published_at') + drop_venue_id = torch.nonzero(venue_in_degrees < 1000, as_tuple=True)[0] + # 删除论文数1000以下的期刊,剩余360种 + tmp_g = dgl.remove_nodes(self.g, drop_venue_id, 'venue') + + pv_p, pv_v = tmp_g.edges(etype='published_at') + labels = torch.full((tmp_g.num_nodes('paper'),), -1) + mask = torch.full((tmp_g.num_nodes('paper'),), False) + labels[pv_p] = pv_v + mask[pv_p] = True + + g = dgl.heterograph({etype: tmp_g.edges(etype=etype) for etype in [ + ('author', 'writes', 'paper'), ('paper', 'has_field', 'field'), + ('paper', 'cites', 'paper'), ('author', 'affiliated_with', 'institution') + ]}) + for ntype in g.ntypes: + g.nodes[ntype].data.update(self.g.nodes[ntype].data) + for etype in g.canonical_etypes: + g.edges[etype].data.update(self.g.edges[etype].data) + + year = g.nodes['paper'].data['year'] + g.nodes['paper'].data.update({ + 'label': labels, + 'train_mask': mask & (year < 2015), + 'val_mask': mask & (year >= 2015) & (year < 2018), + 'test_mask': mask & (year >= 2018) + }) + self.g = g + + @property + def name(self): + return 'oag-venue' + + @property + def num_classes(self): + return 360 + + @property + def predict_ntype(self): + return 'paper' diff --git a/gnnrec/kgrec/random_walk.py b/gnnrec/kgrec/random_walk.py new file mode 100644 index 0000000..f94e5f3 --- /dev/null +++ b/gnnrec/kgrec/random_walk.py @@ -0,0 +1,28 @@ +import argparse + +from gnnrec.hge.metapath2vec.random_walk import random_walk +from gnnrec.hge.utils import add_reverse_edges +from gnnrec.kgrec.data import OAGCSDataset + + +def main(): + parser = argparse.ArgumentParser(description='oag-cs数据集 metapath2vec基于元路径的随机游走') + parser.add_argument('--num-walks', type=int, default=4, help='每个顶点游走次数') + parser.add_argument('--walk-length', type=int, default=10, help='元路径重复次数') + parser.add_argument('output_file', help='输出文件名') + args = parser.parse_args() + + data = OAGCSDataset() + g = add_reverse_edges(data[0]) + metapaths = { + 'author': ['writes', 'published_at', 'published_at_rev', 'writes_rev'], # APVPA + 'paper': ['writes_rev', 'writes', 'published_at', 'published_at_rev', 'has_field', 'has_field_rev'], # PAPVPFP + 'venue': ['published_at_rev', 'writes_rev', 'writes', 'published_at'], # VPAPV + 'field': ['has_field_rev', 'writes_rev', 'writes', 'has_field'], # FPAPF + 'institution': ['affiliated_with_rev', 'writes', 'writes_rev', 'affiliated_with'] # IAPAI + } + random_walk(g, metapaths, args.num_walks, args.walk_length, args.output_file) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/kgrec/rank.py b/gnnrec/kgrec/rank.py new file mode 100644 index 0000000..1068c7e --- /dev/null +++ b/gnnrec/kgrec/rank.py @@ -0,0 +1,32 @@ +import json + +from gnnrec.config import DATA_DIR + + +class Context: + + def __init__(self, recall_ctx, author_rank): + """学者排名模块上下文对象 + + :param recall_ctx: gnnrec.kgrec.recall.Context + :param author_rank: {field_id: [author_id]} 领域学者排名 + """ + self.recall_ctx = recall_ctx + # 之后需要:author_embeds + self.author_rank = author_rank + + +def get_context(recall_ctx): + with open(DATA_DIR / 'rank/author_rank_train.json') as f: + author_rank = json.load(f) + return Context(recall_ctx, author_rank) + + +def rank(ctx, query): + """根据输入的查询词在oag-cs数据集计算学者排名 + + :param ctx: Context 上下文对象 + :param query: str 查询词 + :return: List[float], List[int] 学者得分和id,按得分降序排序 + """ + return [], ctx.author_rank.get(query, []) diff --git a/gnnrec/kgrec/readme.md b/gnnrec/kgrec/readme.md new file mode 100644 index 0000000..86b07fa --- /dev/null +++ b/gnnrec/kgrec/readme.md @@ -0,0 +1,120 @@ +# 基于图神经网络的推荐算法 +## 数据集 +oag-cs - 使用OAG微软学术数据构造的计算机领域的学术网络(见 [readme](data/readme.md)) + +## 预训练顶点嵌入 +使用metapath2vec(随机游走+word2vec)预训练顶点嵌入,作为GNN模型的顶点输入特征 +1. 随机游走 +```shell +python -m gnnrec.kgrec.random_walk model/word2vec/oag_cs_corpus.txt +``` + +2. 训练词向量 +```shell +python -m gnnrec.hge.metapath2vec.train_word2vec --size=128 --workers=8 model/word2vec/oag_cs_corpus.txt model/word2vec/oag_cs.model +``` + +## 召回 +使用微调后的SciBERT模型(见 [readme](data/readme.md) 第2步)将查询词编码为向量,与预先计算好的论文标题向量计算余弦相似度,取top k +```shell +python -m gnnrec.kgrec.recall +``` + +召回结果示例: + +graph neural network +``` +0.9629 Aggregation Graph Neural Networks +0.9579 Neural Graph Learning: Training Neural Networks Using Graphs +0.9556 Heterogeneous Graph Neural Network +0.9552 Neural Graph Machines: Learning Neural Networks Using Graphs +0.9490 On the choice of graph neural network architectures +0.9474 Measuring and Improving the Use of Graph Information in Graph Neural Networks +0.9362 Challenging the generalization capabilities of Graph Neural Networks for network modeling +0.9295 Strategies for Pre-training Graph Neural Networks +0.9142 Supervised Neural Network Models for Processing Graphs +0.9112 Geometrically Principled Connections in Graph Neural Networks +``` + +recommendation algorithm based on knowledge graph +``` +0.9172 Research on Video Recommendation Algorithm Based on Knowledge Reasoning of Knowledge Graph +0.8972 An Improved Recommendation Algorithm in Knowledge Network +0.8558 A personalized recommendation algorithm based on interest graph +0.8431 An Improved Recommendation Algorithm Based on Graph Model +0.8334 The Research of Recommendation Algorithm based on Complete Tripartite Graph Model +0.8220 Recommendation Algorithm based on Link Prediction and Domain Knowledge in Retail Transactions +0.8167 Recommendation Algorithm Based on Graph-Model Considering User Background Information +0.8034 A Tripartite Graph Recommendation Algorithm Based on Item Information and User Preference +0.7774 Improvement of TF-IDF Algorithm Based on Knowledge Graph +0.7770 Graph Searching Algorithms for Semantic-Social Recommendation +``` + +scholar disambiguation +``` +0.9690 Scholar search-oriented author disambiguation +0.9040 Author name disambiguation in scientific collaboration and mobility cases +0.8901 Exploring author name disambiguation on PubMed-scale +0.8852 Author Name Disambiguation in Heterogeneous Academic Networks +0.8797 KDD Cup 2013: author disambiguation +0.8796 A survey of author name disambiguation techniques: 2010–2016 +0.8721 Who is Who: Name Disambiguation in Large-Scale Scientific Literature +0.8660 Use of ResearchGate and Google CSE for author name disambiguation +0.8643 Automatic Methods for Disambiguating Author Names in Bibliographic Data Repositories +0.8641 A brief survey of automatic methods for author name disambiguation +``` + +## 精排 +### 构造ground truth +(1)验证集 + +从AMiner发布的 [AI 2000人工智能全球最具影响力学者榜单](https://www.aminer.cn/ai2000) 抓取人工智能20个子领域的top 100学者 +```shell +pip install scrapy>=2.3.0 +cd gnnrec/kgrec/data/preprocess +scrapy runspider ai2000_crawler.py -a save_path=/home/zzy/GNN-Recommendation/data/rank/ai2000.json +``` + +与oag-cs数据集的学者匹配,并人工确认一些排名较高但未匹配上的学者,作为学者排名ground truth验证集 +```shell +export DJANGO_SETTINGS_MODULE=academic_graph.settings.common +export SECRET_KEY=xxx +python -m gnnrec.kgrec.data.preprocess.build_author_rank build-val +``` + +(2)训练集 + +参考AI 2000的计算公式,根据某个领域的论文引用数加权求和构造学者排名,作为ground truth训练集 + +计算公式: +![计算公式](https://originalfileserver.aminer.cn/data/ranks/%E5%AD%A6%E8%80%85%E8%91%97%E4%BD%9C%E5%85%AC%E5%BC%8F.png) +即:假设一篇论文有n个作者,第k作者的权重为1/k,最后一个视为通讯作者,权重为1/2,归一化之后计算论文引用数的加权求和 + +```shell +python -m gnnrec.kgrec.data.preprocess.build_author_rank build-train +``` + +(3)评估ground truth训练集的质量 +```shell +python -m gnnrec.kgrec.data.preprocess.build_author_rank eval +``` + +``` +nDGC@100=0.2420 Precision@100=0.1859 Recall@100=0.2016 +nDGC@50=0.2308 Precision@50=0.2494 Recall@50=0.1351 +nDGC@20=0.2492 Precision@20=0.3118 Recall@20=0.0678 +nDGC@10=0.2743 Precision@10=0.3471 Recall@10=0.0376 +nDGC@5=0.3165 Precision@5=0.3765 Recall@5=0.0203 +``` + +(4)采样三元组 + +从学者排名训练集中采样三元组(t, ap, an),表示对于领域t,学者ap的排名在an之前 +```shell +python -m gnnrec.kgrec.data.preprocess.build_author_rank sample +``` + +### 训练GNN模型 +```shell +python -m gnnrec.kgrec.train model/word2vec/oag-cs.model model/garec_gnn.pt data/rank/author_embed.pt +``` diff --git a/gnnrec/kgrec/recall.py b/gnnrec/kgrec/recall.py new file mode 100644 index 0000000..8674f81 --- /dev/null +++ b/gnnrec/kgrec/recall.py @@ -0,0 +1,53 @@ +import torch + +from gnnrec.config import DATA_DIR, MODEL_DIR +from gnnrec.kgrec.data import OAGCSContrastDataset +from gnnrec.kgrec.scibert import ContrastiveSciBERT + + +class Context: + + def __init__(self, paper_embeds, scibert_model): + """论文召回模块上下文对象 + + :param paper_embeds: tensor(N, d) 论文标题向量 + :param scibert_model: ContrastiveSciBERT 微调后的SciBERT模型 + """ + self.paper_embeds = paper_embeds + self.scibert_model = scibert_model + + +def get_context(): + paper_embeds = torch.load(DATA_DIR / 'oag/cs/paper_feat.pkl', map_location='cpu') + scibert_model = ContrastiveSciBERT(128, 0.07) + scibert_model.load_state_dict(torch.load(MODEL_DIR / 'scibert.pt', map_location='cpu')) + return Context(paper_embeds, scibert_model) + + +def recall(ctx, query, k=1000): + """根据输入的查询词在oag-cs数据集召回论文 + + :param ctx: Context 上下文对象 + :param query: str 查询词 + :param k: int, optional 召回论文数量,默认为1000 + :return: List[float], List[int] Top k论文的相似度和id,按相似度降序排序 + """ + q = ctx.scibert_model.get_embeds(query) # (1, d) + q = q / q.norm() + similarity = torch.mm(ctx.paper_embeds, q.t()).squeeze(dim=1) # (N,) + score, pid = similarity.topk(k, dim=0) + return score.tolist(), pid.tolist() + + +def main(): + ctx = get_context() + paper_titles = OAGCSContrastDataset(DATA_DIR / 'oag/cs/mag_papers.txt', 'all') + while True: + query = input('query> ').strip() + score, pid = recall(ctx, query, 10) + for i in range(len(pid)): + print('{:.4f}\t{}'.format(score[i], paper_titles[pid[i]][0])) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/kgrec/scibert.py b/gnnrec/kgrec/scibert.py new file mode 100644 index 0000000..d72c8c6 --- /dev/null +++ b/gnnrec/kgrec/scibert.py @@ -0,0 +1,61 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers import AutoTokenizer, AutoModel + + +class ContrastiveSciBERT(nn.Module): + + def __init__(self, out_dim, tau, device='cpu'): + """用于对比学习的SciBERT模型 + + :param out_dim: int 输出特征维数 + :param tau: float 温度参数τ + :param device: torch.device, optional 默认为CPU + """ + super().__init__() + self.tau = tau + self.device = device + self.tokenizer = AutoTokenizer.from_pretrained('allenai/scibert_scivocab_uncased') + self.model = AutoModel.from_pretrained('allenai/scibert_scivocab_uncased').to(device) + self.linear = nn.Linear(self.model.config.hidden_size, out_dim) + + def get_embeds(self, texts, max_length=64): + """将文本编码为向量 + + :param texts: List[str] 输入文本列表,长度为N + :param max_length: int, optional padding最大长度,默认为64 + :return: tensor(N, d_out) + """ + encoded = self.tokenizer( + texts, padding='max_length', truncation=True, max_length=max_length, return_tensors='pt' + ).to(self.device) + return self.linear(self.model(**encoded).pooler_output) + + def calc_sim(self, texts_a, texts_b): + """计算两组文本的相似度 + + :param texts_a: List[str] 输入文本A列表,长度为N + :param texts_b: List[str] 输入文本B列表,长度为N + :return: tensor(N, N) 相似度矩阵,S[i, j] = cos(a[i], b[j]) / τ + """ + embeds_a = self.get_embeds(texts_a) # (N, d_out) + embeds_b = self.get_embeds(texts_b) # (N, d_out) + embeds_a = embeds_a / embeds_a.norm(dim=1, keepdim=True) + embeds_b = embeds_b / embeds_b.norm(dim=1, keepdim=True) + return embeds_a @ embeds_b.t() / self.tau + + def forward(self, texts_a, texts_b): + """计算两组文本的对比损失 + + :param texts_a: List[str] 输入文本A列表,长度为N + :param texts_b: List[str] 输入文本B列表,长度为N + :return: tensor(N, N), float A对B的相似度矩阵,对比损失 + """ + # logits_ab等价于预测概率,对比损失等价于交叉熵损失 + logits_ab = self.calc_sim(texts_a, texts_b) + logits_ba = logits_ab.t() + labels = torch.arange(len(texts_a), device=self.device) + loss_ab = F.cross_entropy(logits_ab, labels) + loss_ba = F.cross_entropy(logits_ba, labels) + return logits_ab, (loss_ab + loss_ba) / 2 diff --git a/gnnrec/kgrec/train.py b/gnnrec/kgrec/train.py new file mode 100644 index 0000000..5104dac --- /dev/null +++ b/gnnrec/kgrec/train.py @@ -0,0 +1,133 @@ +import argparse +import json +import math +import warnings + +import numpy as np +import torch +import torch.optim as optim +import torch.nn.functional as F +from dgl.dataloading import MultiLayerNeighborSampler, NodeDataLoader +from sklearn.metrics import ndcg_score +from tqdm import tqdm + +from gnnrec.config import DATA_DIR +from gnnrec.hge.rhgnn.model import RHGNN +from gnnrec.hge.utils import set_random_seed, get_device, add_reverse_edges, add_node_feat +from gnnrec.kgrec.data import OAGCSDataset +from gnnrec.kgrec.utils import TripletNodeDataLoader + + +def load_data(device): + g = add_reverse_edges(OAGCSDataset()[0]).to(device) + field_feat = g.nodes['field'].data['feat'] + + with open(DATA_DIR / 'rank/author_rank_triplets.txt') as f: + triplets = torch.tensor([[int(x) for x in line.split()] for line in f], device=device) + + with open(DATA_DIR / 'rank/author_rank_train.json') as f: + author_rank_train = json.load(f) + train_fields = list(author_rank_train) + true_relevance = np.zeros((len(train_fields), g.num_nodes('author')), dtype=np.int32) + for i, f in enumerate(train_fields): + for r, a in enumerate(author_rank_train[f]): + true_relevance[i, a] = math.ceil((100 - r) / 10) + train_fields = list(map(int, train_fields)) + + return g, field_feat, triplets, true_relevance, train_fields + + +def train(args): + set_random_seed(args.seed) + device = get_device(args.device) + g, field_feat, triplets, true_relevance, train_fields = load_data(device) + add_node_feat(g, 'pretrained', args.node_embed_path) + + sampler = MultiLayerNeighborSampler([args.neighbor_size] * args.num_layers) + triplet_loader = TripletNodeDataLoader(g, triplets, sampler, device, batch_size=args.batch_size) + node_loader = NodeDataLoader(g, {'author': g.nodes('author')}, sampler, device=device, batch_size=args.batch_size) + + model = RHGNN( + {ntype: g.nodes[ntype].data['feat'].shape[1] for ntype in g.ntypes}, + args.num_hidden, field_feat.shape[1], args.num_rel_hidden, args.num_rel_hidden, + args.num_heads, g.ntypes, g.canonical_etypes, 'author', args.num_layers, args.dropout + ).to(device) + optimizer = optim.Adam(model.parameters(), lr=args.lr) + scheduler = optim.lr_scheduler.CosineAnnealingLR( + optimizer, T_max=len(triplet_loader) * args.epochs, eta_min=args.lr / 100 + ) + warnings.filterwarnings('ignore', 'Setting attributes on ParameterDict is not supported') + for epoch in range(args.epochs): + model.train() + losses = [] + for batch, output_nodes, blocks in tqdm(triplet_loader): + batch_logits = model(blocks, blocks[0].srcdata['feat']) + aid_map = {a: i for i, a in enumerate(output_nodes.tolist())} + anchor = field_feat[batch[:, 0]] + positive = batch_logits[[aid_map[a] for a in batch[:, 1].tolist()]] + negative = batch_logits[[aid_map[a] for a in batch[:, 2].tolist()]] + loss = F.triplet_margin_loss(anchor, positive, negative) + losses.append(loss.item()) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + torch.cuda.empty_cache() + print('Epoch {:d} | Loss {:.4f}'.format(epoch, sum(losses) / len(losses))) + torch.save(model.state_dict(), args.model_save_path) + if epoch % args.eval_every == 0 or epoch == args.epochs - 1: + print('nDCG@{}={:.4f}'.format(args.k, evaluate( + model, node_loader, g, field_feat.shape[1], 'author', + field_feat[train_fields], true_relevance, args.k + ))) + torch.save(model.state_dict(), args.model_save_path) + print('模型已保存到', args.model_save_path) + + author_embeds = infer(model, node_loader, g, field_feat.shape[1], 'author') + torch.save(author_embeds.cpu(), args.author_embed_save_path) + print('学者嵌入已保存到', args.author_embed_save_path) + + +@torch.no_grad() +def evaluate(model, loader, g, out_dim, predict_ntype, field_feat, true_relevance, k): + embeds = infer(model, loader, g, out_dim, predict_ntype) + scores = torch.mm(field_feat, embeds.t()).detach().cpu().numpy() + return ndcg_score(true_relevance, scores, k=k, ignore_ties=True) + + +@torch.no_grad() +def infer(model, loader, g, out_dim, predict_ntype): + model.eval() + embeds = torch.zeros((g.num_nodes(predict_ntype), out_dim), device=g.device) + for _, output_nodes, blocks in tqdm(loader): + embeds[output_nodes[predict_ntype]] = model(blocks, blocks[0].srcdata['feat']) + return embeds + + +def main(): + parser = argparse.ArgumentParser(description='GARec算法 训练GNN模型') + parser.add_argument('--seed', type=int, default=0, help='随机数种子') + parser.add_argument('--device', type=int, default=0, help='GPU设备') + # R-HGNN + parser.add_argument('--num-hidden', type=int, default=64, help='隐藏层维数') + parser.add_argument('--num-rel-hidden', type=int, default=8, help='关系表示的隐藏层维数') + parser.add_argument('--num-heads', type=int, default=8, help='注意力头数') + parser.add_argument('--num-layers', type=int, default=2, help='层数') + parser.add_argument('--dropout', type=float, default=0.5, help='Dropout概率') + parser.add_argument('--epochs', type=int, default=200, help='训练epoch数') + parser.add_argument('--batch-size', type=int, default=1024, help='批大小') + parser.add_argument('--neighbor-size', type=int, default=10, help='邻居采样数') + parser.add_argument('--lr', type=float, default=0.001, help='学习率') + parser.add_argument('--eval-every', type=int, default=10, help='每多少个epoch评价一次') + parser.add_argument('-k', type=int, default=20, help='评价指标只考虑top k的学者') + parser.add_argument('node_embed_path', help='预训练顶点嵌入路径') + parser.add_argument('model_save_path', help='模型保存路径') + parser.add_argument('author_embed_save_path', help='学者嵌入保存路径') + args = parser.parse_args() + print(args) + train(args) + + +if __name__ == '__main__': + main() diff --git a/gnnrec/kgrec/utils/__init__.py b/gnnrec/kgrec/utils/__init__.py new file mode 100644 index 0000000..82f6dbb --- /dev/null +++ b/gnnrec/kgrec/utils/__init__.py @@ -0,0 +1,2 @@ +from .data import * +from .metrics import * diff --git a/gnnrec/kgrec/utils/data.py b/gnnrec/kgrec/utils/data.py new file mode 100644 index 0000000..f2bcba2 --- /dev/null +++ b/gnnrec/kgrec/utils/data.py @@ -0,0 +1,64 @@ +import json + +import dgl +import torch +from dgl.dataloading import Collator +from dgl.utils import to_dgl_context +from torch.utils.data import DataLoader + + +def iter_json(filename): + """遍历每行一个JSON格式的文件。""" + with open(filename, encoding='utf8') as f: + for line in f: + yield json.loads(line) + + +class TripletNodeCollator(Collator): + + def __init__(self, g, triplets, block_sampler, ntype): + """用于OAGCSAuthorRankDataset数据集的NodeCollator + + :param g: DGLGraph 异构图 + :param triplets: tensor(N, 3) (t, ap, an)三元组 + :param block_sampler: BlockSampler 邻居采样器 + :param ntype: str 目标顶点类型 + """ + self.g = g + self.triplets = triplets + self.block_sampler = block_sampler + self.ntype = ntype + + def collate(self, items): + """根据三元组中的学者id构造子图 + + :param items: List[tensor(3)] 一个批次的三元组 + :return: tensor(N_src), tensor(N_dst), List[DGLBlock] (input_nodes, output_nodes, blocks) + """ + items = torch.stack(items, dim=0) + seed_nodes = items[:, 1:].flatten().unique() + blocks = self.block_sampler.sample_blocks(self.g, {self.ntype: seed_nodes}) + output_nodes = blocks[-1].dstnodes[self.ntype].data[dgl.NID] + return items, output_nodes, blocks + + @property + def dataset(self): + return self.triplets + + +class TripletNodeDataLoader(DataLoader): + + def __init__(self, g, triplets, block_sampler, device=None, **kwargs): + """用于OAGCSAuthorRankDataset数据集的NodeDataLoader + + :param g: DGLGraph 异构图 + :param triplets: tensor(N, 3) (t, ap, an)三元组 + :param block_sampler: BlockSampler 邻居采样器 + :param device: torch.device + :param kwargs: DataLoader的其他参数 + """ + if device is None: + device = g.device + block_sampler.set_output_context(to_dgl_context(device)) + self.collator = TripletNodeCollator(g, triplets, block_sampler, 'author') + super().__init__(triplets, collate_fn=self.collator.collate, **kwargs) diff --git a/gnnrec/kgrec/utils/metrics.py b/gnnrec/kgrec/utils/metrics.py new file mode 100644 index 0000000..5adcb3a --- /dev/null +++ b/gnnrec/kgrec/utils/metrics.py @@ -0,0 +1,10 @@ +def precision_at_k(y_true, y_pred, k): + y_true = set(y_true) + y_pred = set(y_pred[:k]) + return len(set(y_true & y_pred)) / k + + +def recall_at_k(y_true, y_pred, k): + y_true = set(y_true) + y_pred = set(y_pred[:k]) + return len(set(y_true & y_pred)) / len(y_true) diff --git a/img/GARec.png b/img/GARec.png new file mode 100644 index 0000000000000000000000000000000000000000..fc2179fd0e2e67b7262c52dd76063bf7ab57adfd GIT binary patch literal 28008 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 literal 0 HcmV?d00001 diff --git a/img/学者详情.png b/img/学者详情.png new file mode 100644 index 0000000000000000000000000000000000000000..e6c68b70f20a942a376f3f71aac5dca71dc7bba7 GIT binary patch literal 123951 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 literal 0 HcmV?d00001 diff --git a/img/搜索学者.png b/img/搜索学者.png new file mode 100644 index 0000000000000000000000000000000000000000..ee7f55a7476601881ea10dc0385345c6344e3c6b GIT binary patch literal 50098 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%0Dnb*D<9mFsPlh> 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

5;?QmMG+Hj=aja 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 literal 0 HcmV?d00001 diff --git a/img/论文详情.png b/img/论文详情.png new file mode 100644 index 0000000000000000000000000000000000000000..e17ee14fabd91fb543a1d9a08e26656fdea6f65c GIT binary patch literal 123517 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т1lkTQ*iIxCX5l9D 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 z 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引用图 + * [x] 尝试增加C&S后处理步骤(重点是标签传播图的构造) + * [x] R-HGNN+C&S → 正样本图上微提升,引用图上下降 + * [x] HeCo+C&S → 26.32% -> 27.7% +* 2021.8.16~8.22 + * [x] 尝试HeCo的最终嵌入使用z_sc → 提升10%! + * [x] 尝试将HeCo的网络结构编码器替换为R-HGNN +* 2021.8.23~8.29 + * [x] 写中期报告 +* 2021.8.30~9.5 + * [x] 尝试将RHCO的网络结构编码器改为两层 → 提升4.4% + * [x] 尝试其他构造正样本图的方法:训练集使用真实标签,验证集和测试集使用HGT预测 → 提升0.6% +* 2021.9.6~9.12 + * [x] 尝试将构造正样本图的方法改为使用预训练的R-HGNN模型计算的注意力权重、训练集使用真实标签 → 下降2.6% + * [x] 使用metapath2vec预训练oag-cs数据集的顶点嵌入,备用 +* 2021.9.13~9.19 + * [x] RHCO模型删除输入特征转换和dropout(网络结构编码器已包含)、增加学习率调度器(对比损失权重为0时应该达到与R-HGNN相似的性能) → +10% + * [x] 设计推荐算法:使用SciBERT+对比学习实现召回:一篇论文的标题和关键词是一对正样本,使用(一个或两个)SciBERT分别将标题和关键词编码为向量, + 计算对比损失,以此方式进行微调;使用微调后的SciBERT模型将论文标题和输入关键词编码为向量,计算相似度即可召回与查询最相关的论文 +* 2021.9.20~9.26 + * [x] 在oag-cs数据集上使用SciBERT+对比学习进行微调 + * [x] 实现输入关键词召回论文的功能 +* 2021.9.27~10.10 + * [x] 实现推荐算法的精排部分 + * [x] 重新构造oag-cs数据集,使field顶点包含所有领域词 + * [x] 在oag-cs数据集上训练RHCO模型(预测任务:期刊分类),获取顶点表示向量 + * [x] 修改训练代码,使其能够适配不同的数据集 + * TODO 预测任务改为学者排名相关(例如学者和领域顶点的相似度),需要先获取ground truth:学者在某个领域的论文引用数之和,排序 + * [x] 初步实现可视化系统 + * [x] 创建Django项目(直接使用当前根目录即可) + * [x] 创建数据库,将oag-cs数据导入数据库 + * [x] 实现论文召回的可视化 +* 2021.10.11~10.17 + * 精排部分GNN模型训练思路: + * (1)对于领域t召回论文,得到论文关联的学者集合,通过论文引用数之和构造学者排名; + * (2)从排名中采样N个三元组(t, ap, an),其中学者ap的排名在an之前,采样应包含简单样本(例如第1名和第10名)和困难样本(例如第1名和第3名); + * (3)计算三元组损失triplet_loss(t, ap, an) = d(t, ap) - d(t, an) + α + * [x] 可视化系统:实现查看论文详情、学者详情等基本功能 + * [x] 开始写毕业论文 + * [x] 第一章 绪论 +* 2021.10.18~10.24 + * [x] 异构图表示学习:增加ACM和DBLP数据集 + * [x] 写毕业论文 + * [x] 第二章 基础理论 + * [x] 第三章 基于对比学习的异构图表示学习模型 +* 2021.10.25~10.31 + * [x] 完成毕业论文初稿 + * [x] 第四章 基于图神经网络的学术推荐算法 + * [x] 第五章 学术推荐系统设计与实现 + * [x] 异构图表示学习 + * [x] 正样本图改为类型的邻居各对应一个(PAP, PFP),使用注意力组合 + * [x] 尝试:网络结构编码器由R-HGNN改为HGConv → ACM: -3.6%, DBLP: +4%, ogbn-mag: -1.86% +* 2021.11.1~11.7 + * [x] 异构图表示学习 + * [x] 完成参数敏感性分析 + * [x] 推荐算法精排部分 + * [x] 抓取AMiner AI 2000的人工智能学者榜单作为学者排名验证集 + * [x] 参考AI 2000的计算公式,使用某个领域的论文引用数加权求和构造学者排名ground truth训练集,采样三元组 + * [x] 训练:使用三元组损失训练GNN模型 +* 2021.11.8~11.14 + * [ ] 异构图表示学习 + * [x] 增加oag-cs期刊分类数据集 + * [ ] 完成消融实验 + * [ ] 推荐算法精排部分 + * [ ] 训练:目前的评价方式有问题,改为先召回论文再计算相关学者与领域的相似度(即与预测步骤相同) + * [ ] 预测:对于召回的论文构造子图,利用顶点嵌入计算查询词与学者的相似度,实现学者排名 diff --git a/rank/__init__.py b/rank/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rank/admin.py b/rank/admin.py new file mode 100644 index 0000000..87f28c4 --- /dev/null +++ b/rank/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from .models import Author, Paper, Venue, Institution, Field + + +class AuthorAdmin(admin.ModelAdmin): + raw_id_fields = ['institution'] + + +class PaperAdmin(admin.ModelAdmin): + raw_id_fields = ['authors', 'venue', 'fos', 'references'] + + +admin.site.register(Author, AuthorAdmin) +admin.site.register(Paper, PaperAdmin) +admin.site.register(Venue) +admin.site.register(Institution) +admin.site.register(Field) diff --git a/rank/apps.py b/rank/apps.py new file mode 100644 index 0000000..dc66d42 --- /dev/null +++ b/rank/apps.py @@ -0,0 +1,15 @@ +from django.apps import AppConfig +from django.conf import settings + +from gnnrec.kgrec import recall, rank + + +class RankConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'rank' + + def ready(self): + if not settings.TESTING: + from . import views + views.recall_ctx = recall.get_context() + views.rank_ctx = rank.get_context(views.recall_ctx) diff --git a/rank/management/__init__.py b/rank/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rank/management/commands/__init__.py b/rank/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rank/management/commands/loadoagcs.py b/rank/management/commands/loadoagcs.py new file mode 100644 index 0000000..8219f7d --- /dev/null +++ b/rank/management/commands/loadoagcs.py @@ -0,0 +1,95 @@ +import dgl +import dgl.function as fn +from django.core.management import BaseCommand +from tqdm import trange + +from gnnrec.config import DATA_DIR +from gnnrec.kgrec.data import OAGCSDataset +from gnnrec.kgrec.utils import iter_json +from rank.models import Venue, Institution, Field, Author, Paper, Writes + + +class Command(BaseCommand): + help = '将oag-cs数据集导入数据库' + + def add_arguments(self, parser): + parser.add_argument('--batch-size', type=int, default=2000, help='批大小') + + def handle(self, *args, **options): + batch_size = options['batch_size'] + raw_path = DATA_DIR / 'oag/cs' + + print('正在导入期刊数据...') + Venue.objects.bulk_create([ + Venue(id=i, name=v['name']) + for i, v in enumerate(iter_json(raw_path / 'mag_venues.txt')) + ], batch_size=batch_size) + vid_map = {v['id']: i for i, v in enumerate(iter_json(raw_path / 'mag_venues.txt'))} + + print('正在导入机构数据...') + Institution.objects.bulk_create([ + Institution(id=i, name=o['name']) + for i, o in enumerate(iter_json(raw_path / 'mag_institutions.txt')) + ], batch_size=batch_size) + oid_map = {o['id']: i for i, o in enumerate(iter_json(raw_path / 'mag_institutions.txt'))} + + print('正在导入领域数据...') + Field.objects.bulk_create([ + Field(id=i, name=f['name']) + for i, f in enumerate(iter_json(raw_path / 'mag_fields.txt')) + ], batch_size=batch_size) + + data = OAGCSDataset() + g = data[0] + apg = dgl.reverse(g['author', 'writes', 'paper'], copy_ndata=False) + apg.nodes['paper'].data['c'] = g.nodes['paper'].data['citation'].float() + apg.update_all(fn.copy_u('c', 'm'), fn.sum('m', 'c')) + author_citation = apg.nodes['author'].data['c'].int().tolist() + + print('正在导入学者数据...') + Author.objects.bulk_create([ + Author( + id=i, name=a['name'], n_citation=author_citation[i], + institution_id=oid_map[a['org']] if a['org'] is not None else None + ) for i, a in enumerate(iter_json(raw_path / 'mag_authors.txt')) + ], batch_size=batch_size) + + print('正在导入论文数据...') + Paper.objects.bulk_create([ + Paper( + id=i, title=p['title'], venue_id=vid_map[p['venue']], year=p['year'], + abstract=p['abstract'], n_citation=p['n_citation'] + ) for i, p in enumerate(iter_json(raw_path / 'mag_papers.txt')) + ], batch_size=batch_size) + + print('正在导入论文关联数据(很慢)...') + print('writes') + u, v = g.edges(etype='writes') + order = g.edges['writes'].data['order'] + edges = list(zip(u.tolist(), v.tolist(), order.tolist())) + for i in trange(0, len(edges), batch_size): + Writes.objects.bulk_create([ + Writes(author_id=a, paper_id=p, order=r) + for a, p, r in edges[i:i + batch_size] + ]) + + print('has_field') + u, v = g.edges(etype='has_field') + edges = list(zip(u.tolist(), v.tolist())) + HasField = Paper.fos.through + for i in trange(0, len(edges), batch_size): + HasField.objects.bulk_create([ + HasField(paper_id=p, field_id=f) + for p, f in edges[i:i + batch_size] + ]) + + print('cites') + u, v = g.edges(etype='cites') + edges = list(zip(u.tolist(), v.tolist())) + Cites = Paper.references.through + for i in trange(0, len(edges), batch_size): + Cites.objects.bulk_create([ + Cites(from_paper_id=p, to_paper_id=r) + for p, r in edges[i:i + batch_size] + ]) + print('导入完成') diff --git a/rank/migrations/0001_initial.py b/rank/migrations/0001_initial.py new file mode 100644 index 0000000..6d1a214 --- /dev/null +++ b/rank/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# Generated by Django 3.2.8 on 2021-11-03 13:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Author', + fields=[ + ('id', models.BigIntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(db_index=True, max_length=255)), + ('n_citation', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='Field', + fields=[ + ('id', models.BigIntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255, unique=True)), + ], + ), + migrations.CreateModel( + name='Institution', + fields=[ + ('id', models.BigIntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(db_index=True, max_length=255)), + ], + ), + migrations.CreateModel( + name='Paper', + fields=[ + ('id', models.BigIntegerField(primary_key=True, serialize=False)), + ('title', models.CharField(db_index=True, max_length=255)), + ('year', models.IntegerField()), + ('abstract', models.CharField(max_length=4095)), + ('n_citation', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='Venue', + fields=[ + ('id', models.BigIntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(db_index=True, max_length=255)), + ], + ), + migrations.CreateModel( + name='Writes', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.IntegerField(default=1)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rank.author')), + ('paper', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rank.paper')), + ], + ), + migrations.AddField( + model_name='paper', + name='authors', + field=models.ManyToManyField(related_name='papers', through='rank.Writes', to='rank.Author'), + ), + migrations.AddField( + model_name='paper', + name='fos', + field=models.ManyToManyField(to='rank.Field'), + ), + migrations.AddField( + model_name='paper', + name='references', + field=models.ManyToManyField(related_name='citations', to='rank.Paper'), + ), + migrations.AddField( + model_name='paper', + name='venue', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='rank.venue'), + ), + migrations.AddField( + model_name='author', + name='institution', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='rank.institution'), + ), + migrations.AddConstraint( + model_name='writes', + constraint=models.UniqueConstraint(fields=('author', 'paper'), name='unique_writes'), + ), + ] diff --git a/rank/migrations/0002_alter_writes_ordering.py b/rank/migrations/0002_alter_writes_ordering.py new file mode 100644 index 0000000..171bce1 --- /dev/null +++ b/rank/migrations/0002_alter_writes_ordering.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.8 on 2021-11-04 04:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('rank', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='writes', + options={'ordering': ['paper_id', 'order']}, + ), + ] diff --git a/rank/migrations/__init__.py b/rank/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rank/models.py b/rank/models.py new file mode 100644 index 0000000..21e2a29 --- /dev/null +++ b/rank/models.py @@ -0,0 +1,63 @@ +from django.db import models + + +class Venue(models.Model): + id = models.BigIntegerField(primary_key=True) + name = models.CharField(max_length=255, db_index=True) + + def __str__(self): + return self.name + + +class Institution(models.Model): + id = models.BigIntegerField(primary_key=True) + name = models.CharField(max_length=255, db_index=True) + + def __str__(self): + return self.name + + +class Field(models.Model): + id = models.BigIntegerField(primary_key=True) + name = models.CharField(max_length=255, unique=True) + + def __str__(self): + return self.name + + +class Author(models.Model): + id = models.BigIntegerField(primary_key=True) + name = models.CharField(max_length=255, db_index=True) + institution = models.ForeignKey(Institution, on_delete=models.SET_NULL, null=True) + n_citation = models.IntegerField(default=0) + + def __str__(self): + return self.name + + +class Paper(models.Model): + id = models.BigIntegerField(primary_key=True) + title = models.CharField(max_length=255, db_index=True) + authors = models.ManyToManyField(Author, related_name='papers', through='Writes') + venue = models.ForeignKey(Venue, on_delete=models.SET_NULL, null=True) + year = models.IntegerField() + abstract = models.CharField(max_length=4095) + fos = models.ManyToManyField(Field) + references = models.ManyToManyField('self', related_name='citations', symmetrical=False) + n_citation = models.IntegerField(default=0) + + def __str__(self): + return self.title + + +class Writes(models.Model): + author = models.ForeignKey(Author, on_delete=models.CASCADE) + paper = models.ForeignKey(Paper, on_delete=models.CASCADE) + order = models.IntegerField(default=1) + + class Meta: + constraints = [models.UniqueConstraint(fields=['author', 'paper'], name='unique_writes')] + ordering = ['paper_id', 'order'] + + def __str__(self): + return f'(author_id={self.author_id}, paper_id={self.paper_id}, order={self.order})' diff --git a/rank/templates/rank/_author_list.html b/rank/templates/rank/_author_list.html new file mode 100644 index 0000000..0601644 --- /dev/null +++ b/rank/templates/rank/_author_list.html @@ -0,0 +1,13 @@ +{% for author in object_list %} + +{% endfor %} diff --git a/rank/templates/rank/_paper_list.html b/rank/templates/rank/_paper_list.html new file mode 100644 index 0000000..0f2602f --- /dev/null +++ b/rank/templates/rank/_paper_list.html @@ -0,0 +1,20 @@ +{% for paper in object_list %} +
+
+
+
+ {{ paper.title }} +
+ {{ paper.n_citation }} citations +
+
{{ paper.year }} {{ paper.venue }}
+
+ {% for author in paper.authors.all %} + {{ author }} + {% if not forloop.last %}; {% endif %} + {% endfor %} +
+

{{ paper.abstract|truncatewords:50 }}

+
+
+{% endfor %} diff --git a/rank/templates/rank/author_detail.html b/rank/templates/rank/author_detail.html new file mode 100644 index 0000000..d8482aa --- /dev/null +++ b/rank/templates/rank/author_detail.html @@ -0,0 +1,13 @@ +{% extends 'rank/base.html' %} + +{% block title %}学者详情{% endblock %} + +{% block content %} +

{{ author.name }}

+

{% firstof author.institution %}

+
+

{{ author.papers.count }} papers

+

{{ author.n_citation }} citations

+
+ {% include 'rank/_paper_list.html' %} +{% endblock %} diff --git a/rank/templates/rank/author_rank.html b/rank/templates/rank/author_rank.html new file mode 100644 index 0000000..a0ac965 --- /dev/null +++ b/rank/templates/rank/author_rank.html @@ -0,0 +1,12 @@ +{% extends 'rank/base.html' %} + +{% block title %}学者排名{% endblock %} + +{% block content %} +
+ + +
+ {% include 'rank/_author_list.html' %} +{% endblock %} diff --git a/rank/templates/rank/base.html b/rank/templates/rank/base.html new file mode 100644 index 0000000..daa1c04 --- /dev/null +++ b/rank/templates/rank/base.html @@ -0,0 +1,54 @@ + + + + + {% block title %}Academic Graph{% endblock %} + + + + + + + + +
+ +
+
+
+ {% block content %}{% endblock %} +
+
+
+
+ Author: ZZy +
+
+ + diff --git a/rank/templates/rank/index.html b/rank/templates/rank/index.html new file mode 100644 index 0000000..b316a7d --- /dev/null +++ b/rank/templates/rank/index.html @@ -0,0 +1,10 @@ +{% extends 'rank/base.html' %} + +{% block content %} +

学术推荐系统

+
+ + +
+{% endblock %} diff --git a/rank/templates/rank/login.html b/rank/templates/rank/login.html new file mode 100644 index 0000000..c5e1ceb --- /dev/null +++ b/rank/templates/rank/login.html @@ -0,0 +1,24 @@ +{% extends 'rank/base.html' %} + +{% block title %}用户登录{% endblock %} + +{% block content %} +
+

用户登录

+ {% if message %} +
{{ message }}
+ {% endif %} +
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+{% endblock %} diff --git a/rank/templates/rank/paper_detail.html b/rank/templates/rank/paper_detail.html new file mode 100644 index 0000000..a1496aa --- /dev/null +++ b/rank/templates/rank/paper_detail.html @@ -0,0 +1,24 @@ +{% extends 'rank/base.html' %} + +{% block title %}论文详情{% endblock %} + +{% block content %} +

{{ paper.title }}

+

{{ paper.year }} {{ paper.venue }}

+
+ {% for author in paper.authors.all %} + {{ author }} + {% if not forloop.last %}; {% endif %} + {% endfor %} +
+
+

{{ paper.references.count }} references

+

{{ paper.n_citation }} citations

+
+

Abstract

+

{{ paper.abstract }}

+

Fields

+ {% for field in paper.fos.all %} + {{ field }} + {% endfor %} +{% endblock %} diff --git a/rank/templates/rank/register.html b/rank/templates/rank/register.html new file mode 100644 index 0000000..5412d77 --- /dev/null +++ b/rank/templates/rank/register.html @@ -0,0 +1,38 @@ +{% extends 'rank/base.html' %} + +{% block title %}用户注册{% endblock %} + +{% block content %} +
+

用户注册

+ {% if message %} +
{{ message }}
+ {% endif %} +
+ {% csrf_token %} +
+ + + 只能包含字母、数字和下划线 +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endblock %} diff --git a/rank/templates/rank/search_author.html b/rank/templates/rank/search_author.html new file mode 100644 index 0000000..4fc655e --- /dev/null +++ b/rank/templates/rank/search_author.html @@ -0,0 +1,15 @@ +{% extends 'rank/base.html' %} + +{% block title %}搜索学者{% endblock %} + +{% block content %} +
+ + +
+ {% include 'rank/_author_list.html' %} + {% if q and not object_list %} +

未找到学者{{ q }}

+ {% endif %} +{% endblock %} diff --git a/rank/templates/rank/search_paper.html b/rank/templates/rank/search_paper.html new file mode 100644 index 0000000..c2e9b5f --- /dev/null +++ b/rank/templates/rank/search_paper.html @@ -0,0 +1,12 @@ +{% extends 'rank/base.html' %} + +{% block title %}搜索论文{% endblock %} + +{% block content %} +
+ + +
+ {% include 'rank/_paper_list.html' %} +{% endblock %} diff --git a/rank/tests.py b/rank/tests.py new file mode 100644 index 0000000..73747b1 --- /dev/null +++ b/rank/tests.py @@ -0,0 +1,217 @@ +from unittest.mock import patch +from urllib.parse import quote + +from django.conf import settings +from django.contrib.auth import SESSION_KEY +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse + +from .models import Author, Paper, Writes + + +def create_test_data(): + User.objects.create_user('alice', 'alice@example.com', '1234') + Author.objects.bulk_create([ + Author(id=i, name=f'A{i}', n_citation=c) + for i, c in enumerate([4, 5, 3]) + ]) + papers = Paper.objects.bulk_create([ + Paper(id=i, title=f'P{i}', year=2021, abstract='', n_citation=3 - i) + for i in range(3) + ]) + writes = [[0, 1], [1, 2], [0, 2]] + Writes.objects.bulk_create(reversed([ + Writes(author_id=a, paper_id=p, order=r + 1) + for p, authors in enumerate(writes) for r, a in enumerate(authors) + ])) + for i, r in enumerate([[], [0], [0, 1]]): + papers[i].references.set(r) + + +class LoginViewTests(TestCase): + + @classmethod + def setUpTestData(cls): + create_test_data() + + def test_get(self): + response = self.client.get(reverse('rank:login')) + self.assertTemplateUsed(response, 'rank/login.html') + + def test_get_already_login(self): + self.client.post(reverse('rank:login'), data={'username': 'alice', 'password': '1234'}) + response = self.client.get(reverse('rank:login')) + self.assertRedirects(response, reverse('rank:index')) + + def test_ok(self): + data = {'username': 'alice', 'password': '1234'} + response = self.client.post(reverse('rank:login'), data) + self.assertEqual('1', self.client.session[SESSION_KEY]) + self.assertRedirects(response, reverse('rank:index')) + + def test_redirect(self): + redirect_url = reverse('rank:index') + '?foo=123&bar=abc' + login_url = '{}?next={}'.format(reverse('rank:login'), quote(redirect_url)) + response = self.client.get(login_url) + self.assertContains(response, 'action="{}"'.format(login_url)) + + data = {'username': 'alice', 'password': '1234'} + response = self.client.post(login_url, data) + self.assertRedirects(response, redirect_url) + + def test_wrong_username_or_password(self): + data = {'username': 'alice', 'password': '5678'} + response = self.client.post(reverse('rank:login'), data) + self.assertTemplateUsed(response, 'rank/login.html') + self.assertContains(response, '用户名或密码错误') + + +class RegisterViewTests(TestCase): + + @classmethod + def setUpTestData(cls): + create_test_data() + + def test_get(self): + response = self.client.get(reverse('rank:register')) + self.assertTemplateUsed(response, 'rank/register.html') + + def test_invalid_username(self): + data = {'username': '@#%', 'password': '1234', 'password2': '1234'} + response = self.client.post(reverse('rank:register'), data) + self.assertEqual('用户名只能包含字母、数字和下划线', response.context['message']) + + def test_username_already_exists(self): + data = {'username': 'alice', 'password': '1234', 'password2': '1234'} + response = self.client.post(reverse('rank:register'), data) + self.assertEqual('用户名已存在', response.context['message']) + + def test_passwords_not_match(self): + data = {'username': 'cindy', 'password': '1234', 'password2': '5678'} + response = self.client.post(reverse('rank:register'), data) + self.assertEqual('两次密码不一致', response.context['message']) + + def test_ok(self): + data = {'username': 'bob', 'password': '1234', 'password2': '1234', 'name': '', 'email': ''} + response = self.client.post(reverse('rank:register'), data) + self.assertRedirects(response, reverse('rank:login')) + self.assertTrue(User.objects.filter(username='bob').exists()) + + +class SearchPaperViewTests(TestCase): + + @classmethod + def setUpTestData(cls): + create_test_data() + + def setUp(self): + self.client.post(reverse('rank:login'), data={'username': 'alice', 'password': '1234'}) + + @patch('gnnrec.kgrec.recall.recall', return_value=(None, [1, 2])) + def test_ok(self, recall): + response = self.client.get(reverse('rank:search-paper'), data={'q': 'xxx'}) + self.assertEqual(200, response.status_code) + self.assertTemplateUsed(response, 'rank/search_paper.html') + self.assertQuerysetEqual(response.context['object_list'], ['P1', 'P2'], transform=str) + recall.assert_called_with(None, 'xxx', settings.PAGE_SIZE) + + +class PaperDetailViewTests(TestCase): + + @classmethod + def setUpTestData(cls): + create_test_data() + + def setUp(self): + self.client.post(reverse('rank:login'), data={'username': 'alice', 'password': '1234'}) + + def test_ok(self): + response = self.client.get(reverse('rank:paper-detail', args=(1,))) + self.assertEqual(200, response.status_code) + self.assertTemplateUsed(response, 'rank/paper_detail.html') + self.assertContains(response, 'P1') + + def test_not_found(self): + response = self.client.get(reverse('rank:paper-detail', args=(999,))) + self.assertEqual(404, response.status_code) + + +class AuthorDetailViewTests(TestCase): + + @classmethod + def setUpTestData(cls): + create_test_data() + + def setUp(self): + self.client.post(reverse('rank:login'), data={'username': 'alice', 'password': '1234'}) + + def test_ok(self): + response = self.client.get(reverse('rank:author-detail', args=(0,))) + self.assertEqual(200, response.status_code) + self.assertTemplateUsed(response, 'rank/author_detail.html') + self.assertContains(response, 'A0') + self.assertContains(response, '4 citations') + self.assertQuerysetEqual(response.context['object_list'], ['P0', 'P2'], transform=str) + + def test_not_found(self): + response = self.client.get(reverse('rank:author-detail', args=(999,))) + self.assertEqual(404, response.status_code) + + +class SearchAuthorViewTests(TestCase): + + @classmethod + def setUpTestData(cls): + create_test_data() + + def setUp(self): + self.client.post(reverse('rank:login'), data={'username': 'alice', 'password': '1234'}) + + def test_ok(self): + response = self.client.get(reverse('rank:search-author'), data={'q': 'A0'}) + self.assertEqual(200, response.status_code) + self.assertTemplateUsed(response, 'rank/search_author.html') + self.assertQuerysetEqual(response.context['object_list'], ['A0'], transform=str) + + def test_no_result(self): + response = self.client.get(reverse('rank:search-author'), data={'q': 'xxx'}) + self.assertQuerysetEqual(response.context['object_list'], [], transform=str) + self.assertContains(response, '未找到学者xxx') + + +class AuthorRankViewTests(TestCase): + + @classmethod + def setUpTestData(cls): + create_test_data() + + def setUp(self): + self.client.post(reverse('rank:login'), data={'username': 'alice', 'password': '1234'}) + + @patch('gnnrec.kgrec.rank.rank', return_value=(None, [1, 0])) + def test_ok(self, rank): + response = self.client.get(reverse('rank:author-rank'), data={'q': 'xxx'}) + self.assertEqual(200, response.status_code) + self.assertTemplateUsed(response, 'rank/author_rank.html') + self.assertQuerysetEqual(response.context['object_list'], ['A1', 'A0'], transform=str) + rank.assert_called_with(None, 'xxx') + + def test_not_login(self): + self.client.get(reverse('rank:logout')) + response = self.client.get(reverse('rank:author-rank'), {'q': 'xxx'}) + self.assertRedirects(response, '{}?next={}'.format( + reverse('rank:login'), quote(reverse('rank:author-rank') + '?q=xxx') + )) + + +class WritesModelTests(TestCase): + + @classmethod + def setUpTestData(cls): + create_test_data() + + def test_ordering(self): + writes = Writes.objects.filter(paper_id=2) + expected = ['(author_id=0, paper_id=2, order=1)', '(author_id=2, paper_id=2, order=2)'] + self.assertQuerysetEqual(writes, expected, transform=str) diff --git a/rank/urls.py b/rank/urls.py new file mode 100644 index 0000000..69d97fb --- /dev/null +++ b/rank/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from . import views + +app_name = 'rank' +urlpatterns = [ + path('login/', views.LoginView.as_view(), name='login'), + path('logout/', views.logout_view, name='logout'), + path('register/', views.RegisterView.as_view(), name='register'), + + path('', views.index, name='index'), + path('search-paper/', views.SearchPaperView.as_view(), name='search-paper'), + path('paper//', views.PaperDetailView.as_view(), name='paper-detail'), + path('author//', views.AuthorDetailView.as_view(), name='author-detail'), + path('author-rank/', views.AuthorRankView.as_view(), name='author-rank'), + path('search-author/', views.SearchAuthorView.as_view(), name='search-author'), +] diff --git a/rank/views.py b/rank/views.py new file mode 100644 index 0000000..6fc48ab --- /dev/null +++ b/rank/views.py @@ -0,0 +1,143 @@ +import re + +from django.conf import settings +from django.contrib.auth import authenticate, login, logout, REDIRECT_FIELD_NAME +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.models import User +from django.shortcuts import render, redirect +from django.views import View +from django.views.generic import ListView, DetailView +from django.views.generic.detail import SingleObjectMixin + +from gnnrec.kgrec import recall, rank +from .models import Author, Paper + + +class LoginView(View): + + def get(self, request): + if request.user.is_authenticated: + return redirect('rank:index') + return render(request, 'rank/login.html', {'login_url': request.get_full_path()}) + + def post(self, request): + username = request.POST.get('username') + password = request.POST.get('password') + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + return redirect(self.get_redirect_url()) + else: + return render(request, 'rank/login.html', {'message': '用户名或密码错误'}) + + def get_redirect_url(self): + return self.request.POST.get(REDIRECT_FIELD_NAME) \ + or self.request.GET.get(REDIRECT_FIELD_NAME, 'rank:index') + + +def logout_view(request): + logout(request) + return redirect('rank:login') + + +class RegisterView(View): + + def get(self, request): + return render(request, 'rank/register.html') + + def post(self, request): + username = request.POST.get('username') + password = request.POST.get('password') + password2 = request.POST.get('password2') + name = request.POST.get('name') + email = request.POST.get('email') + message = '' + + if not re.fullmatch('[0-9A-Za-z_]+', username): + message = '用户名只能包含字母、数字和下划线' + elif User.objects.filter(username=username).exists(): + message = '用户名已存在' + elif password != password2: + message = '两次密码不一致' + + if message: + return render(request, 'rank/register.html', {'message': message}) + User.objects.create_user(username, email, password, first_name=name) + return redirect('rank:login') + + +@login_required +def index(request): + return render(request, 'rank/index.html') + + +# 召回和学者排名模块上下文对象,在RankConfig.ready()中初始化 +recall_ctx = None +rank_ctx = None + + +class SearchPaperView(LoginRequiredMixin, ListView): + template_name = 'rank/search_paper.html' + + def get_queryset(self): + if not self.request.GET.get('q'): + return Paper.objects.none() + _, pid = recall.recall(recall_ctx, self.request.GET['q'], settings.PAGE_SIZE) + return sorted(Paper.objects.filter(id__in=pid), key=lambda p: pid.index(p.id)) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['q'] = self.request.GET.get('q', '') + return context + + +class PaperDetailView(LoginRequiredMixin, DetailView): + model = Paper + + +# 参考 https://docs.djangoproject.com/en/3.2/topics/class-based-views/mixins/#using-singleobjectmixin-with-listview +class AuthorDetailView(LoginRequiredMixin, SingleObjectMixin, ListView): + template_name = 'rank/author_detail.html' + paginate_by = settings.PAGE_SIZE + + def get(self, request, *args, **kwargs): + self.object = self.get_object(queryset=Author.objects.all()) + return super().get(request, *args, **kwargs) + + def get_queryset(self): + return self.object.papers.order_by('-n_citation') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['author'] = self.object + return context + + +class SearchAuthorView(LoginRequiredMixin, ListView): + template_name = 'rank/search_author.html' + + def get_queryset(self): + if not self.request.GET.get('q'): + return Author.objects.none() + return Author.objects.filter(name=self.request.GET['q']) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['q'] = self.request.GET.get('q', '') + return context + + +class AuthorRankView(LoginRequiredMixin, ListView): + template_name = 'rank/author_rank.html' + + def get_queryset(self): + if not self.request.GET.get('q'): + return Author.objects.none() + _, aid = rank.rank(rank_ctx, self.request.GET['q']) + return sorted(Author.objects.filter(id__in=aid), key=lambda a: aid.index(a.id)) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['q'] = self.request.GET.get('q', '') + return context diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..05a905d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +-f https://data.dgl.ai/wheels/repo.html +dgl==0.7.0 +django>=3.2.8 +gensim>=3.8.3 +matplotlib>=3.3.1 +mysqlclient>=2.0.3 +numpy>=1.20.1 +ogb>=1.2.5 +pandas>=1.2.2 +scikit-learn>=0.24.1 +scipy>=1.6.1 +-f https://download.pytorch.org/whl/torch_stable.html +torch==1.7.1+cpu +tqdm>=4.57.0 +transformers>=4.2.2 diff --git a/requirements_cuda.txt b/requirements_cuda.txt new file mode 100644 index 0000000..b3eae6c --- /dev/null +++ b/requirements_cuda.txt @@ -0,0 +1,15 @@ +-f https://data.dgl.ai/wheels/repo.html +dgl==0.7.0-cu110 +django>=3.2.8 +gensim>=3.8.3 +matplotlib>=3.3.1 +mysqlclient>=2.0.3 +numpy>=1.20.1 +ogb>=1.2.5 +pandas>=1.2.2 +scikit-learn>=0.24.1 +scipy>=1.6.1 +-f https://download.pytorch.org/whl/torch_stable.html +torch==1.7.1+cu110 +tqdm>=4.57.0 +transformers>=4.2.2
+
+
+
+ {{ author.name }} +
+ {{ author.n_citation }} citations +
+
{{ author.institution }}
+
+