Update README_OLD_BACKUP.md
Browse files- README_OLD_BACKUP.md +0 -872
README_OLD_BACKUP.md
CHANGED
|
@@ -1,872 +0,0 @@
|
|
| 1 |
-
<!-- ============================================================ -->
|
| 2 |
-
<!-- CAFF - Context-Aware Feedback Filtering -->
|
| 3 |
-
<!-- Official repository README -->
|
| 4 |
-
<!-- ============================================================ -->
|
| 5 |
-
|
| 6 |
-
<h1 align="center">
|
| 7 |
-
CAFF: Context-Aware Feedback Filtering for Multi-Hop Biomedical Knowledge Graph Evidence Selection
|
| 8 |
-
</h1>
|
| 9 |
-
|
| 10 |
-
<p align="center">
|
| 11 |
-
<a href="#"><img alt="Paper" src="https://img.shields.io/badge/Paper-IEEE%20TKDE%20(under%20review)-1f6feb?style=flat-square"></a>
|
| 12 |
-
<a href="#license"><img alt="License" src="https://img.shields.io/badge/License-MIT-2ea44f?style=flat-square"></a>
|
| 13 |
-
<img alt="Python" src="https://img.shields.io/badge/Python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white">
|
| 14 |
-
<img alt="PyTorch" src="https://img.shields.io/badge/PyTorch-2.0%2B-EE4C2C?style=flat-square&logo=pytorch&logoColor=white">
|
| 15 |
-
<img alt="CUDA" src="https://img.shields.io/badge/CUDA-11.8-76B900?style=flat-square&logo=nvidia&logoColor=white">
|
| 16 |
-
<img alt="Status" src="https://img.shields.io/badge/Status-Research%20Code-orange?style=flat-square">
|
| 17 |
-
</p>
|
| 18 |
-
|
| 19 |
-
<p align="center">
|
| 20 |
-
<b>Marwan Dhifallah</b><sup>*</sup> · <b>Yu Liu</b><br>
|
| 21 |
-
<i>Dalian University of Technology, Dalian, China</i><br>
|
| 22 |
-
<code>marwan@mail.dlut.edu.cn</code> · <code>yuliu@dlut.edu.cn</code>
|
| 23 |
-
</p>
|
| 24 |
-
|
| 25 |
-
<p align="center">
|
| 26 |
-
<i>Official PyTorch implementation of the paper</i><br>
|
| 27 |
-
<b>"CAFF: Context-Aware Feedback Filtering for Multi-Hop Biomedical Knowledge Graph Evidence Selection"</b><br>
|
| 28 |
-
<i>(under review, IEEE Transactions on Knowledge and Data Engineering, 2026).</i>
|
| 29 |
-
</p>
|
| 30 |
-
|
| 31 |
-
---
|
| 32 |
-
|
| 33 |
-
## Table of Contents
|
| 34 |
-
|
| 35 |
-
1. [TL;DR](#tldr)
|
| 36 |
-
2. [The Context Blindness Error (CBE)](#the-context-blindness-error-cbe)
|
| 37 |
-
3. [Key Contributions](#key-contributions)
|
| 38 |
-
4. [Method Overview](#method-overview)
|
| 39 |
-
- [Stage 1 BFS Candidate Stratification](#stage-1-bfs-candidate-stratification)
|
| 40 |
-
- [Stage 2 Contextual Summary Vector (CSV)](#stage-2-contextual-summary-vector-csv)
|
| 41 |
-
- [Stage 3 Dynamic Bilinear Modulation (DBM)](#stage-3-dynamic-bilinear-modulation-dbm)
|
| 42 |
-
- [Stage 4 Hop-Conditioned Context Contrast (HC3) Loss](#stage-4-hop-conditioned-context-contrast-hc3-loss)
|
| 43 |
-
5. [Theoretical Guarantees](#theoretical-guarantees)
|
| 44 |
-
6. [Repository Structure](#repository-structure)
|
| 45 |
-
7. [Installation](#installation)
|
| 46 |
-
8. [Data Preparation](#data-preparation)
|
| 47 |
-
9. [Training](#training)
|
| 48 |
-
10. [Evaluation](#evaluation)
|
| 49 |
-
11. [Main Results](#main-results)
|
| 50 |
-
12. [Ablation Study](#ablation-study)
|
| 51 |
-
13. [Hyperparameters](#hyperparameters)
|
| 52 |
-
14. [Reproducibility](#reproducibility)
|
| 53 |
-
15. [Hardware Requirements](#hardware-requirements)
|
| 54 |
-
16. [Limitations](#limitations)
|
| 55 |
-
17. [Citation](#citation)
|
| 56 |
-
18. [License](#license)
|
| 57 |
-
19. [Acknowledgements](#acknowledgements)
|
| 58 |
-
20. [Contact](#contact)
|
| 59 |
-
|
| 60 |
-
---
|
| 61 |
-
|
| 62 |
-
## TL;DR
|
| 63 |
-
|
| 64 |
-
> Existing triple filters for multi-hop KG-RAG score each candidate from `(Query, relation, BFS_depth)` alone , they are **blind** to which triples were retained at the previous hop. We prove this blindness incurs an **irreducible** Bayes error floor `ε* > 0` (Theorem 1, via the Data Processing Inequality). **CAFF** closes this gap with a three-piece, filtering-layer-only feedback loop:
|
| 65 |
-
>
|
| 66 |
-
> - **CSV** a parameter-free, permutation-invariant summary of the previously retained set.
|
| 67 |
-
> - **DBM** a low-rank, sigmoid-gated perturbation of the bilinear scoring matrix, *generated dynamically* from the CSV.
|
| 68 |
-
> - **HC3** a contrastive loss that provably maximizes a variational lower bound on the conditional mutual information `I(Y; S | z)`.
|
| 69 |
-
>
|
| 70 |
-
> CAFF lifts PubMedQA accuracy from **76.9 → 79.6** (+2.7 pts) and BioASQ 7b macro-F1 from **71.1 → 74.3** (+3.2 pts) over the strongest depth-stratified baseline, with gains concentrated at the deepest hops (**+6.9** pts at hop 2, **+9.3** pts at hop 3) exactly where CBE is most severe.
|
| 71 |
-
|
| 72 |
-
---
|
| 73 |
-
|
| 74 |
-
## The Context Blindness Error (CBE)
|
| 75 |
-
|
| 76 |
-
Consider the clinical query:
|
| 77 |
-
|
| 78 |
-
> *"What drug targets the pathway of the causal gene of Fanconi anemia complementation group D1?"*
|
| 79 |
-
|
| 80 |
-
The same hop-2 triple `⟨BRCA2, participates_in, HR-repair⟩` is:
|
| 81 |
-
- **Diagnostically essential** when hop 1 retained `⟨FANCD1, causal_mutation, BRCA2⟩`,
|
| 82 |
-
- **Irrelevant noise** when hop 1 retained only `⟨FANCD1, has_phenotype, bone-marrow-failure⟩`.
|
| 83 |
-
|
| 84 |
-
A depth-stratified filter sees only `(Q, participates_in, ℓ=2)` and assigns **the same score in both cases**. It cannot distinguish the two evidential trajectories it commits the **Context Blindness Error**.
|
| 85 |
-
|
| 86 |
-
Formally, for a context-agnostic filter `f ∈ F_agn` and any threshold `τ`:
|
| 87 |
-
|
| 88 |
-
```
|
| 89 |
-
P( 𝟙[f(X) ≥ τ] ≠ Y ) ≥ R* + ε*, where ε* > 0
|
| 90 |
-
```
|
| 91 |
-
|
| 92 |
-
with the closed-form lower bound
|
| 93 |
-
|
| 94 |
-
```
|
| 95 |
-
ε* ≥ ½ · 𝔼_X [ Var_{Z|X} ( P(Y=1 | X, Z) ) ].
|
| 96 |
-
```
|
| 97 |
-
|
| 98 |
-
This floor is **architectural**, not representational no parameter scaling of `f(Q, r, ℓ)` can recover information about `S_{ℓ-1}` that was never given to it as input.
|
| 99 |
-
|
| 100 |
-
---
|
| 101 |
-
|
| 102 |
-
## Key Contributions
|
| 103 |
-
|
| 104 |
-
| # | Contribution | Paper Section |
|
| 105 |
-
|---|---|---|
|
| 106 |
-
| **C1** | Formal definition of the Context Blindness Error (CBE) and a proof that it induces an irreducible Bayes error floor `ε* > 0`, with a closed-form variance lower bound. | §4 |
|
| 107 |
-
| **C2** | **Contextual Summary Vector (CSV)** a parameter-free, permutation-invariant encoder of the previously retained set, with a formal injectivity guarantee under linearly independent relation embeddings. | §5.2 |
|
| 108 |
-
| **C3** | **Dynamic Bilinear Modulation (DBM)** a low-rank, sigmoid-gated, *dynamically generated* perturbation of the scoring matrix, with **zero per-candidate overhead** after one-time precomputation. | §5.3 |
|
| 109 |
-
| **C4** | **HC3 loss** an InfoNCE-derived contrastive objective formally equivalent to maximizing a variational lower bound on the conditional mutual information `I(Y; S \| z)`. | §5.4 |
|
| 110 |
-
| **C5** | State-of-the-art results on PubMedQA and BioASQ 7b, with hop-stratified ablations and a diagnostic context-swap experiment showing **1.84 bits** of context separation vs. **0.00 bits** for every context-agnostic baseline. | §7–§8 |
|
| 111 |
-
|
| 112 |
-
---
|
| 113 |
-
|
| 114 |
-
## Method Overview
|
| 115 |
-
|
| 116 |
-
```
|
| 117 |
-
┌─────────┐ ┌────────────┐ ┌─────────────────────────────────────────┐ ┌─────────────┐
|
| 118 |
-
│ Query Q │──▶│ Entity │──▶│ BFS subgraph extraction (depth L=3) │──▶│ Candidate │
|
| 119 |
-
└─────────┘ │ Linker │ └─────────────────────────────────────────┘ │ sets {C_ℓ} │
|
| 120 |
-
└────────────┘ └──────┬──────┘
|
| 121 |
-
│
|
| 122 |
-
┌────────────────── CAFF filtering layer ──────────────┘
|
| 123 |
-
▼
|
| 124 |
-
┌───────────────┐ z_{ℓ-1} ┌────────────────┐
|
| 125 |
-
│ CSV │──────────────▶│ DBM gate │
|
| 126 |
-
│ (Eq. 14) │ │ Δ_ℓ (Eq. 17) │
|
| 127 |
-
└───────────────┘ └────────┬───────┘
|
| 128 |
-
▼
|
| 129 |
-
W^ctx_ℓ = W₀ + A_ℓB_ℓᵀ + Δ_ℓ
|
| 130 |
-
│
|
| 131 |
-
▼
|
| 132 |
-
s_ℓ = σ(qᵀ W^ctx_ℓ e_r + ...)
|
| 133 |
-
│
|
| 134 |
-
▼ (threshold θ)
|
| 135 |
-
S_ℓ ────┐
|
| 136 |
-
│ feedback
|
| 137 |
-
└──▶ z_ℓ = CSV(S_ℓ)
|
| 138 |
-
│
|
| 139 |
-
▼
|
| 140 |
-
(next hop ℓ+1)
|
| 141 |
-
```
|
| 142 |
-
|
| 143 |
-
### Stage 1 BFS Candidate Stratification
|
| 144 |
-
|
| 145 |
-
For each head entity at depth `ℓ-1`, retain at most `K_r = 20` triples per relation type, ranked by descending tail degree. This **frequency cap** prevents hub-entity relation embeddings from saturating the CSV.
|
| 146 |
-
|
| 147 |
-
### Stage 2 Contextual Summary Vector (CSV)
|
| 148 |
-
|
| 149 |
-
Parameter-free, permutation-invariant, formally faithful:
|
| 150 |
-
|
| 151 |
-
```
|
| 152 |
-
1
|
| 153 |
-
z_{ℓ-1} = ──── · Σ_{(h,r,t) ∈ S_{ℓ-1}} e_r (mean of frozen relation embeddings)
|
| 154 |
-
|S_{ℓ-1}|
|
| 155 |
-
```
|
| 156 |
-
|
| 157 |
-
with `z_{ℓ-1} = 0` when `S_{ℓ-1} = ∅` (so CAFF reduces *gracefully* to the depth-stratified baseline at hop 1).
|
| 158 |
-
|
| 159 |
-
**Lemma (CSV faithfulness).** If the relation-embedding matrix `E` has full row rank, the map `Z ↦ EᵀZ` is **injective** on the simplex distinct retained-context distributions produce distinct CSVs.
|
| 160 |
-
|
| 161 |
-
### Stage 3 Dynamic Bilinear Modulation (DBM)
|
| 162 |
-
|
| 163 |
-
A low-rank, sigmoid-gated, **runtime-generated** increment to the scoring matrix:
|
| 164 |
-
|
| 165 |
-
```
|
| 166 |
-
g_ℓ = σ( P_ℓ z_{ℓ-1} + b_ℓ ) ∈ (0,1)^ρ (context gate, ρ = 16)
|
| 167 |
-
|
| 168 |
-
Δ_ℓ = U_ℓ · diag(g_ℓ) · V_ℓᵀ (rank-ρ context perturbation)
|
| 169 |
-
|
| 170 |
-
W^ctx_ℓ = W₀ + A_ℓ B_ℓᵀ + Δ_ℓ(g_ℓ)
|
| 171 |
-
───── ───────── ─────────
|
| 172 |
-
shared depth-specific context-specific
|
| 173 |
-
base (PCE correction) (CBE correction)
|
| 174 |
-
|
| 175 |
-
s_ℓ = σ( qᵀ W^ctx_ℓ e_r + vᵀ(q ⊙ e_r) + β_ℓ )
|
| 176 |
-
```
|
| 177 |
-
|
| 178 |
-
**Cost.** `W^ctx_ℓ` is precomputed **once per hop** (not per candidate). Per-hop overhead with `d=768`, `ρ=16`, `N_ℓ ≤ 500`:
|
| 179 |
-
- gate: `O(ρd)`,
|
| 180 |
-
- DBM assembly: `O(d²ρ)`,
|
| 181 |
-
- candidate scoring: `O(N_ℓ d²)`,
|
| 182 |
-
- total CAFF surcharge ≈ **9.4 × 10⁶ FLOPs / hop**, **zero per candidate**.
|
| 183 |
-
|
| 184 |
-
> **DBM vs. LoRA / Adapters.** LoRA learns a *fixed* low-rank increment during fine-tuning. DBM **generates** its rank-ρ increment *dynamically at inference time* from the CSV context-specific modulation without a separate parameter set per context.
|
| 185 |
-
|
| 186 |
-
### Stage 4 Hop-Conditioned Context Contrast (HC3) Loss
|
| 187 |
-
|
| 188 |
-
For each anchor `(Q, r, ℓ)`, mine a positive context `z^(a)` (where the triple was labeled 1) and up to 8 negative contexts `z^(b)` (where it was labeled 0):
|
| 189 |
-
|
| 190 |
-
```
|
| 191 |
-
L_HC3 = (1 / |T_HC3|) · Σ max( 0, s(Q, r, ℓ, z^(b)) − s(Q, r, ℓ, z^(a)) + γ_C )
|
| 192 |
-
```
|
| 193 |
-
|
| 194 |
-
with margin `γ_C = 0.25`. **Proposition.** Minimizing `L_HC3` maximizes a variational lower bound on `I(Y; S | z)`.
|
| 195 |
-
|
| 196 |
-
The full objective:
|
| 197 |
-
|
| 198 |
-
```
|
| 199 |
-
L = L_BCE + λ_D · L_DC + λ_C · L_HC3 (λ_D = 0.40, λ_C = 0.35)
|
| 200 |
-
```
|
| 201 |
-
|
| 202 |
-
---
|
| 203 |
-
|
| 204 |
-
## Theoretical Guarantees
|
| 205 |
-
|
| 206 |
-
| # | Statement | Where |
|
| 207 |
-
|---|---|---|
|
| 208 |
-
| **Theorem 1** | Under positive CMI `I(Y; Z \| X) > 0`, every context-agnostic filter incurs an irreducible Bayes error floor `ε* > 0` with `ε* ≥ ½ · 𝔼_X[Var_{Z\|X}(π(X,Z))]`. | §4.2 |
|
| 209 |
-
| **Lemma 1** | CBE forces label indistinguishability: there exist `(X, z₁, y=1)` and `(X, z₂, y=0)` receiving identical agnostic scores. | §3.3 |
|
| 210 |
-
| **Lemma 2** | The CSV is injective on the simplex of retained-context distributions whenever the relation-embedding matrix has full row rank. | §5.2 |
|
| 211 |
-
| **Proposition 1** | CSV noise stability: `𝔼[‖z̃ − z‖²] = σ²d / \|S_{ℓ-1}\|` context summaries are **most stable in the regime where they carry the most information**. | §5.2 |
|
| 212 |
-
| **Proposition 2** | DBM rank sufficiency: with `ρ ≥ rank(Δ*)`, DBM can exactly represent any context-induced perturbation (Eckart–Young). | §6 |
|
| 213 |
-
| **Proposition 3** | Minimizing `L_HC3` maximizes a variational lower bound on `I(Y; S \| z)`. | §5.4 |
|
| 214 |
-
| **Proposition 4** | `ε*` is monotone non-decreasing in `I(Y; Z \| X)` (via Pinsker). | §4.2 |
|
| 215 |
-
| **Theorem 2** | Strict TPR benefit: there exist contexts at which the optimal context-aware filter strictly outperforms the optimal context-agnostic one. | §6 |
|
| 216 |
-
|
| 217 |
-
---
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
---
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
---
|
| 225 |
-
|
| 226 |
-
## Repository Structure
|
| 227 |
-
|
| 228 |
-
```
|
| 229 |
-
CAFF/
|
| 230 |
-
├── caff/ # Core package (importable)
|
| 231 |
-
│ ├── __init__.py # Public API surface
|
| 232 |
-
│ ├── config.py # CAFFConfig + AblationFlags dataclasses
|
| 233 |
-
│ ├── csv.py # Contextual Summary Vector (Stage 2)
|
| 234 |
-
│ ├── data.py # KG loader, BFS extractor, datasets
|
| 235 |
-
│ ├── dbm.py # Dynamic Bilinear Modulation (Stage 3)
|
| 236 |
-
│ ├── encoders.py # Frozen encoder + relation cache
|
| 237 |
-
│ ├── evaluator.py # Metrics, MAP / NDCG, threshold tuning
|
| 238 |
-
│ ├── losses.py # BCE + DC + HC3 loss objects
|
| 239 |
-
│ ├── miners.py # DCMiner + HC3Miner + buffers
|
| 240 |
-
│ ├── model.py # CAFFModel (CSV + DBM + scoring head)
|
| 241 |
-
│ ├── scorer.py # DepthBilinear + HopScorer
|
| 242 |
-
│ ├── trainer.py # CAFFTrainer + CheckpointManager
|
| 243 |
-
│ └── utils/ # seeding, logging
|
| 244 |
-
│
|
| 245 |
-
├── scripts/ # Reproduction pipeline
|
| 246 |
-
│ ├── convert_orphanet_xml_to_tsv.py # Orphanet XML -> TSV
|
| 247 |
-
│ ├── convert_hpo_to_tsv.py # HPO obo -> TSV
|
| 248 |
-
│ ├── convert_mondo_to_tsv.py # MONDO obo -> TSV (experimental)
|
| 249 |
-
│ ├── build_kg.py # Base KG from Orphanet TSV
|
| 250 |
-
│ ├── merge_hpo_into_kg.py # KG v2 = + HPO/OMIM annotations
|
| 251 |
-
│ ├── merge_mondo_into_kg.py # KG v3 = + MONDO (experimental)
|
| 252 |
-
│ ├── build_orphanet_qa.py # Sample QA records from KG
|
| 253 |
-
│ ├── annotate_triples.py # Paper-spec shortest-path gold
|
| 254 |
-
│ ├── extract_bfs.py # Precompute BFS candidates
|
| 255 |
-
│ ├── threshold_sweep.py # Find optimal global theta
|
| 256 |
-
│ └── per_hop_threshold_sweep.py # Per-hop theta tuning
|
| 257 |
-
│
|
| 258 |
-
├── configs/ # YAML training configs
|
| 259 |
-
│ ├── caff_full.yaml # Paper config (BioLinkBERT, GPU)
|
| 260 |
-
│ ├── caff_no_hc3.yaml # Ablation (no HC3 loss)
|
| 261 |
-
│ ├── depthbilinear.yaml # Baseline (no CSV / DBM)
|
| 262 |
-
│ ├── caff_orphanet.yaml # Repository default (CPU, KG v2)
|
| 263 |
-
│ └── caff_smoke.yaml # Smoke test (tiny synthetic KG)
|
| 264 |
-
│
|
| 265 |
-
├── tests/ # 48+ unit tests (run by CI)
|
| 266 |
-
│ ├── fixtures/ # Tiny synthetic KG
|
| 267 |
-
│ ├── test_csv.py # CSV faithfulness
|
| 268 |
-
│ ├── test_data.py # KG loader + dataset
|
| 269 |
-
│ ├── test_dbm.py # DBM rank sufficiency
|
| 270 |
-
│ ├── test_evaluator.py # Metrics + JSD diagnostic
|
| 271 |
-
│ ├── test_losses.py # BCE / DC / HC3 loss math
|
| 272 |
-
│ ├── test_miners.py # DC + HC3 mining (Phase 2)
|
| 273 |
-
│ ├── test_scorer.py # Bilinear scoring
|
| 274 |
-
│ └── test_smoke_pipeline.py # End-to-end smoke run
|
| 275 |
-
│
|
| 276 |
-
├── .github/workflows/
|
| 277 |
-
│ └── tests.yml # CI: lint + pytest on every push
|
| 278 |
-
│
|
| 279 |
-
├── data/ # gitignored (raw + processed)
|
| 280 |
-
├── runs/ # gitignored (checkpoints, logs)
|
| 281 |
-
├── cache/ # gitignored (BFS + relation cache)
|
| 282 |
-
├── examples/ # short usage snippets
|
| 283 |
-
│
|
| 284 |
-
├── train.py # Training entry point
|
| 285 |
-
├── evaluate.py # Standalone evaluation script
|
| 286 |
-
├── context_swap_diagnostic.py # Appendix C diagnostic
|
| 287 |
-
│
|
| 288 |
-
├── README.md # This file
|
| 289 |
-
├── CONTRIBUTING.md # Contribution guidelines
|
| 290 |
-
├── PAPER_DISCREPANCIES.md # 10-section running experiment log
|
| 291 |
-
├── LICENSE # MIT
|
| 292 |
-
├── requirements.txt # Core dependencies
|
| 293 |
-
├── requirements-optional.txt # Optional (wandb, scispacy, openai)
|
| 294 |
-
├── .gitattributes
|
| 295 |
-
└── .gitignore
|
| 296 |
-
```
|
| 297 |
-
|
| 298 |
-
### Notes on file purposes
|
| 299 |
-
|
| 300 |
-
- **`caff/`** is the importable package. Its public API is declared in
|
| 301 |
-
`__init__.py` and matches the names referenced from `train.py`,
|
| 302 |
-
`evaluate.py`, and the test suite.
|
| 303 |
-
- **`scripts/`** contains everything outside the training loop: data
|
| 304 |
-
conversion, KG construction, QA sampling, threshold tuning. Each
|
| 305 |
-
script is self-contained and can be run independently from the
|
| 306 |
-
command line.
|
| 307 |
-
- **`configs/`** holds five YAML files. The paper's `caff_full.yaml`
|
| 308 |
-
expects BioLinkBERT-Large and the four-source KG and is the right
|
| 309 |
-
starting point on GPU. `caff_orphanet.yaml` is the CPU-only
|
| 310 |
-
default used throughout this repository's Implementation Reality
|
| 311 |
-
Check section.
|
| 312 |
-
- **`tests/`** runs in CI on every push. All commits on `main` have
|
| 313 |
-
the suite green; see the badge near the top of this file.
|
| 314 |
-
- **`PAPER_DISCREPANCIES.md`** is the source of truth for every
|
| 315 |
-
empirical decision in the repo (bug fixes, threshold choices, KG
|
| 316 |
-
scaling, the MONDO experiment). New experiments should append a
|
| 317 |
-
new numbered section there before changing the headline numbers.
|
| 318 |
-
|
| 319 |
-
## Installation
|
| 320 |
-
|
| 321 |
-
### Prerequisites
|
| 322 |
-
|
| 323 |
-
- **Python** ≥ 3.10
|
| 324 |
-
- **CUDA** 11.8 (a single NVIDIA A100-80GB or equivalent is recommended)
|
| 325 |
-
- **Git LFS** (for downloading model checkpoints, when released)
|
| 326 |
-
|
| 327 |
-
### Setup
|
| 328 |
-
|
| 329 |
-
```bash
|
| 330 |
-
# 1. Clone the repository
|
| 331 |
-
git clone https://github.com/<your-org>/caff.git
|
| 332 |
-
cd caff
|
| 333 |
-
|
| 334 |
-
# 2. Create a clean virtual environment
|
| 335 |
-
python -m venv .venv
|
| 336 |
-
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
| 337 |
-
|
| 338 |
-
# 3. Install PyTorch (matched to your CUDA toolkit)
|
| 339 |
-
pip install torch>=2.0 --index-url https://download.pytorch.org/whl/cu118
|
| 340 |
-
|
| 341 |
-
# 4. Install remaining dependencies
|
| 342 |
-
pip install -r requirements.txt
|
| 343 |
-
|
| 344 |
-
# 5. (Optional) Download the BioLinkBERT-Large encoder
|
| 345 |
-
python -c "from transformers import AutoModel; AutoModel.from_pretrained('michiyasunaga/BioLinkBERT-large')"
|
| 346 |
-
```
|
| 347 |
-
|
| 348 |
-
### Core Dependencies
|
| 349 |
-
|
| 350 |
-
```
|
| 351 |
-
torch>=2.0
|
| 352 |
-
transformers>=4.30
|
| 353 |
-
scispacy>=0.5
|
| 354 |
-
networkx>=3.0
|
| 355 |
-
numpy, scipy, scikit-learn, pandas
|
| 356 |
-
tqdm, pyyaml, wandb (optional)
|
| 357 |
-
openai>=1.0 # only for end-to-end QA with GPT-3.5-turbo
|
| 358 |
-
```
|
| 359 |
-
|
| 360 |
-
---
|
| 361 |
-
|
| 362 |
-
## Data Preparation
|
| 363 |
-
> **Note.** This section documents the *paper configuration*. The repository ships a CPU-only reproduction path with Orphanet + HPO + OMIM annotations; see [Implementation Reality Check](#implementation-reality-check) for the as-shipped workflow and the corresponding numbers.
|
| 364 |
-
|
| 365 |
-
CAFF operates on a **merged biomedical KG** built from three primary sources, joined on shared **UMLS Concept Unique Identifiers (CUIs)**.
|
| 366 |
-
|
| 367 |
-
| Source | Version | Used for | Access |
|
| 368 |
-
|---|---|---|---|
|
| 369 |
-
| **Orphanet** | 2024 release | Rare-disease ontology, gene–disease links | <https://www.orphadata.com> |
|
| 370 |
-
| **DisGeNET** | v7.0 (2020) | Gene–disease associations | <https://www.disgenet.org> |
|
| 371 |
-
| **OMIM** | 2023 update | Mendelian inheritance, gene–phenotype | <https://www.omim.org> |
|
| 372 |
-
| **PubMedQA** | — | QA benchmark (1,000 questions) | <https://pubmedqa.github.io/> |
|
| 373 |
-
| **BioASQ 7b** | 2019 release | QA benchmark (1,141 yes/no + factoid) | <http://bioasq.org> |
|
| 374 |
-
|
| 375 |
-
> **Licensing note.** Orphanet, DisGeNET, OMIM, and BioASQ have their own access terms. We **do not redistribute** the raw data; users must obtain it directly from each source. The build script reproduces the merged KG deterministically.
|
| 376 |
-
|
| 377 |
-
### Build the merged KG
|
| 378 |
-
|
| 379 |
-
```bash
|
| 380 |
-
python scripts/build_kg.py \
|
| 381 |
-
--orphanet data/raw/orphanet/ \
|
| 382 |
-
--disgenet data/raw/disgenet/all_gene_disease_associations.tsv \
|
| 383 |
-
--omim data/raw/omim/ \
|
| 384 |
-
--umls data/raw/umls/MRCONSO.RRF \
|
| 385 |
-
--out data/processed/kg.parquet \
|
| 386 |
-
--min-relation-freq 50
|
| 387 |
-
```
|
| 388 |
-
|
| 389 |
-
After construction, the merged KG contains:
|
| 390 |
-
|
| 391 |
-
| Property | Value |
|
| 392 |
-
|---|---|
|
| 393 |
-
| Entities `\|V\|` | **148,423** |
|
| 394 |
-
| Triples `\|E\|` | **2,318,941** |
|
| 395 |
-
| Relation types `\|R\|` (after singleton removal) | **42** |
|
| 396 |
-
| Max BFS depth `L` | 3 |
|
| 397 |
-
|
| 398 |
-
### Annotate gold relevance
|
| 399 |
-
|
| 400 |
-
Triples on any shortest path from a seed entity to the gold answer entity receive `y = 1`; all others `y = 0`. This yields **≈ 3.7 M** labeled training instances across hop depths 1–3.
|
| 401 |
-
|
| 402 |
-
```bash
|
| 403 |
-
python scripts/annotate_triples.py \
|
| 404 |
-
--kg data/processed/kg.parquet \
|
| 405 |
-
--pubmedqa data/benchmarks/pubmedqa.json \
|
| 406 |
-
--linker scispacy + UMLS \
|
| 407 |
-
--out data/processed/gold_pubmedqa.jsonl
|
| 408 |
-
```
|
| 409 |
-
|
| 410 |
-
---
|
| 411 |
-
|
| 412 |
-
## Training
|
| 413 |
-
> **Note.** This section documents the *paper configuration*. The repository ships a CPU-only reproduction path with Orphanet + HPO + OMIM annotations; see [Implementation Reality Check](#implementation-reality-check) for the as-shipped workflow and the corresponding numbers.
|
| 414 |
-
|
| 415 |
-
### Quick start - full CAFF on PubMedQA
|
| 416 |
-
|
| 417 |
-
```bash
|
| 418 |
-
python train.py --config configs/caff_full.yaml
|
| 419 |
-
```
|
| 420 |
-
|
| 421 |
-
### Reproduce the full benchmark suite
|
| 422 |
-
|
| 423 |
-
```bash
|
| 424 |
-
# 1. Strongest baseline (depth-stratified bilinear, no context)
|
| 425 |
-
python train.py --config configs/depthbilinear.yaml -seed 42
|
| 426 |
-
|
| 427 |
-
# 2. CAFF without HC3 loss (CSV + DBM only — ablates the CMI bound)
|
| 428 |
-
python train.py --config configs/caff_no_hc3.yaml -seed 42
|
| 429 |
-
|
| 430 |
-
# 3. Full CAFF
|
| 431 |
-
python train.py --config configs/caff_full.yaml -seed 42
|
| 432 |
-
|
| 433 |
-
# 4. Repeat across the three reported seeds
|
| 434 |
-
for s in 42 1337 2024; do
|
| 435 |
-
python train.py --config configs/caff_full.yaml --seed $s
|
| 436 |
-
done
|
| 437 |
-
```
|
| 438 |
-
|
| 439 |
-
### Key training hyperparameters
|
| 440 |
-
|
| 441 |
-
| Hyperparameter | Default | Notes |
|
| 442 |
-
|---|---:|---|
|
| 443 |
-
| Optimizer | AdamW | weight decay `1e-2` |
|
| 444 |
-
| Base learning rate | `3e-4` | cosine decay to `1e-5`, 2-epoch linear warmup |
|
| 445 |
-
| Batch size | 256 | |
|
| 446 |
-
| Epochs | 30 | early stopping on dev, patience 5 |
|
| 447 |
-
| Gradient clip | `‖∇‖₂ ≤ 1.0` | |
|
| 448 |
-
| Random seeds | `{42, 1337, 2024}` | three runs reported |
|
| 449 |
-
|
| 450 |
-
---
|
| 451 |
-
|
| 452 |
-
## Evaluation
|
| 453 |
-
|
| 454 |
-
### Filtering-layer metrics
|
| 455 |
-
|
| 456 |
-
```bash
|
| 457 |
-
python evaluate.py \
|
| 458 |
-
--checkpoint runs/caff_full/seed_42/best.pt \
|
| 459 |
-
--benchmark pubmedqa \
|
| 460 |
-
--metrics precision recall f1 map ndcg@10 \
|
| 461 |
-
--hop-stratified
|
| 462 |
-
```
|
| 463 |
-
|
| 464 |
-
### End-to-end question answering (GPT-3.5-turbo backbone)
|
| 465 |
-
|
| 466 |
-
```bash
|
| 467 |
-
export OPENAI_API_KEY=<your-key>
|
| 468 |
-
|
| 469 |
-
python evaluate.py \
|
| 470 |
-
--checkpoint runs/caff_full/seed_42/best.pt \
|
| 471 |
-
--benchmark pubmedqa \
|
| 472 |
-
--llm-backbone gpt-3.5-turbo \
|
| 473 |
-
--temperature 0 --top-p 1 \
|
| 474 |
-
--metrics accuracy
|
| 475 |
-
```
|
| 476 |
-
|
| 477 |
-
Triples are serialized as one per line:
|
| 478 |
-
|
| 479 |
-
```
|
| 480 |
-
[head] -- [relation] --> [tail]
|
| 481 |
-
```
|
| 482 |
-
|
| 483 |
-
### Diagnostic context-swap experiment (Appendix C)
|
| 484 |
-
|
| 485 |
-
This experiment **directly measures CBE**: it presents the same `(Q, r, ℓ=2)` under two semantically opposing upstream contexts and reports the Jensen–Shannon divergence between the resulting score distributions.
|
| 486 |
-
|
| 487 |
-
```bash
|
| 488 |
-
python context_swap_diagnostic.py \
|
| 489 |
-
--checkpoint runs/caff_full/seed_42/best.pt \
|
| 490 |
-
--report-bits
|
| 491 |
-
```
|
| 492 |
-
|
| 493 |
-
> Every context-agnostic baseline yields **JSD = 0.00 bits** (the empirical signature of CBE). CAFF achieves **JSD = 1.84 bits** concrete proof that the architectural fix is doing what the theory predicts.
|
| 494 |
-
|
| 495 |
-
---
|
| 496 |
-
|
| 497 |
-
## Main Results
|
| 498 |
-
> **Note.** The numbers in this section are the *paper headline* reproduced from the manuscript. For the as-shipped CPU pipeline (Orphanet + HPO + OMIM, bert-base-uncased), see [Implementation Reality Check](#implementation-reality-check) directly below, where F1 = 0.522 +/- 0.001 is reported with full 3-seed validation.
|
| 499 |
-
|
| 500 |
-
### End-to-end QA (mean over 3 seeds)
|
| 501 |
-
|
| 502 |
-
| Method | PubMedQA Acc. | PubMedQA MAP | BioASQ 7b Macro-F1 | BioASQ MAP |
|
| 503 |
-
|---|---:|---:|---:|---:|
|
| 504 |
-
| BM25 | 68.2 | 0.571 | 61.3 | 0.519 |
|
| 505 |
-
| DPR-Bio | 72.4 | 0.618 | 65.7 | 0.562 |
|
| 506 |
-
| BioRAG | 74.1 | 0.641 | 68.9 | 0.591 |
|
| 507 |
-
| SubgraphRAG | 75.6 | 0.658 | 69.8 | 0.605 |
|
| 508 |
-
| DepthBilinear (B5, immediate predecessor) | 76.9 | 0.672 | 71.1 | 0.621 |
|
| 509 |
-
| CAFF - NoHC3 | _78.2_ | _0.689_ | _72.8_ | _0.638_ |
|
| 510 |
-
| **CAFF (Full)** | **79.6** | **0.703** | **74.3** | **0.652** |
|
| 511 |
-
| **Δ (Full vs. B5)** | **+2.7** | **+0.031** | **+3.2** | **+0.031** |
|
| 512 |
-
|
| 513 |
-
All gains over DepthBilinear are statistically significant at `p < 0.01` (paired bootstrap, 10,000 resamples).
|
| 514 |
-
|
| 515 |
-
### Hop-stratified triple precision (PubMedQA test)
|
| 516 |
-
|
| 517 |
-
| Method | Hop 1 | Hop 2 | Hop 3 | Avg |
|
| 518 |
-
|---|---:|---:|---:|---:|
|
| 519 |
-
| DepthBilinear | 61.7 | 54.3 | 48.8 | 54.9 |
|
| 520 |
-
| **CAFF (Full)** | **62.4** | **61.2** | **58.1** | **60.6** |
|
| 521 |
-
| **Δ_ℓ** | **+0.7** | **+6.9** | **+9.3** | **+5.7** |
|
| 522 |
-
|
| 523 |
-
> The gain at hop 1 is **near zero** by design , with no prior retained set, `z₀ = 0` and CAFF reduces exactly to DepthBilinear. Gains concentrate at hops 2 and 3, **directly validating Theorem 1**: context-agnostic filters accumulate disproportionate error at deeper hops because `I(Y_ℓ; Z_{ℓ-1} | Q, r, ℓ)` grows with depth.
|
| 524 |
-
|
| 525 |
-
### Path-survival rate (PSR)
|
| 526 |
-
|
| 527 |
-
A filter that maximizes edge-level F1 independently per hop can still drive multi-hop **path survival** to zero. CAFF's context conditioning correlates retention decisions *along the same path*:
|
| 528 |
-
|
| 529 |
-
| Method | F1 | PSR | End-to-end Acc. |
|
| 530 |
-
|---|---:|---:|---:|
|
| 531 |
-
| DepthBilinear | 66.6 | 63.3 | 76.9 |
|
| 532 |
-
| CAFF - NoHC3 | 68.8 | 71.2 | 78.2 |
|
| 533 |
-
| **CAFF (Full)** | **70.5** | **75.7** | **79.6** |
|
| 534 |
-
|
| 535 |
-
> The **+12.4-point PSR gap** between CAFF and DepthBilinear is the finite-sample manifestation of the `ε*` floor.
|
| 536 |
-
|
| 537 |
-
### Context-separation diagnostic (controlled context-swap)
|
| 538 |
-
|
| 539 |
-
| Method | s^(A) | s^(B) | **JSD (bits)** |
|
| 540 |
-
|---|---:|---:|---:|
|
| 541 |
-
| BM25 | 0.617 | 0.617 | 0.00 |
|
| 542 |
-
| DPR-Bio | 0.638 | 0.638 | 0.00 |
|
| 543 |
-
| BioRAG | 0.621 | 0.621 | 0.00 |
|
| 544 |
-
| SubgraphRAG | 0.634 | 0.634 | 0.00 |
|
| 545 |
-
| DepthBilinear | 0.620 | 0.620 | 0.00 |
|
| 546 |
-
| CAFF - NoHC3 | 0.741 | 0.301 | **1.41** |
|
| 547 |
-
| **CAFF (Full)** | **0.792** | **0.238** | **1.84** |
|
| 548 |
-
|
| 549 |
-
> Every context-agnostic baseline yields exactly **0.00 bits** of context separation — direct empirical confirmation of CBE.
|
| 550 |
-
|
| 551 |
-
---
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
---
|
| 555 |
-
|
| 556 |
-
## Implementation Reality Check
|
| 557 |
-
|
| 558 |
-
The numbers above (`Main Results`) are reproduced from the paper with
|
| 559 |
-
the configuration described in Sections 7-8 of the manuscript:
|
| 560 |
-
BioLinkBERT-Large encoder, 30 epochs on a single A100-80GB, the full
|
| 561 |
-
four-source merged KG (Orphanet + DisGeNET + OMIM + UMLS) with
|
| 562 |
-
`|V| = 148,423` and `|E| = 2,318,941`. They represent the published
|
| 563 |
-
upper bound that the method can reach when given the full compute and
|
| 564 |
-
data budget.
|
| 565 |
-
|
| 566 |
-
This repository ships an implementation that any reviewer can run
|
| 567 |
-
**today on consumer hardware** (CPU laptop or single 8 GB GPU) without
|
| 568 |
-
DisGeNET / UMLS credentials. To make the gap explicit, this section
|
| 569 |
-
reports the numbers we obtain in two settings, both validated with
|
| 570 |
-
three random seeds:
|
| 571 |
-
|
| 572 |
-
1. **CPU baseline** (`bert-base-uncased`, 20K QA, 3 seeds): F1 = 0.522 +/- 0.001
|
| 573 |
-
2. **GPU + BioLinkBERT-Large** (paper-spec encoder, same KG/QA): **F1 = 0.5315 +/- 0.0003**
|
| 574 |
-
|
| 575 |
-
The GPU + BioLinkBERT result lifts F1 by +0.016 absolute (+3.1%
|
| 576 |
-
relative) over the CPU baseline, with the tightest variance in the
|
| 577 |
-
project's history (sigma = 0.0003).
|
| 578 |
-
|
| 579 |
-
### Configuration as shipped
|
| 580 |
-
|
| 581 |
-
| Component | Paper setting | Repository default |
|
| 582 |
-
|-----------------------|---------------------------|-----------------------------------|
|
| 583 |
-
| Encoder | BioLinkBERT-Large (340 M) | `bert-base-uncased` (110 M, frozen) |
|
| 584 |
-
| KG sources | Orphanet + DisGeNET + OMIM + UMLS | Orphanet + HPO + OMIM annotations |
|
| 585 |
-
| KG size `\|V\|/\|E\|/\|R\|` | 148,423 / 2,318,941 / 42 | 38,456 / 291,335 / 11 |
|
| 586 |
-
| QA records | PubMedQA + BioASQ (~2.1K) | 20,000 sampled from KG (14K/3K/3K) |
|
| 587 |
-
| Hardware | 1 x A100-80GB | CPU (Intel/AMD, 16 GB RAM) |
|
| 588 |
-
| Epochs | 30 with patience 5 | 10 with patience 5 |
|
| 589 |
-
| Threshold `theta` | 0.50 | 0.80 (chosen from a dev sweep) |
|
| 590 |
-
| Seeds | {42, 1337, 2024} | {42, 1337, 2024} |
|
| 591 |
-
|
| 592 |
-
### Measured numbers on the held-out test set (3 seeds)
|
| 593 |
-
|
| 594 |
-
| metric | mean +/- std |
|
| 595 |
-
|------------------|---------------------|
|
| 596 |
-
| F1 | **0.522 +/- 0.001** |
|
| 597 |
-
| Precision | 0.479 +/- 0.007 |
|
| 598 |
-
| Recall | 0.574 +/- 0.014 |
|
| 599 |
-
| MAP | 0.638 +/- 0.000 |
|
| 600 |
-
| NDCG@10 | 0.681 +/- 0.000 |
|
| 601 |
-
| Hop-1 precision | 0.803 +/- 0.005 |
|
| 602 |
-
| Hop-2 precision | 0.417 +/- 0.012 |
|
| 603 |
-
| Hop-3 precision | 0.254 +/- 0.001 |
|
| 604 |
-
|
| 605 |
-
Test set: 3,000 queries, 102K candidate triples, never used in
|
| 606 |
-
training or threshold tuning. Variance across the three seeds is
|
| 607 |
-
extremely tight (std on F1 is 0.001, std on MAP and NDCG is 0.0002).
|
| 608 |
-
|
| 609 |
-
### How the gap to the paper headline is composed
|
| 610 |
-
|
| 611 |
-
The paper reports filtering F1 at the upper end of 0.70 and end-to-end
|
| 612 |
-
QA accuracy of 0.796. Our 0.522 is the same method run with materially
|
| 613 |
-
weaker pieces. A rough decomposition of where the missing ~0.27 should
|
| 614 |
-
come from, supported by the experiments documented in
|
| 615 |
-
`PAPER_DISCREPANCIES.md`:
|
| 616 |
-
|
| 617 |
-
| Change | Expected delta on F1 |
|
| 618 |
-
|------------------------------------------------|---------------------:|
|
| 619 |
-
| `bert-base-uncased` -> `BioLinkBERT-Large` (MEASURED) | +0.016 (3.1%) |
|
| 620 |
-
| Add DisGeNET + UMLS gene-disease layer | +0.05 to +0.10 |
|
| 621 |
-
| Larger trainable head (current is 0.77 M) | +0.02 to +0.05 |
|
| 622 |
-
| Per-relation thresholds | +0.02 to +0.05 |
|
| 623 |
-
| Longer training on GPU (30 epochs vs 10) | +0.02 to +0.05 |
|
| 624 |
-
|
| 625 |
-
Plausible reach with all of the above: F1 in the 0.65 - 0.75 band.
|
| 626 |
-
The first row (encoder upgrade) is now empirically measured at +0.016;
|
| 627 |
-
the remaining four rows are open. Closing the full distance to 0.79
|
| 628 |
-
is not yet supported by an end-to-end run on this codebase, but the
|
| 629 |
-
encoder upgrade alone moved F1 from 0.522 to 0.5315.
|
| 630 |
-
|
| 631 |
-
### Data-scaling experiment (5K vs 20K QA records)
|
| 632 |
-
|
| 633 |
-
The QA-set size matters but not as much as one might expect on this
|
| 634 |
-
KG/encoder combination. Both rows below are 3-seed validated on the
|
| 635 |
-
same held-out test set:
|
| 636 |
-
|
| 637 |
-
| QA records | Test F1 | Test recall | Test MAP |
|
| 638 |
-
|------------|-----------------|-----------------|-----------------|
|
| 639 |
-
| 5,000 | 0.509 +/- 0.005 | 0.500 +/- 0.002 | 0.625 +/- 0.007 |
|
| 640 |
-
| 20,000 | 0.522 +/- 0.001 | 0.574 +/- 0.014 | 0.638 +/- 0.000 |
|
| 641 |
-
|
| 642 |
-
Quadrupling the training data buys +2.6% absolute F1, almost entirely
|
| 643 |
-
through recall (+14.8%). Variance shrinks 5x. This is consistent with
|
| 644 |
-
the bottleneck being model capacity (frozen 110 M encoder + 0.77 M
|
| 645 |
-
trainable head), not data quantity.
|
| 646 |
-
|
| 647 |
-
### GPU + BioLinkBERT-Large upgrade (Phase 5)
|
| 648 |
-
|
| 649 |
-
The CPU baseline above uses `bert-base-uncased` because that fits in
|
| 650 |
-
memory without a GPU. Phase 5 moved training to a single 8 GB
|
| 651 |
-
consumer GPU (NVIDIA RTX 4060) and replaced the encoder with the
|
| 652 |
-
paper-cited `michiyasunaga/BioLinkBERT-large` (340 M params, frozen).
|
| 653 |
-
All other settings, the KG, the QA sample, the seeds, and the
|
| 654 |
-
threshold are unchanged.
|
| 655 |
-
|
| 656 |
-
| Configuration | Test F1 | Test recall | Hop-2 prec | Variance |
|
| 657 |
-
|------------------------------|-----------------|-----------------|-----------------|----------|
|
| 658 |
-
| CPU + bert-base-uncased | 0.522 +/- 0.001 | 0.574 +/- 0.014 | 0.417 +/- 0.012 | tight |
|
| 659 |
-
| GPU + bert-base-uncased | 0.515 +/- 0.003 | 0.561 +/- 0.012 | 0.424 +/- 0.005 | tight |
|
| 660 |
-
| GPU + BioLinkBERT-Large | **0.5315 +/- 0.0003** | **0.5791 +/- 0.0010** | **0.4430 +/- 0.0009** | tightest |
|
| 661 |
-
|
| 662 |
-
Two findings stand out:
|
| 663 |
-
|
| 664 |
-
1. **The biomedical encoder lifts test F1 by +0.016 absolute (+3.1%
|
| 665 |
-
relative).** The lift is concentrated in classification metrics
|
| 666 |
-
(precision, recall, per-hop precision); MAP and NDCG@10 are
|
| 667 |
-
essentially unchanged. This means BioLinkBERT shifts the score
|
| 668 |
-
distribution toward a more favorable operating point rather than
|
| 669 |
-
reordering the candidate ranking.
|
| 670 |
-
|
| 671 |
-
2. **The variance collapses to sigma = 0.0003 on F1.** This is the
|
| 672 |
-
tightest reproducibility in the project's history and suggests
|
| 673 |
-
that larger biomedical-pretrained encoders produce more stable
|
| 674 |
-
CAFF behaviour on this KG.
|
| 675 |
-
|
| 676 |
-
The dev-set lift was much smaller (+0.34%), so the GPU + BioLinkBERT
|
| 677 |
-
gain only becomes visible on the held-out test set. Section 11 of
|
| 678 |
-
`PAPER_DISCREPANCIES.md` documents the two-stage validation in full.
|
| 679 |
-
|
| 680 |
-
GPU hardware overrides handled automatically by `train.py`:
|
| 681 |
-
`micro_batch_size: 4, grad_accum_steps: 64, mixed_precision: fp16`
|
| 682 |
-
(effective batch size 256, matching the paper). Each seed takes
|
| 683 |
-
~40-50 minutes on the 8 GB RTX 4060 (vs ~80 minutes on CPU); total
|
| 684 |
-
3-seed time was ~130 minutes including BioLinkBERT first-load.
|
| 685 |
-
|
| 686 |
-
### Negative result kept on record: MONDO ontology integration
|
| 687 |
-
|
| 688 |
-
We tried adding the MONDO Disease Ontology (26K terms, 40K is_a edges,
|
| 689 |
-
17K equivalent_to xrefs to Orphanet/OMIM) to grow the KG to 66K
|
| 690 |
-
nodes / 348K edges. MONDO improved ranking quality (MAP +5%, NDCG +8%)
|
| 691 |
-
but reduced F1 by 7% at the existing threshold because it injected
|
| 692 |
-
many borderline-confident candidates the model had not learned to
|
| 693 |
-
suppress. We reverted to the Orphanet+HPO KG and kept the MONDO
|
| 694 |
-
scripts (`scripts/convert_mondo_to_tsv.py`,
|
| 695 |
-
`scripts/merge_mondo_into_kg.py`) for future work that addresses
|
| 696 |
-
candidate filtering or per-source thresholds.
|
| 697 |
-
|
| 698 |
-
### Reproducing the numbers in this section
|
| 699 |
-
|
| 700 |
-
The CPU baseline (F1 = 0.522) reproduces with `bert-base-uncased`.
|
| 701 |
-
For the GPU + BioLinkBERT-Large result (F1 = 0.5315), set
|
| 702 |
-
`encoder_name: michiyasunaga/BioLinkBERT-large` and `d: 1024` in
|
| 703 |
-
`configs/caff_orphanet.yaml` before step 4 below; the rest is
|
| 704 |
-
identical. `train.py` auto-detects CUDA and applies sensible
|
| 705 |
-
hardware overrides for an 8 GB GPU.
|
| 706 |
-
|
| 707 |
-
```bash
|
| 708 |
-
# 1. Convert raw ontologies to TSV (one-time, ~5 minutes)
|
| 709 |
-
python scripts/convert_orphanet_xml_to_tsv.py
|
| 710 |
-
python scripts/convert_hpo_to_tsv.py
|
| 711 |
-
|
| 712 |
-
# 2. Build the merged KG v2 (Orphanet + HPO + OMIM annotations)
|
| 713 |
-
python scripts/build_kg.py --orphanet data/raw/orphanet/ --out data/processed/merged_kg.tsv
|
| 714 |
-
python scripts/merge_hpo_into_kg.py # produces merged_kg_v2.tsv
|
| 715 |
-
|
| 716 |
-
# 3. Sample 20,000 QA records from the KG
|
| 717 |
-
python scripts/build_orphanet_qa.py \
|
| 718 |
-
--kg data/processed/merged_kg_v2.tsv \
|
| 719 |
-
--out-dir data/processed --n 20000
|
| 720 |
-
|
| 721 |
-
# 4. Train three seeds (each takes ~80 min on CPU)
|
| 722 |
-
for s in 42 1337 2024; do
|
| 723 |
-
python train.py --config configs/caff_orphanet.yaml --seed $s
|
| 724 |
-
done
|
| 725 |
-
|
| 726 |
-
# 5. Sweep thresholds on dev to find the optimum (chosen: 0.80)
|
| 727 |
-
python scripts/threshold_sweep.py
|
| 728 |
-
|
| 729 |
-
# 6. Evaluate each seed on the held-out test set
|
| 730 |
-
for s in 42 1337 2024; do
|
| 731 |
-
python scripts/threshold_sweep.py \
|
| 732 |
-
--checkpoint runs/caff_orphanet/seed_${s}/best.pt \
|
| 733 |
-
--thresholds 0.80
|
| 734 |
-
done
|
| 735 |
-
```
|
| 736 |
-
|
| 737 |
-
A reviewer running these commands deterministically reproduces the
|
| 738 |
-
numbers in the table above. See `PAPER_DISCREPANCIES.md` for the
|
| 739 |
-
ten-section running log of every experiment that informed the choices
|
| 740 |
-
in this README.
|
| 741 |
-
|
| 742 |
-
## Ablation Study
|
| 743 |
-
|
| 744 |
-
| Variant | Acc. | F1 | ΔAcc. |
|
| 745 |
-
|---|---:|---:|---:|
|
| 746 |
-
| **CAFF (Full)** | **79.6** | **70.5** | — |
|
| 747 |
-
| − CSV (`z_{ℓ-1} ≡ 0`) | 76.9 | 66.6 | −2.7 |
|
| 748 |
-
| − DBM (`Δ_ℓ ≡ 0`) | 77.8 | 67.9 | −1.8 |
|
| 749 |
-
| − `L_HC3` | 78.2 | 68.8 | −1.4 |
|
| 750 |
-
| − `L_DC` | 78.9 | 69.7 | −0.7 |
|
| 751 |
-
| − Frequency cap | 78.4 | 69.1 | −1.2 |
|
| 752 |
-
| Mean-CSV → max-pool | 79.1 | 69.9 | −0.5 |
|
| 753 |
-
| sigmoid gate → ReLU | 79.3 | 70.1 | −0.3 |
|
| 754 |
-
| `ρ = 8` (half rank) | 79.0 | 69.7 | −0.6 |
|
| 755 |
-
| `ρ = 32` (double rank) | 79.5 | 70.4 | −0.1 |
|
| 756 |
-
|
| 757 |
-
**Take-aways.**
|
| 758 |
-
1. Removing the CSV is the **largest single-component drop** , CSV is the primary CBE-elimination mechanism.
|
| 759 |
-
2. HC3 contributes an independent **+1.4 pts** by maximizing the CMI bound.
|
| 760 |
-
3. `ρ = 16` is near-optimal; `ρ = 32` yields only a marginal `−0.1` improvement.
|
| 761 |
-
|
| 762 |
-
---
|
| 763 |
-
|
| 764 |
-
## Hyperparameters
|
| 765 |
-
|
| 766 |
-
| Symbol | Meaning | Default |
|
| 767 |
-
|---|---|---:|
|
| 768 |
-
| `d` | Embedding dimension (BioLinkBERT-Large) | 768 |
|
| 769 |
-
| `L` | Maximum BFS hop depth | 3 |
|
| 770 |
-
| `ρ` | DBM rank | 16 |
|
| 771 |
-
| `θ` | Retention threshold | 0.50 |
|
| 772 |
-
| `K_r` | Frequency cap per relation per head | 20 |
|
| 773 |
-
| `γ_C` | HC3 margin | 0.25 |
|
| 774 |
-
| `γ_D` | Depth-contrastive margin | 0.20 |
|
| 775 |
-
| `λ_C` | HC3 loss weight | 0.35 |
|
| 776 |
-
| `λ_D` | DC loss weight | 0.40 |
|
| 777 |
-
|
| 778 |
-
CAFF maintains accuracy within **±1.5 pts** of its best configuration over the joint robustness box
|
| 779 |
-
|
| 780 |
-
```
|
| 781 |
-
γ_C ∈ [0.15, 0.35], γ_D ∈ [0.10, 0.30], lr ∈ [2e-4, 5e-4],
|
| 782 |
-
```
|
| 783 |
-
|
| 784 |
-
a wider basin than reported for SubgraphRAG or BioRAG.
|
| 785 |
-
|
| 786 |
-
---
|
| 787 |
-
|
| 788 |
-
## Reproducibility
|
| 789 |
-
|
| 790 |
-
- All reported results are **mean across three seeds** `{42, 1337, 2024}`.
|
| 791 |
-
- Standard deviations exceeding `0.3` points are noted in the paper text.
|
| 792 |
-
- Statistical significance is assessed via **paired bootstrap resampling** (`B = 10,000`).
|
| 793 |
-
- The frozen relation encoder (`BioLinkBERT-Large`, 340 M params) is **never updated**, confining all learnable capacity to **< 12 M parameters** (≈ 3.5% of the backbone).
|
| 794 |
-
- Code, preprocessing scripts, trained checkpoints, and the merged-KG construction pipeline released under the MIT License.
|
| 795 |
-
|
| 796 |
-
---
|
| 797 |
-
|
| 798 |
-
## Hardware Requirements
|
| 799 |
-
|
| 800 |
-
| Resource | Specification |
|
| 801 |
-
|---|---|
|
| 802 |
-
| GPU | 1 × NVIDIA A100-80GB SXM4 (recommended) |
|
| 803 |
-
| GPU memory | ≥ 40 GB for default batch size (256) |
|
| 804 |
-
| System RAM | ≥ 64 GB (KG fits in memory) |
|
| 805 |
-
| Disk | ≈ 25 GB (raw + processed data + checkpoints) |
|
| 806 |
-
| Framework | PyTorch 2.0+ · CUDA 11.8 |
|
| 807 |
-
|
| 808 |
-
| Stage | Wall-clock (single A100) |
|
| 809 |
-
|---|---:|
|
| 810 |
-
| KG construction (one-time) | ≈ 25 min |
|
| 811 |
-
| Triple annotation (one-time) | ≈ 40 min |
|
| 812 |
-
| Training (single seed) | ≈ 4.5 h |
|
| 813 |
-
| Training (3 seeds, full pipeline) | ≈ 13.5 h |
|
| 814 |
-
|
| 815 |
-
---
|
| 816 |
-
|
| 817 |
-
## Limitations
|
| 818 |
-
|
| 819 |
-
1. **Relation-type aggregation only.** The CSV summarizes relation types; entity-type information in `S_{ℓ-1}` is currently discarded.
|
| 820 |
-
2. **Fixed hop depth.** Evaluation uses `L = 3`; deeper paths are mechanically supported but require longer HC3 chains and larger triplet-mining buffers.
|
| 821 |
-
3. **LLM backbone.** End-to-end QA results use GPT-3.5-turbo. Sensitivity to other backbones (LLaMA-3, Mistral-7B) is left to future work.
|
| 822 |
-
4. **KG completeness.** CAFF cannot recover missing triples or correct factually incorrect edges in the source KG.
|
| 823 |
-
5. **Annotation method.** Gold labels rely on shortest-path reachability and may miss clinically relevant longer paths.
|
| 824 |
-
6. **Empty retained sets.** When `S_{ℓ-1} = ∅`, CAFF degrades gracefully to DepthBilinear; a soft-retention CSV variant is a natural extension.
|
| 825 |
-
|
| 826 |
-
---
|
| 827 |
-
|
| 828 |
-
## Citation
|
| 829 |
-
|
| 830 |
-
If you use CAFF, the merged KG construction, or the context-swap diagnostic in your work, please cite:
|
| 831 |
-
|
| 832 |
-
```bibtex
|
| 833 |
-
@article{dhifallah2026caff,
|
| 834 |
-
title = {{CAFF}: Context-Aware Feedback Filtering for Multi-Hop
|
| 835 |
-
Biomedical Knowledge Graph Evidence Selection},
|
| 836 |
-
author = {Dhifallah, Marwan and Liu, Yu},
|
| 837 |
-
journal = {IEEE Transactions on Knowledge and Data Engineering},
|
| 838 |
-
year = {2026},
|
| 839 |
-
note = {Under review}
|
| 840 |
-
}
|
| 841 |
-
```
|
| 842 |
-
|
| 843 |
-
---
|
| 844 |
-
|
| 845 |
-
## License
|
| 846 |
-
|
| 847 |
-
This project is released under the **MIT License** see [`LICENSE`](LICENSE) for the full text.
|
| 848 |
-
|
| 849 |
-
> The merged KG **derived from** Orphanet, DisGeNET, and OMIM is **not redistributed**; users must obtain the source data directly under each provider's terms.
|
| 850 |
-
|
| 851 |
-
---
|
| 852 |
-
|
| 853 |
-
## Acknowledgements
|
| 854 |
-
|
| 855 |
-
This research was conducted at the **School of Software Engineering, Dalian University of Technology (DUT)**, with support from the **CSC Type-B Scholarship**. We thank the maintainers of **Orphanet**, **DisGeNET**, **OMIM**, **PubMedQA**, **BioASQ**, **UMLS**, **SciSpacy**, and **BioLinkBERT** for making their resources publicly available.
|
| 856 |
-
|
| 857 |
-
---
|
| 858 |
-
|
| 859 |
-
## Contact
|
| 860 |
-
|
| 861 |
-
| Role | Name | Email |
|
| 862 |
-
|---|---|---|
|
| 863 |
-
| Corresponding author | **Marwan Dhifallah** (M.Sc. student, DUT) | <marwan@mail.dlut.edu.cn> |
|
| 864 |
-
| Supervisor | **Prof. Yu Liu** (Associate Professor, DUT) | <yuliu@dlut.edu.cn> |
|
| 865 |
-
|
| 866 |
-
For bugs and feature requests, please open an [issue](../../issues). For research collaborations, please contact the corresponding author directly.
|
| 867 |
-
|
| 868 |
-
---
|
| 869 |
-
|
| 870 |
-
<p align="center">
|
| 871 |
-
<i>If CAFF helps your research, a ⭐ on this repository is appreciated.</i>
|
| 872 |
-
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|