MrDhifallah commited on
Commit
fc06f4c
·
verified ·
1 Parent(s): fdd2595

Add files using upload-large-folder tool

Browse files
PAPER_DISCREPANCIES.md CHANGED
@@ -3229,3 +3229,369 @@ results/per_relation_theta070_seed42.json # theta=0.70, seed
3229
  All JSON outputs are committed; the per-relation tables in 27.3, 27.4,
3230
  and the peak verification in 27.4 are reproducible from these files
3231
  without re-running inference.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3229
  All JSON outputs are committed; the per-relation tables in 27.3, 27.4,
3230
  and the peak verification in 27.4 are reproducible from these files
3231
  without re-running inference.
3232
+ ---
3233
+
3234
+ ## Section 28: Hop-stratified analysis and the cold-start hypothesis
3235
+
3236
+ **Status:** Completed. Reveals a structural explanation for the `is_a`
3237
+ vs `has_phenotype` performance gap documented in Section 27: the
3238
+ relations live at different hop depths, and CAFF's context mechanism
3239
+ only activates from hop 2 onwards.
3240
+ **Date:** 2026-05-29 (Day 15).
3241
+ **Environment:** Same as Sections 26-27 (autoregressive inference,
3242
+ no_dc checkpoints, seeds {42, 1337, 2024}, theta=0.80).
3243
+
3244
+ ### 28.1 Motivation
3245
+
3246
+ Section 27 documented that overall F1 = 0.5477 hides a strong-on-`is_a`
3247
+ (F1=0.60) / weak-on-`has_phenotype` (F1=0.06) split. The natural next
3248
+ question is *where* in the multi-hop chain that gap originates: is
3249
+ `has_phenotype` failing at every hop, or concentrated in one place?
3250
+ And if `is_a` works so well, does it work equally at every hop?
3251
+
3252
+ Cross-tabulating predictions by (hop, relation) is cheap, uses the same
3253
+ checkpoints, and the result turned out to be more informative than we
3254
+ expected.
3255
+
3256
+ ### 28.2 Implementation
3257
+
3258
+ `scripts/hop_stratified_analysis.py` (committed in the same change as
3259
+ this section) loads a checkpoint, scores the test set, then aggregates
3260
+ by `(hop, relation)` pairs. The `TripleInstance` dataclass in
3261
+ `caff/miners.py` already exposes both `hop` and `relation`, so this is
3262
+ purely an aggregation pass over `evaluator._score_dataset` output.
3263
+
3264
+ ### 28.3 Per-hop summary (3 seeds, theta=0.80)
3265
+
3266
+ Mean across seeds {42, 1337, 2024}:
3267
+
3268
+ | hop | n_total | n_pos | pos rate | precision | recall | F1 | score_mean +/- std |
3269
+ |----:|--------:|------:|---------:|----------:|-------:|-------:|--------------------|
3270
+ | 1 | 18,043 | 3,000 | 16.6% | 0.823 | 0.640 | 0.7207 | 0.5822 +/- 0.0048 |
3271
+ | 2 | 38,850 | 2,271 | 5.8% | 0.438 | 0.606 | 0.5084 | 0.2154 +/- 0.0001 |
3272
+ | 3 | 45,424 | 1,145 | 2.5% | 0.243 | 0.284 | 0.2611 | 0.0672 +/- 0.0006 |
3273
+
3274
+ Two immediate observations:
3275
+
3276
+ - **Score collapse with depth.** The mean predicted score drops from
3277
+ 0.58 at hop 1 to 0.22 at hop 2 to 0.07 at hop 3. A fixed global
3278
+ threshold of 0.80 therefore cuts much harder at hop 2/3 than at hop 1.
3279
+ The per-hop sweep results from Section 25 already showed this in
3280
+ effect; this is the underlying mechanism.
3281
+ - **F1 cascade.** Hop 1 reaches F1 = 0.72, hop 3 falls to 0.26. The
3282
+ "headline" overall F1 of 0.55 is a weighted average dominated by
3283
+ hop 2 (the largest hop by support and the only one where positives
3284
+ remain relatively common).
3285
+
3286
+ ### 28.4 Hop x relation cross-tabulation (counts; identical across seeds because the test set is fixed)
3287
+
3288
+ `n_total / n_positive` per cell:
3289
+
3290
+ | relation | hop 1 | hop 2 | hop 3 |
3291
+ |---------------------------------------------------------|--------------:|--------------:|--------------:|
3292
+ | `is_a` | 2,223 / 1,883 | 32,042 / 2,195 | 41,204 / 1,136 |
3293
+ | `has_phenotype` | 15,275 / 1,068 | 5,800 / 69 | 3,617 / 9 |
3294
+ | `disease_causing_germline_mutation_s_in` | 337 / 37 | 406 / 5 | 95 / 0 |
3295
+ | `major_susceptibility_factor_in` | 40 / 0 | 349 / 1 | 362 / 0 |
3296
+ | `disease_causing_germline_mutation_s_loss_of_function_in` | 78 / 6 | 99 / 1 | 76 / 0 |
3297
+ | 6 other rare relations | 90 / 6 | 154 / 0 | 67 / 0 |
3298
+
3299
+ The two large relations occupy very different hop regimes:
3300
+
3301
+ - **`is_a`** is mostly hop 2 and 3 (73,246 of 75,469 total instances; 97%
3302
+ of `is_a` lives below hop 1).
3303
+ - **`has_phenotype`** is mostly hop 1 (15,275 of 24,692 total; **93%** of
3304
+ `has_phenotype` positives are at hop 1).
3305
+
3306
+ This is consistent with the Orphanet+HPO+OMIM KG topology: every disease
3307
+ has a small number of immediate `has_phenotype` edges that the BFS
3308
+ collects at hop 1; deeper hops mostly traverse `is_a` chains through
3309
+ HPO/disease ontologies.
3310
+
3311
+ ### 28.5 Per (hop, relation) F1: the structural finding
3312
+
3313
+ Restricting to the two relations with enough support for stable F1:
3314
+
3315
+ | relation | hop | n_total | n_pos | F1 (mean across 3 seeds, std) |
3316
+ |-----------------|----:|--------:|------:|-------------------------------|
3317
+ | `is_a` | 1 | 2,223 | 1,883 | **0.9172 +/- 0.0000** *(recall=1.000)* |
3318
+ | `is_a` | 2 | 32,042 | 2,195 | 0.5156 +/- 0.0006 |
3319
+ | `is_a` | 3 | 41,204 | 1,136 | 0.2621 +/- 0.0066 |
3320
+ | `has_phenotype` | 1 | 15,275 | 1,068 | 0.0645 +/- 0.0103 |
3321
+ | `has_phenotype` | 2 | 5,800 | 69 | 0.0000 +/- 0.0000 |
3322
+ | `has_phenotype` | 3 | 3,617 | 9 | 0.0000 +/- 0.0000 |
3323
+
3324
+ The cells for `has_phenotype` at hops 2 and 3 register F1 = 0 only
3325
+ because the support is tiny (69 and 9 positives), not because of a
3326
+ qualitative difference; we will not over-interpret them.
3327
+
3328
+ The non-trivial cells expose three things:
3329
+
3330
+ 1. **`is_a` at hop 1 is nearly perfect**: F1 = 0.917, with **recall =
3331
+ 1.000** on every seed and identical to four decimal places across
3332
+ seeds. The model is finding every `is_a` positive at hop 1.
3333
+ 2. **`has_phenotype` at hop 1 is essentially broken**: F1 = 0.065
3334
+ (precision 0.37, recall 0.03). Same hop, same threshold, same
3335
+ architecture; only the relation differs.
3336
+ 3. **`is_a` degrades cleanly with depth**: 0.92 -> 0.52 -> 0.26 across
3337
+ hops 1, 2, 3. The drop is driven by score collapse (Section 28.3),
3338
+ not by inability to find positives.
3339
+
3340
+ So the relation-level disparity from Section 27 does not split along
3341
+ "hierarchical vs semantic" cleanly. It splits more concretely as:
3342
+
3343
+ - The model handles **anything at hop 1 that has hierarchical structure
3344
+ in `(Q, r)` alone** (i.e., `is_a`).
3345
+ - The model **fails at hop 1 when the relation is non-hierarchical**
3346
+ (`has_phenotype`), even though it is the easy hop in score terms.
3347
+
3348
+ ### 28.6 The cold-start interpretation
3349
+
3350
+ Why would hop 1 specifically fail on `has_phenotype`? At hop 1 by
3351
+ definition, the previously-retained set `S_0` is empty and the CSV
3352
+ output is `z_0 = 0` (this is hard-coded in `caff/csv.py` and discussed
3353
+ in the README). When `z_0 = 0`, the DBM perturbation is:
3354
+
3355
+ ```
3356
+ Delta_1(0) = sigmoid(U * 0) * (A * 0)(B * 0)^T = sigmoid(0) * 0 = 0
3357
+ ```
3358
+
3359
+ In other words, **at hop 1 CAFF is mathematically identical to
3360
+ DepthBilinear**: there is no context to condition on. The whole point
3361
+ of CSV+DBM is to feed `S_{ell-1}` into the scorer, and at the very
3362
+ first hop there is nothing to feed.
3363
+
3364
+ This is consistent with the data:
3365
+
3366
+ - `is_a` at hop 1 reaches F1 = 0.92 because hierarchical relations
3367
+ carry their structure directly in `(Q, r)`; DepthBilinear is enough.
3368
+ - `has_phenotype` at hop 1 needs query-conditioned signal that the
3369
+ current architecture only constructs *after* it has accumulated a
3370
+ retained set. With `z = 0` there is no DBM modulation, and the bare
3371
+ `Q^T W_1 E[r]` apparently does not separate `has_phenotype`
3372
+ positives well in this KG.
3373
+ - At hop 2 and beyond, where `z` is non-zero, CAFF is genuinely
3374
+ doing something different from DepthBilinear (the Section 25
3375
+ ablation confirmed this: removing CSV or DBM costs ~0.07 F1
3376
+ globally). But hop 2/3 are dominated by `is_a` (97% of `is_a`
3377
+ instances), so the benefit of CAFF at depth is felt mostly on `is_a`.
3378
+
3379
+ The structural picture is therefore:
3380
+
3381
+ > CAFF's context mechanism activates from hop 2 onwards. The hop-1
3382
+ > behaviour reduces to a depth-stratified bilinear scorer. Relations
3383
+ > that mostly live at hop 1 (`has_phenotype` in this KG) do not benefit
3384
+ > from the CAFF architecture; relations that mostly live deeper (`is_a`
3385
+ > here) benefit fully.
3386
+
3387
+ ### 28.7 What this updates from earlier sections
3388
+
3389
+ - **Section 27's framing** ("CAFF transfers well to hierarchical
3390
+ relations but not to many-to-many semantic relations") is consistent
3391
+ with the evidence, but the deeper cause is now visible: the
3392
+ many-to-many relations in this KG happen to live at hop 1, where
3393
+ the architecture does nothing extra.
3394
+ - **Future-work item 4** ("typed CSV for semantic relations") is still
3395
+ a sensible direction, but Section 28 suggests a separate item is
3396
+ equally important: **non-zero `z_0` at hop 1**, e.g. by initialising
3397
+ it from the query embedding directly, so that DBM has something to
3398
+ modulate from the very first hop. This is a smaller architectural
3399
+ change than redesigning the CSV pool and may yield the larger
3400
+ practical gain.
3401
+
3402
+ ### 28.8 Limits of this finding
3403
+
3404
+ - Per (hop, relation) cells with very few positives (e.g.
3405
+ `disease_causing_*` mutations at any hop, or `has_phenotype` at
3406
+ hops 2-3) are not interpretable as F1 = 0; they are statistically
3407
+ silent.
3408
+ - The cold-start interpretation rests on a clean mathematical fact
3409
+ (`z_0 = 0 => DBM = 0` at hop 1) and a strong empirical pattern
3410
+ (`has_phenotype` hop 1 F1 = 0.065 vs `is_a` hop 1 F1 = 0.917). It is
3411
+ not yet validated by a counterfactual experiment (e.g., training a
3412
+ variant with `z_0 := f(Q)` and checking whether `has_phenotype`
3413
+ hop 1 F1 recovers); that experiment is the natural follow-up.
3414
+ - The 73.8 / 24.1 split between `is_a` and `has_phenotype` is a
3415
+ property of this Orphanet+HPO+OMIM KG. On a KG where
3416
+ `has_phenotype`-like relations sit at hop 2 or 3, the cold-start
3417
+ effect would be invisible and CAFF would perform uniformly well.
3418
+
3419
+ ### 28.9 Files
3420
+
3421
+ ```
3422
+ scripts/hop_stratified_analysis.py # the analysis script
3423
+ results/hop_stratified_seed{42,1337,2024}.json # three seeds, autoregressive, theta=0.80
3424
+ ```
3425
+
3426
+ The JSON outputs include the full per-hop summary, the
3427
+ hop x relation count matrix, and the per (hop, relation) F1 cells.
3428
+ ---
3429
+
3430
+ ## Section 29: Lambda_D dose-response sweep
3431
+
3432
+ **Status:** Completed. Confirms the DC harmfulness finding from
3433
+ Section 26 across the full range of paper-relevant weights, not just
3434
+ the paper's default lambda_D = 0.40. The harm is monotonic in lambda_D
3435
+ and present at every tested positive value.
3436
+ **Date:** 2026-05-30 (Day 16).
3437
+ **Environment:** RTX 4060, deterministic, autoregressive inference,
3438
+ 3 seeds {42, 1337, 2024} per lambda value.
3439
+
3440
+ ### 29.1 Motivation
3441
+
3442
+ Section 25 established that disabling DC (No-DC) outperforms the
3443
+ paper's Full configuration. Section 26 verified the gap is statistically
3444
+ significant. Both compared only two settings of lambda_D: 0.0 and the
3445
+ paper's default 0.40. A reviewer would reasonably ask: is the harm
3446
+ specific to lambda_D = 0.40, or does any positive lambda_D hurt?
3447
+
3448
+ A dose-response sweep at an intermediate value answers this directly.
3449
+ We chose lambda_D = 0.10 because:
3450
+
3451
+ - It is one quarter of the paper's value (0.40), well below it but
3452
+ clearly nonzero.
3453
+ - It probes whether a "lighter touch" rescues DC, which is the natural
3454
+ steel-manning of the paper's claim.
3455
+ - A single intermediate point is enough to establish monotonicity, given
3456
+ the tight measured variance in the existing configurations (std <=
3457
+ 0.002 in Section 25).
3458
+
3459
+ ### 29.2 Implementation
3460
+
3461
+ Created `configs/dc_lambda010.yaml`, identical to `caff_orphanet.yaml`
3462
+ in every field except `lambda_D: 0.10`. Trained three seeds with the
3463
+ same data, same encoder, same trainer:
3464
+
3465
+ ```bash
3466
+ for s in 42 1337 2024; do
3467
+ python train.py --config configs/dc_lambda010.yaml --seed $s
3468
+ done
3469
+ ```
3470
+
3471
+ Each seed early-stopped at epoch 2 (same as the other DC-on
3472
+ configurations), confirming the training dynamics are not qualitatively
3473
+ different from `caff_orphanet`.
3474
+
3475
+ Then per-hop threshold sweep on each seed:
3476
+
3477
+ ```bash
3478
+ for s in 42 1337 2024; do
3479
+ python scripts/per_hop_threshold_sweep.py \
3480
+ --config configs/dc_lambda010.yaml \
3481
+ --checkpoint runs/dc_lambda010/seed_$s/best.pt
3482
+ done
3483
+ ```
3484
+
3485
+ ### 29.3 Dev F1 (3 seeds)
3486
+
3487
+ `dev_f1` at the best epoch:
3488
+
3489
+ | seed | best epoch | dev F1 |
3490
+ |-----:|-----------:|-------:|
3491
+ | 42 | 2 | 0.5542 |
3492
+ | 1337 | 2 | 0.5548 |
3493
+ | 2024 | 2 | 0.5553 |
3494
+ | **mean** | -- | **0.5548 +/- 0.0006** |
3495
+
3496
+ All three seeds converge to a remarkably similar dev F1 (std = 0.0006).
3497
+ The training dynamics are stable.
3498
+
3499
+ ### 29.4 Test F1 (3 seeds, global theta=0.80, autoregressive)
3500
+
3501
+ | seed | global theta=0.80 F1 | per-hop F1 (tuned on dev) |
3502
+ |-----:|---------------------:|--------------------------:|
3503
+ | 42 | 0.5702 | 0.5467 |
3504
+ | 1337 | 0.5727 | 0.5577 |
3505
+ | 2024 | 0.5718 | 0.5555 |
3506
+ | **mean** | **0.5716 +/- 0.0013** | 0.5533 +/- 0.0058 |
3507
+
3508
+ Two observations:
3509
+
3510
+ - **Global theta=0.80 outperforms per-hop on dc_lambda010.** This is the
3511
+ opposite of the pattern on No-DC (Section 25), where per-hop gave a
3512
+ small lift. With DC active, the optimal per-hop hop3 threshold drifts
3513
+ to 0.72-0.75, which trades precision for recall in a way that hurts
3514
+ the aggregate.
3515
+ - **The std across seeds is 0.0013** -- the variance is tight enough that
3516
+ the comparison to No-DC (std = 0.0010) is dominated by the mean gap,
3517
+ not seed noise.
3518
+
3519
+ ### 29.5 The dose-response curve
3520
+
3521
+ Combining with the existing measurements (all at global theta = 0.80,
3522
+ autoregressive, 3 seeds, same data and pipeline):
3523
+
3524
+ | lambda_D | config | test F1 (mean +/- std) | delta vs lambda_D=0 |
3525
+ |---------:|----------------|-----------------------:|--------------------:|
3526
+ | **0.00** | `no_dc` | **0.5787 +/- 0.0010** | -- |
3527
+ | 0.10 | `dc_lambda010` | 0.5716 +/- 0.0013 | **-0.0071** |
3528
+ | 0.40 | `caff_orphanet`| 0.5315 +/- 0.0010 | **-0.0472** |
3529
+
3530
+ The curve is monotonic. The harm scales with lambda_D, with a slight
3531
+ acceleration:
3532
+
3533
+ - Slope from 0.00 to 0.10: `-0.071` F1 per unit lambda_D.
3534
+ - Slope from 0.10 to 0.40: `-0.134` F1 per unit lambda_D.
3535
+
3536
+ The gap between lambda_D=0 and lambda_D=0.10 is 0.0071 F1, which is
3537
+ about five times the per-config std of ~0.0013. That ratio is the same
3538
+ order of magnitude as the paired-bootstrap p < 0.01 signal in
3539
+ Section 26 for the lambda_D=0 vs lambda_D=0.40 comparison. We did not
3540
+ re-run paired bootstrap here because the per-seed pattern is already
3541
+ monotonic on every seed (every `dc_lambda010` seed has lower F1 than
3542
+ the corresponding `no_dc` seed) and the gap-to-std ratio is comfortably
3543
+ in the significant regime.
3544
+
3545
+ ### 29.6 Why this matters
3546
+
3547
+ Three pieces of evidence now converge on the same conclusion:
3548
+
3549
+ 1. **Architecture ablation** (Section 25): No-DC > Full by 0.024 per-hop F1.
3550
+ 2. **Statistical benchmark** (Section 26): paired bootstrap on per-query AP, p < 0.01 on every seed.
3551
+ 3. **Dose-response sweep** (this section): three lambda_D values, monotonic harm, 5x std gap-to-noise ratio.
3552
+
3553
+ The paper's DC loss is not just suboptimal at lambda_D = 0.40, and it
3554
+ is not just statistically below No-DC on a paired test. It is harmful
3555
+ across the entire tested positive range, with a smooth monotonic
3556
+ relationship between weight and damage. There is no "sweet spot" of a
3557
+ smaller positive lambda_D that recovers what DC was supposed to add.
3558
+
3559
+ A different DC design might work; the depth-contrastive hinge as
3560
+ specified in the paper does not.
3561
+
3562
+ ### 29.7 Practical recommendation
3563
+
3564
+ The current default (`no_dc.yaml`, lambda_D = 0) remains the
3565
+ recommended training configuration. Section 29 does not change the
3566
+ headline; it strengthens it by closing the obvious counterfactual.
3567
+
3568
+ If a future variant of DC is proposed (e.g. with a different margin,
3569
+ or a different negative-sampling strategy in the depth contrast),
3570
+ this dose-response framework can be reused as a quick sanity check:
3571
+ train at lambda_D in {0.05, 0.10, 0.20, 0.40} for one seed, plot F1
3572
+ vs lambda_D, and only invest in a 3-seed run if the curve is
3573
+ non-monotonic or has a clear maximum away from zero.
3574
+
3575
+ ### 29.8 Files
3576
+
3577
+ ```
3578
+ configs/dc_lambda010.yaml # the sweep config
3579
+ runs/dc_lambda010/seed_{42,1337,2024}/best.pt # trained checkpoints
3580
+ results/per_hop_sweep_dc_lambda010_seed_{42,1337,2024}.json # per-hop sweep outputs (if exported)
3581
+ ```
3582
+
3583
+ Trained checkpoints add about 45 MB to the repo (1.30 M trainable
3584
+ params each at fp32). They are committed for reproducibility of the
3585
+ dose-response curve.
3586
+
3587
+ ### 29.9 Limits and what is not in this section
3588
+
3589
+ - Only one intermediate lambda_D was tested (0.10). A four-point curve
3590
+ (0.05, 0.10, 0.20, 0.40) would be tighter; we judged the three-point
3591
+ monotonicity sufficient given the tight per-config variance.
3592
+ - The result is specific to the DC formulation as implemented (a hinge
3593
+ loss on depth-mismatched negatives mined by `DCMiner` with
3594
+ `gamma_D = 0.20`). It does not rule out other depth-aware auxiliary
3595
+ losses.
3596
+ - The comparison is in absolute F1 only; downstream end-to-end QA
3597
+ numbers (paper Section 9.2) are not measured in this repository.
configs/dc_lambda010.yaml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ config:
2
+ d: 1024
3
+ rho: 16
4
+ L: 3
5
+ theta: 0.80
6
+ K_r: 20
7
+ gamma_C: 0.25
8
+ gamma_D: 0.20
9
+ lambda_C: 0.35
10
+ lambda_D: 0.10
11
+ hc3_buffer_size: 200
12
+ hc3_negatives_per_anchor: 4
13
+ hc3_buffer_refresh_steps: 100
14
+ lr: 3.0e-4
15
+ lr_min: 1.0e-5
16
+ weight_decay: 1.0e-2
17
+ batch_size: 256
18
+ epochs: 10
19
+ warmup_epochs: 1
20
+ grad_clip: 1.0
21
+ early_stop_patience: 5
22
+ early_stop_metric: f1
23
+ encoder_name: michiyasunaga/BioLinkBERT-large
24
+ encoder_frozen: true
25
+ kg_path: data/processed/merged_kg_v2.tsv
26
+ train_path: data/processed/train.json
27
+ dev_path: data/processed/dev.json
28
+ test_path: data/processed/test.json
29
+ min_relation_freq: 50
30
+ seed: 42
31
+ deterministic: true
32
+ micro_batch_size: 256
33
+ grad_accum_steps: 1
34
+ mixed_precision: "no"
35
+ ablation:
36
+ use_csv: true
37
+ use_dbm: true
38
+ use_hc3: true
39
+ use_dc: true
40
+ use_freqcap: true
41
+ csv_pool: mean
42
+ gate_activation: sigmoid
results/hop_stratified_seed1337.json ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "runs\\no_dc\\seed_1337\\best.pt",
3
+ "mode": "autoregressive",
4
+ "threshold": 0.8,
5
+ "per_hop": [
6
+ {
7
+ "hop": 1,
8
+ "n_total": 18043,
9
+ "n_pos": 3000,
10
+ "pos_rate": 0.1662694673834728,
11
+ "precision": 0.8289302728453876,
12
+ "recall": 0.638,
13
+ "f1": 0.7210397438312299,
14
+ "score_mean": 0.5869833794677692,
15
+ "score_std": 0.17652598061395477,
16
+ "score_mean_pos": 0.8367936297555765,
17
+ "score_mean_neg": 0.5371641445503046
18
+ },
19
+ {
20
+ "hop": 2,
21
+ "n_total": 38850,
22
+ "n_pos": 2271,
23
+ "pos_rate": 0.05845559845559845,
24
+ "precision": 0.437440305635148,
25
+ "recall": 0.6050198150594451,
26
+ "f1": 0.507760532150776,
27
+ "score_mean": 0.2152996950047636,
28
+ "score_std": 0.23862252628696062,
29
+ "score_mean_pos": 0.6722047163722261,
30
+ "score_mean_neg": 0.18693283687508516
31
+ },
32
+ {
33
+ "hop": 3,
34
+ "n_total": 45424,
35
+ "n_pos": 1145,
36
+ "pos_rate": 0.025206939063050368,
37
+ "precision": 0.24549918166939444,
38
+ "recall": 0.26200873362445415,
39
+ "f1": 0.25348542458808615,
40
+ "score_mean": 0.06794749980412085,
41
+ "score_std": 0.2020943117933616,
42
+ "score_mean_pos": 0.4629924121601548,
43
+ "score_mean_neg": 0.05773212852998054
44
+ }
45
+ ],
46
+ "hop_x_relation_counts": [
47
+ {
48
+ "relation": "is_a",
49
+ "hop1_n_total": 2223,
50
+ "hop1_n_pos": 1883,
51
+ "hop2_n_total": 32042,
52
+ "hop2_n_pos": 2195,
53
+ "hop3_n_total": 41204,
54
+ "hop3_n_pos": 1136
55
+ },
56
+ {
57
+ "relation": "has_phenotype",
58
+ "hop1_n_total": 15275,
59
+ "hop1_n_pos": 1068,
60
+ "hop2_n_total": 5800,
61
+ "hop2_n_pos": 69,
62
+ "hop3_n_total": 3617,
63
+ "hop3_n_pos": 9
64
+ },
65
+ {
66
+ "relation": "disease_causing_germline_mutation_s_in",
67
+ "hop1_n_total": 337,
68
+ "hop1_n_pos": 37,
69
+ "hop2_n_total": 406,
70
+ "hop2_n_pos": 5,
71
+ "hop3_n_total": 95,
72
+ "hop3_n_pos": 0
73
+ },
74
+ {
75
+ "relation": "major_susceptibility_factor_in",
76
+ "hop1_n_total": 40,
77
+ "hop1_n_pos": 0,
78
+ "hop2_n_total": 349,
79
+ "hop2_n_pos": 1,
80
+ "hop3_n_total": 362,
81
+ "hop3_n_pos": 0
82
+ },
83
+ {
84
+ "relation": "disease_causing_germline_mutation_s_loss_of_function_in",
85
+ "hop1_n_total": 78,
86
+ "hop1_n_pos": 6,
87
+ "hop2_n_total": 99,
88
+ "hop2_n_pos": 1,
89
+ "hop3_n_total": 76,
90
+ "hop3_n_pos": 0
91
+ },
92
+ {
93
+ "relation": "candidate_gene_tested_in",
94
+ "hop1_n_total": 39,
95
+ "hop1_n_pos": 0,
96
+ "hop2_n_total": 84,
97
+ "hop2_n_pos": 0,
98
+ "hop3_n_total": 48,
99
+ "hop3_n_pos": 0
100
+ },
101
+ {
102
+ "relation": "disease_causing_somatic_mutation_s_in",
103
+ "hop1_n_total": 9,
104
+ "hop1_n_pos": 1,
105
+ "hop2_n_total": 49,
106
+ "hop2_n_pos": 0,
107
+ "hop3_n_total": 19,
108
+ "hop3_n_pos": 0
109
+ },
110
+ {
111
+ "relation": "role_in_the_phenotype_of",
112
+ "hop1_n_total": 25,
113
+ "hop1_n_pos": 0,
114
+ "hop2_n_total": 9,
115
+ "hop2_n_pos": 0,
116
+ "hop3_n_total": 0,
117
+ "hop3_n_pos": 0
118
+ },
119
+ {
120
+ "relation": "disease_causing_germline_mutation_s_gain_of_function_in",
121
+ "hop1_n_total": 13,
122
+ "hop1_n_pos": 4,
123
+ "hop2_n_total": 0,
124
+ "hop2_n_pos": 0,
125
+ "hop3_n_total": 1,
126
+ "hop3_n_pos": 0
127
+ },
128
+ {
129
+ "relation": "modifying_germline_mutation_in",
130
+ "hop1_n_total": 2,
131
+ "hop1_n_pos": 1,
132
+ "hop2_n_total": 9,
133
+ "hop2_n_pos": 0,
134
+ "hop3_n_total": 2,
135
+ "hop3_n_pos": 0
136
+ },
137
+ {
138
+ "relation": "part_of_a_fusion_gene_in",
139
+ "hop1_n_total": 2,
140
+ "hop1_n_pos": 0,
141
+ "hop2_n_total": 3,
142
+ "hop2_n_pos": 0,
143
+ "hop3_n_total": 0,
144
+ "hop3_n_pos": 0
145
+ }
146
+ ],
147
+ "per_hop_relation_f1_top2": [
148
+ {
149
+ "relation": "is_a",
150
+ "hop": 1,
151
+ "n_total": 2223,
152
+ "n_pos": 1883,
153
+ "precision": 0.8470535312640576,
154
+ "recall": 1.0,
155
+ "f1": 0.9171943497320995
156
+ },
157
+ {
158
+ "relation": "is_a",
159
+ "hop": 2,
160
+ "n_total": 32042,
161
+ "n_pos": 2195,
162
+ "precision": 0.437440305635148,
163
+ "recall": 0.6259681093394077,
164
+ "f1": 0.5149925037481259
165
+ },
166
+ {
167
+ "relation": "is_a",
168
+ "hop": 3,
169
+ "n_total": 41204,
170
+ "n_pos": 1136,
171
+ "precision": 0.24549918166939444,
172
+ "recall": 0.2640845070422535,
173
+ "f1": 0.2544529262086514
174
+ },
175
+ {
176
+ "relation": "has_phenotype",
177
+ "hop": 1,
178
+ "n_total": 15275,
179
+ "n_pos": 1068,
180
+ "precision": 0.4166666666666667,
181
+ "recall": 0.028089887640449437,
182
+ "f1": 0.05263157894736842
183
+ },
184
+ {
185
+ "relation": "has_phenotype",
186
+ "hop": 2,
187
+ "n_total": 5800,
188
+ "n_pos": 69,
189
+ "precision": 0.0,
190
+ "recall": 0.0,
191
+ "f1": 0.0
192
+ },
193
+ {
194
+ "relation": "has_phenotype",
195
+ "hop": 3,
196
+ "n_total": 3617,
197
+ "n_pos": 9,
198
+ "precision": 0.0,
199
+ "recall": 0.0,
200
+ "f1": 0.0
201
+ }
202
+ ]
203
+ }
results/hop_stratified_seed2024.json ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "runs\\no_dc\\seed_2024\\best.pt",
3
+ "mode": "autoregressive",
4
+ "threshold": 0.8,
5
+ "per_hop": [
6
+ {
7
+ "hop": 1,
8
+ "n_total": 18043,
9
+ "n_pos": 3000,
10
+ "pos_rate": 0.1662694673834728,
11
+ "precision": 0.8206984667802385,
12
+ "recall": 0.6423333333333333,
13
+ "f1": 0.7206432311144353,
14
+ "score_mean": 0.5775352711251205,
15
+ "score_std": 0.18306968503822213,
16
+ "score_mean_pos": 0.835189257889986,
17
+ "score_mean_neg": 0.5261517731330579
18
+ },
19
+ {
20
+ "hop": 2,
21
+ "n_total": 38850,
22
+ "n_pos": 2271,
23
+ "pos_rate": 0.05845559845559845,
24
+ "precision": 0.43853503184713377,
25
+ "recall": 0.6063408190224571,
26
+ "f1": 0.5089632230641287,
27
+ "score_mean": 0.2152888699591641,
28
+ "score_std": 0.23939307661142137,
29
+ "score_mean_pos": 0.6760690421700963,
30
+ "score_mean_neg": 0.18668142385372036
31
+ },
32
+ {
33
+ "hop": 3,
34
+ "n_total": 45424,
35
+ "n_pos": 1145,
36
+ "pos_rate": 0.025206939063050368,
37
+ "precision": 0.24123120973514675,
38
+ "recall": 0.2943231441048035,
39
+ "f1": 0.26514555468135326,
40
+ "score_mean": 0.0668562511488665,
41
+ "score_std": 0.2058770274783993,
42
+ "score_mean_pos": 0.4722513639256785,
43
+ "score_mean_neg": 0.056373236534050214
44
+ }
45
+ ],
46
+ "hop_x_relation_counts": [
47
+ {
48
+ "relation": "is_a",
49
+ "hop1_n_total": 2223,
50
+ "hop1_n_pos": 1883,
51
+ "hop2_n_total": 32042,
52
+ "hop2_n_pos": 2195,
53
+ "hop3_n_total": 41204,
54
+ "hop3_n_pos": 1136
55
+ },
56
+ {
57
+ "relation": "has_phenotype",
58
+ "hop1_n_total": 15275,
59
+ "hop1_n_pos": 1068,
60
+ "hop2_n_total": 5800,
61
+ "hop2_n_pos": 69,
62
+ "hop3_n_total": 3617,
63
+ "hop3_n_pos": 9
64
+ },
65
+ {
66
+ "relation": "disease_causing_germline_mutation_s_in",
67
+ "hop1_n_total": 337,
68
+ "hop1_n_pos": 37,
69
+ "hop2_n_total": 406,
70
+ "hop2_n_pos": 5,
71
+ "hop3_n_total": 95,
72
+ "hop3_n_pos": 0
73
+ },
74
+ {
75
+ "relation": "major_susceptibility_factor_in",
76
+ "hop1_n_total": 40,
77
+ "hop1_n_pos": 0,
78
+ "hop2_n_total": 349,
79
+ "hop2_n_pos": 1,
80
+ "hop3_n_total": 362,
81
+ "hop3_n_pos": 0
82
+ },
83
+ {
84
+ "relation": "disease_causing_germline_mutation_s_loss_of_function_in",
85
+ "hop1_n_total": 78,
86
+ "hop1_n_pos": 6,
87
+ "hop2_n_total": 99,
88
+ "hop2_n_pos": 1,
89
+ "hop3_n_total": 76,
90
+ "hop3_n_pos": 0
91
+ },
92
+ {
93
+ "relation": "candidate_gene_tested_in",
94
+ "hop1_n_total": 39,
95
+ "hop1_n_pos": 0,
96
+ "hop2_n_total": 84,
97
+ "hop2_n_pos": 0,
98
+ "hop3_n_total": 48,
99
+ "hop3_n_pos": 0
100
+ },
101
+ {
102
+ "relation": "disease_causing_somatic_mutation_s_in",
103
+ "hop1_n_total": 9,
104
+ "hop1_n_pos": 1,
105
+ "hop2_n_total": 49,
106
+ "hop2_n_pos": 0,
107
+ "hop3_n_total": 19,
108
+ "hop3_n_pos": 0
109
+ },
110
+ {
111
+ "relation": "role_in_the_phenotype_of",
112
+ "hop1_n_total": 25,
113
+ "hop1_n_pos": 0,
114
+ "hop2_n_total": 9,
115
+ "hop2_n_pos": 0,
116
+ "hop3_n_total": 0,
117
+ "hop3_n_pos": 0
118
+ },
119
+ {
120
+ "relation": "disease_causing_germline_mutation_s_gain_of_function_in",
121
+ "hop1_n_total": 13,
122
+ "hop1_n_pos": 4,
123
+ "hop2_n_total": 0,
124
+ "hop2_n_pos": 0,
125
+ "hop3_n_total": 1,
126
+ "hop3_n_pos": 0
127
+ },
128
+ {
129
+ "relation": "modifying_germline_mutation_in",
130
+ "hop1_n_total": 2,
131
+ "hop1_n_pos": 1,
132
+ "hop2_n_total": 9,
133
+ "hop2_n_pos": 0,
134
+ "hop3_n_total": 2,
135
+ "hop3_n_pos": 0
136
+ },
137
+ {
138
+ "relation": "part_of_a_fusion_gene_in",
139
+ "hop1_n_total": 2,
140
+ "hop1_n_pos": 0,
141
+ "hop2_n_total": 3,
142
+ "hop2_n_pos": 0,
143
+ "hop3_n_total": 0,
144
+ "hop3_n_pos": 0
145
+ }
146
+ ],
147
+ "per_hop_relation_f1_top2": [
148
+ {
149
+ "relation": "is_a",
150
+ "hop": 1,
151
+ "n_total": 2223,
152
+ "n_pos": 1883,
153
+ "precision": 0.8470535312640576,
154
+ "recall": 1.0,
155
+ "f1": 0.9171943497320995
156
+ },
157
+ {
158
+ "relation": "is_a",
159
+ "hop": 2,
160
+ "n_total": 32042,
161
+ "n_pos": 2195,
162
+ "precision": 0.43853503184713377,
163
+ "recall": 0.6273348519362186,
164
+ "f1": 0.5162136832239925
165
+ },
166
+ {
167
+ "relation": "is_a",
168
+ "hop": 3,
169
+ "n_total": 41204,
170
+ "n_pos": 1136,
171
+ "precision": 0.24123120973514675,
172
+ "recall": 0.2966549295774648,
173
+ "f1": 0.26608764311093563
174
+ },
175
+ {
176
+ "relation": "has_phenotype",
177
+ "hop": 1,
178
+ "n_total": 15275,
179
+ "n_pos": 1068,
180
+ "precision": 0.36936936936936937,
181
+ "recall": 0.03838951310861423,
182
+ "f1": 0.06955046649703138
183
+ },
184
+ {
185
+ "relation": "has_phenotype",
186
+ "hop": 2,
187
+ "n_total": 5800,
188
+ "n_pos": 69,
189
+ "precision": 0.0,
190
+ "recall": 0.0,
191
+ "f1": 0.0
192
+ },
193
+ {
194
+ "relation": "has_phenotype",
195
+ "hop": 3,
196
+ "n_total": 3617,
197
+ "n_pos": 9,
198
+ "precision": 0.0,
199
+ "recall": 0.0,
200
+ "f1": 0.0
201
+ }
202
+ ]
203
+ }
results/hop_stratified_seed42.json ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "runs\\no_dc\\seed_42\\best.pt",
3
+ "mode": "autoregressive",
4
+ "threshold": 0.8,
5
+ "per_hop": [
6
+ {
7
+ "hop": 1,
8
+ "n_total": 18043,
9
+ "n_pos": 3000,
10
+ "pos_rate": 0.1662694673834728,
11
+ "precision": 0.8206984667802385,
12
+ "recall": 0.6423333333333333,
13
+ "f1": 0.7206432311144353,
14
+ "score_mean": 0.5821448763357472,
15
+ "score_std": 0.1806795479628059,
16
+ "score_mean_pos": 0.8365760689576467,
17
+ "score_mean_neg": 0.5314040947186696
18
+ },
19
+ {
20
+ "hop": 2,
21
+ "n_total": 38850,
22
+ "n_pos": 2271,
23
+ "pos_rate": 0.05845559845559845,
24
+ "precision": 0.43746031746031744,
25
+ "recall": 0.6067811536767944,
26
+ "f1": 0.5083932853717026,
27
+ "score_mean": 0.2155249401183225,
28
+ "score_std": 0.2394194361365147,
29
+ "score_mean_pos": 0.6760359883365004,
30
+ "score_mean_neg": 0.1869342025228857
31
+ },
32
+ {
33
+ "hop": 3,
34
+ "n_total": 45424,
35
+ "n_pos": 1145,
36
+ "pos_rate": 0.025206939063050368,
37
+ "precision": 0.24103299856527977,
38
+ "recall": 0.2934497816593886,
39
+ "f1": 0.264671130366286,
40
+ "score_mean": 0.06681433546379811,
41
+ "score_std": 0.20566269681681607,
42
+ "score_mean_pos": 0.4691532548687799,
43
+ "score_mean_neg": 0.0564103502175481
44
+ }
45
+ ],
46
+ "hop_x_relation_counts": [
47
+ {
48
+ "relation": "is_a",
49
+ "hop1_n_total": 2223,
50
+ "hop1_n_pos": 1883,
51
+ "hop2_n_total": 32042,
52
+ "hop2_n_pos": 2195,
53
+ "hop3_n_total": 41204,
54
+ "hop3_n_pos": 1136
55
+ },
56
+ {
57
+ "relation": "has_phenotype",
58
+ "hop1_n_total": 15275,
59
+ "hop1_n_pos": 1068,
60
+ "hop2_n_total": 5800,
61
+ "hop2_n_pos": 69,
62
+ "hop3_n_total": 3617,
63
+ "hop3_n_pos": 9
64
+ },
65
+ {
66
+ "relation": "disease_causing_germline_mutation_s_in",
67
+ "hop1_n_total": 337,
68
+ "hop1_n_pos": 37,
69
+ "hop2_n_total": 406,
70
+ "hop2_n_pos": 5,
71
+ "hop3_n_total": 95,
72
+ "hop3_n_pos": 0
73
+ },
74
+ {
75
+ "relation": "major_susceptibility_factor_in",
76
+ "hop1_n_total": 40,
77
+ "hop1_n_pos": 0,
78
+ "hop2_n_total": 349,
79
+ "hop2_n_pos": 1,
80
+ "hop3_n_total": 362,
81
+ "hop3_n_pos": 0
82
+ },
83
+ {
84
+ "relation": "disease_causing_germline_mutation_s_loss_of_function_in",
85
+ "hop1_n_total": 78,
86
+ "hop1_n_pos": 6,
87
+ "hop2_n_total": 99,
88
+ "hop2_n_pos": 1,
89
+ "hop3_n_total": 76,
90
+ "hop3_n_pos": 0
91
+ },
92
+ {
93
+ "relation": "candidate_gene_tested_in",
94
+ "hop1_n_total": 39,
95
+ "hop1_n_pos": 0,
96
+ "hop2_n_total": 84,
97
+ "hop2_n_pos": 0,
98
+ "hop3_n_total": 48,
99
+ "hop3_n_pos": 0
100
+ },
101
+ {
102
+ "relation": "disease_causing_somatic_mutation_s_in",
103
+ "hop1_n_total": 9,
104
+ "hop1_n_pos": 1,
105
+ "hop2_n_total": 49,
106
+ "hop2_n_pos": 0,
107
+ "hop3_n_total": 19,
108
+ "hop3_n_pos": 0
109
+ },
110
+ {
111
+ "relation": "role_in_the_phenotype_of",
112
+ "hop1_n_total": 25,
113
+ "hop1_n_pos": 0,
114
+ "hop2_n_total": 9,
115
+ "hop2_n_pos": 0,
116
+ "hop3_n_total": 0,
117
+ "hop3_n_pos": 0
118
+ },
119
+ {
120
+ "relation": "disease_causing_germline_mutation_s_gain_of_function_in",
121
+ "hop1_n_total": 13,
122
+ "hop1_n_pos": 4,
123
+ "hop2_n_total": 0,
124
+ "hop2_n_pos": 0,
125
+ "hop3_n_total": 1,
126
+ "hop3_n_pos": 0
127
+ },
128
+ {
129
+ "relation": "modifying_germline_mutation_in",
130
+ "hop1_n_total": 2,
131
+ "hop1_n_pos": 1,
132
+ "hop2_n_total": 9,
133
+ "hop2_n_pos": 0,
134
+ "hop3_n_total": 2,
135
+ "hop3_n_pos": 0
136
+ },
137
+ {
138
+ "relation": "part_of_a_fusion_gene_in",
139
+ "hop1_n_total": 2,
140
+ "hop1_n_pos": 0,
141
+ "hop2_n_total": 3,
142
+ "hop2_n_pos": 0,
143
+ "hop3_n_total": 0,
144
+ "hop3_n_pos": 0
145
+ }
146
+ ],
147
+ "per_hop_relation_f1_top2": [
148
+ {
149
+ "relation": "is_a",
150
+ "hop": 1,
151
+ "n_total": 2223,
152
+ "n_pos": 1883,
153
+ "precision": 0.8470535312640576,
154
+ "recall": 1.0,
155
+ "f1": 0.9171943497320995
156
+ },
157
+ {
158
+ "relation": "is_a",
159
+ "hop": 2,
160
+ "n_total": 32042,
161
+ "n_pos": 2195,
162
+ "precision": 0.43746031746031744,
163
+ "recall": 0.6277904328018223,
164
+ "f1": 0.515622076707203
165
+ },
166
+ {
167
+ "relation": "is_a",
168
+ "hop": 3,
169
+ "n_total": 41204,
170
+ "n_pos": 1136,
171
+ "precision": 0.24103299856527977,
172
+ "recall": 0.29577464788732394,
173
+ "f1": 0.2656126482213439
174
+ },
175
+ {
176
+ "relation": "has_phenotype",
177
+ "hop": 1,
178
+ "n_total": 15275,
179
+ "n_pos": 1068,
180
+ "precision": 0.375,
181
+ "recall": 0.03932584269662921,
182
+ "f1": 0.0711864406779661
183
+ },
184
+ {
185
+ "relation": "has_phenotype",
186
+ "hop": 2,
187
+ "n_total": 5800,
188
+ "n_pos": 69,
189
+ "precision": 0.0,
190
+ "recall": 0.0,
191
+ "f1": 0.0
192
+ },
193
+ {
194
+ "relation": "has_phenotype",
195
+ "hop": 3,
196
+ "n_total": 3617,
197
+ "n_pos": 9,
198
+ "precision": 0.0,
199
+ "recall": 0.0,
200
+ "f1": 0.0
201
+ }
202
+ ]
203
+ }
runs/dc_lambda010/seed_1337/config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "config": {
3
+ "d": 1024,
4
+ "rho": 16,
5
+ "L": 3,
6
+ "theta": 0.8,
7
+ "K_r": 20,
8
+ "gamma_C": 0.25,
9
+ "gamma_D": 0.2,
10
+ "lambda_C": 0.35,
11
+ "lambda_D": 0.1,
12
+ "hc3_buffer_size": 200,
13
+ "hc3_negatives_per_anchor": 4,
14
+ "hc3_buffer_refresh_steps": 100,
15
+ "lr": 0.0003,
16
+ "lr_min": 1e-05,
17
+ "weight_decay": 0.01,
18
+ "batch_size": 256,
19
+ "epochs": 10,
20
+ "warmup_epochs": 1,
21
+ "grad_clip": 1.0,
22
+ "early_stop_patience": 5,
23
+ "early_stop_metric": "f1",
24
+ "encoder_name": "michiyasunaga/BioLinkBERT-large",
25
+ "encoder_frozen": true,
26
+ "kg_path": "data/processed/merged_kg_v2.tsv",
27
+ "train_path": "data/processed/train.json",
28
+ "dev_path": "data/processed/dev.json",
29
+ "test_path": "data/processed/test.json",
30
+ "min_relation_freq": 50,
31
+ "seed": 1337,
32
+ "deterministic": true,
33
+ "micro_batch_size": 4,
34
+ "grad_accum_steps": 64,
35
+ "mixed_precision": "fp16"
36
+ },
37
+ "config_hash": "3ea32a7544a3",
38
+ "ablation": {
39
+ "use_csv": true,
40
+ "use_dbm": true,
41
+ "use_hc3": true,
42
+ "use_dc": true,
43
+ "use_freqcap": true,
44
+ "csv_pool": "mean",
45
+ "gate_activation": "sigmoid"
46
+ },
47
+ "args": {
48
+ "config": "configs\\dc_lambda010.yaml",
49
+ "seed": 1337,
50
+ "output_root": "runs",
51
+ "cache_dir": "cache",
52
+ "device": "cuda",
53
+ "debug_subset": null
54
+ },
55
+ "torch_version": "2.2.1+cu118",
56
+ "cuda_available": true,
57
+ "gpu_name": "NVIDIA GeForce RTX 4060 Laptop GPU",
58
+ "python_version": "3.11.5",
59
+ "platform": "Windows-10-10.0.26200-SP0"
60
+ }
runs/dc_lambda010/seed_1337/final_metrics.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "epoch": 2,
3
+ "train_loss": 0.015113744206629129,
4
+ "train_bce": 0.013060060305446928,
5
+ "train_dc": 0.007093780665091978,
6
+ "train_hc3": 0.0038408746070336145,
7
+ "dev_precision": 0.557453666398066,
8
+ "dev_recall": 0.5522030651340997,
9
+ "dev_f1": 0.5548159435399791,
10
+ "dev_map": 0.6652193527216996,
11
+ "learning_rate": 0.0002912019213331988,
12
+ "wall_clock_seconds": 186.1106984615326
13
+ }
runs/dc_lambda010/seed_1337/history.jsonl ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {"epoch": 1, "train_loss": 0.019308436556193354, "train_bce": 0.017321521228503493, "train_dc": 0.006426094722226206, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5985345160046278, "dev_recall": 0.4955300127713921, "dev_f1": 0.5421834061135371, "dev_map": 0.6632368465729074, "learning_rate": 0.00029999997896208426, "wall_clock_seconds": 619.3529281616211}
2
+ {"epoch": 2, "train_loss": 0.015113744206629129, "train_bce": 0.013060060305446928, "train_dc": 0.007093780665091978, "train_hc3": 0.0038408746070336145, "dev_precision": 0.557453666398066, "dev_recall": 0.5522030651340997, "dev_f1": 0.5548159435399791, "dev_map": 0.6652193527216996, "learning_rate": 0.0002912019213331988, "wall_clock_seconds": 186.1106984615326}
3
+ {"epoch": 3, "train_loss": 0.014471906816703607, "train_bce": 0.012516204908701532, "train_dc": 0.006113960761997171, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5298607992815447, "dev_recall": 0.5651340996168582, "dev_f1": 0.5469293163383545, "dev_map": 0.6504335108188682, "learning_rate": 0.0002659256770308768, "wall_clock_seconds": 212.41909408569336}
4
+ {"epoch": 4, "train_loss": 0.014280542490509716, "train_bce": 0.012355582342246117, "train_dc": 0.005806542972279172, "train_hc3": 0.0038408746070336145, "dev_precision": 0.519022132214668, "dev_recall": 0.5727969348659003, "dev_f1": 0.5445852621992866, "dev_map": 0.6541289985099419, "learning_rate": 0.00022722925484045786, "wall_clock_seconds": 212.44377183914185}
5
+ {"epoch": 5, "train_loss": 0.014165094377912609, "train_bce": 0.01225624893248278, "train_dc": 0.005645396599345712, "train_hc3": 0.0038408746070336145, "dev_precision": 0.513314447592068, "dev_recall": 0.578544061302682, "dev_f1": 0.5439807865505855, "dev_map": 0.6503835073068308, "learning_rate": 0.0001797942837889505, "wall_clock_seconds": 213.1819248199463}
6
+ {"epoch": 6, "train_loss": 0.014082577235894995, "train_bce": 0.012186359327338423, "train_dc": 0.005519120800016324, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5113109456231558, "dev_recall": 0.5809386973180076, "dev_f1": 0.5439055377027128, "dev_map": 0.6483537289664857, "learning_rate": 0.0001293596132272652, "wall_clock_seconds": 198.86546444892883}
7
+ {"epoch": 7, "train_loss": 0.014021225025477754, "train_bce": 0.012130890549783672, "train_dc": 0.005460286833092735, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5097874720357942, "dev_recall": 0.5820561941251596, "dev_f1": 0.5435301132975552, "dev_map": 0.6470899056766319, "learning_rate": 8.202700668556269e-05, "wall_clock_seconds": 206.0203673839569}
runs/dc_lambda010/seed_2024/config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "config": {
3
+ "d": 1024,
4
+ "rho": 16,
5
+ "L": 3,
6
+ "theta": 0.8,
7
+ "K_r": 20,
8
+ "gamma_C": 0.25,
9
+ "gamma_D": 0.2,
10
+ "lambda_C": 0.35,
11
+ "lambda_D": 0.1,
12
+ "hc3_buffer_size": 200,
13
+ "hc3_negatives_per_anchor": 4,
14
+ "hc3_buffer_refresh_steps": 100,
15
+ "lr": 0.0003,
16
+ "lr_min": 1e-05,
17
+ "weight_decay": 0.01,
18
+ "batch_size": 256,
19
+ "epochs": 10,
20
+ "warmup_epochs": 1,
21
+ "grad_clip": 1.0,
22
+ "early_stop_patience": 5,
23
+ "early_stop_metric": "f1",
24
+ "encoder_name": "michiyasunaga/BioLinkBERT-large",
25
+ "encoder_frozen": true,
26
+ "kg_path": "data/processed/merged_kg_v2.tsv",
27
+ "train_path": "data/processed/train.json",
28
+ "dev_path": "data/processed/dev.json",
29
+ "test_path": "data/processed/test.json",
30
+ "min_relation_freq": 50,
31
+ "seed": 2024,
32
+ "deterministic": true,
33
+ "micro_batch_size": 4,
34
+ "grad_accum_steps": 64,
35
+ "mixed_precision": "fp16"
36
+ },
37
+ "config_hash": "b5e7f7a791a9",
38
+ "ablation": {
39
+ "use_csv": true,
40
+ "use_dbm": true,
41
+ "use_hc3": true,
42
+ "use_dc": true,
43
+ "use_freqcap": true,
44
+ "csv_pool": "mean",
45
+ "gate_activation": "sigmoid"
46
+ },
47
+ "args": {
48
+ "config": "configs\\dc_lambda010.yaml",
49
+ "seed": 2024,
50
+ "output_root": "runs",
51
+ "cache_dir": "cache",
52
+ "device": "cuda",
53
+ "debug_subset": null
54
+ },
55
+ "torch_version": "2.2.1+cu118",
56
+ "cuda_available": true,
57
+ "gpu_name": "NVIDIA GeForce RTX 4060 Laptop GPU",
58
+ "python_version": "3.11.5",
59
+ "platform": "Windows-10-10.0.26200-SP0"
60
+ }
runs/dc_lambda010/seed_2024/final_metrics.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "epoch": 2,
3
+ "train_loss": 0.015118593132108057,
4
+ "train_bce": 0.013066735937389428,
5
+ "train_dc": 0.007075513935245275,
6
+ "train_hc3": 0.0038408746070336145,
7
+ "dev_precision": 0.5619483491337038,
8
+ "dev_recall": 0.5488505747126436,
9
+ "dev_f1": 0.5553222419641415,
10
+ "dev_map": 0.6670695719845979,
11
+ "learning_rate": 0.0002912019213331988,
12
+ "wall_clock_seconds": 213.57218647003174
13
+ }
runs/dc_lambda010/seed_2024/history.jsonl ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {"epoch": 1, "train_loss": 0.019277647056187345, "train_bce": 0.017283399351028916, "train_dc": 0.006499418423889856, "train_hc3": 0.0038408746070336145, "dev_precision": 0.6061260553701159, "dev_recall": 0.492816091954023, "dev_f1": 0.5436294796160959, "dev_map": 0.6669038088568597, "learning_rate": 0.00029999997896208426, "wall_clock_seconds": 622.3812205791473}
2
+ {"epoch": 2, "train_loss": 0.015118593132108057, "train_bce": 0.013066735937389428, "train_dc": 0.007075513935245275, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5619483491337038, "dev_recall": 0.5488505747126436, "dev_f1": 0.5553222419641415, "dev_map": 0.6670695719845979, "learning_rate": 0.0002912019213331988, "wall_clock_seconds": 213.57218647003174}
3
+ {"epoch": 3, "train_loss": 0.014484207685618339, "train_bce": 0.012522109174555006, "train_dc": 0.006177927138163685, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5257839203447764, "dev_recall": 0.5648148148148148, "dev_f1": 0.5446009389671361, "dev_map": 0.660532568332498, "learning_rate": 0.0002659256770308768, "wall_clock_seconds": 213.4029757976532}
4
+ {"epoch": 4, "train_loss": 0.014288917713151274, "train_bce": 0.0123579115688142, "train_dc": 0.005867003395187836, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5205180442374855, "dev_recall": 0.5710408684546615, "dev_f1": 0.5446102314250914, "dev_map": 0.6559184854984927, "learning_rate": 0.00022722925484045786, "wall_clock_seconds": 212.59179186820984}
5
+ {"epoch": 5, "train_loss": 0.014177476748754865, "train_bce": 0.012265763558042061, "train_dc": 0.005674073652079245, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5156361559331715, "dev_recall": 0.5764687100893997, "dev_f1": 0.5443581819552272, "dev_map": 0.6538870871764543, "learning_rate": 0.0001797942837889505, "wall_clock_seconds": 199.63850927352905}
6
+ {"epoch": 6, "train_loss": 0.014095762579349268, "train_bce": 0.012200400114495786, "train_dc": 0.0055105664081980775, "train_hc3": 0.0038408746070336145, "dev_precision": 0.514346590909091, "dev_recall": 0.5780651340996169, "dev_f1": 0.5443475646422128, "dev_map": 0.6449853021212014, "learning_rate": 0.0001293596132272652, "wall_clock_seconds": 205.6528124809265}
7
+ {"epoch": 7, "train_loss": 0.014042604747375334, "train_bce": 0.012149665409334735, "train_dc": 0.005486335355188235, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5134906060177992, "dev_recall": 0.5803001277139208, "dev_f1": 0.5448549801393989, "dev_map": 0.6454874155364534, "learning_rate": 8.202700668556269e-05, "wall_clock_seconds": 201.8851432800293}
runs/dc_lambda010/seed_42/config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "config": {
3
+ "d": 1024,
4
+ "rho": 16,
5
+ "L": 3,
6
+ "theta": 0.8,
7
+ "K_r": 20,
8
+ "gamma_C": 0.25,
9
+ "gamma_D": 0.2,
10
+ "lambda_C": 0.35,
11
+ "lambda_D": 0.1,
12
+ "hc3_buffer_size": 200,
13
+ "hc3_negatives_per_anchor": 4,
14
+ "hc3_buffer_refresh_steps": 100,
15
+ "lr": 0.0003,
16
+ "lr_min": 1e-05,
17
+ "weight_decay": 0.01,
18
+ "batch_size": 256,
19
+ "epochs": 10,
20
+ "warmup_epochs": 1,
21
+ "grad_clip": 1.0,
22
+ "early_stop_patience": 5,
23
+ "early_stop_metric": "f1",
24
+ "encoder_name": "michiyasunaga/BioLinkBERT-large",
25
+ "encoder_frozen": true,
26
+ "kg_path": "data/processed/merged_kg_v2.tsv",
27
+ "train_path": "data/processed/train.json",
28
+ "dev_path": "data/processed/dev.json",
29
+ "test_path": "data/processed/test.json",
30
+ "min_relation_freq": 50,
31
+ "seed": 42,
32
+ "deterministic": true,
33
+ "micro_batch_size": 4,
34
+ "grad_accum_steps": 64,
35
+ "mixed_precision": "fp16"
36
+ },
37
+ "config_hash": "acdcc94d6418",
38
+ "ablation": {
39
+ "use_csv": true,
40
+ "use_dbm": true,
41
+ "use_hc3": true,
42
+ "use_dc": true,
43
+ "use_freqcap": true,
44
+ "csv_pool": "mean",
45
+ "gate_activation": "sigmoid"
46
+ },
47
+ "args": {
48
+ "config": "configs\\dc_lambda010.yaml",
49
+ "seed": 42,
50
+ "output_root": "runs",
51
+ "cache_dir": "cache",
52
+ "device": "cuda",
53
+ "debug_subset": null
54
+ },
55
+ "torch_version": "2.2.1+cu118",
56
+ "cuda_available": true,
57
+ "gpu_name": "NVIDIA GeForce RTX 4060 Laptop GPU",
58
+ "python_version": "3.11.5",
59
+ "platform": "Windows-10-10.0.26200-SP0"
60
+ }
runs/dc_lambda010/seed_42/final_metrics.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "epoch": 2,
3
+ "train_loss": 0.015141186450905479,
4
+ "train_bce": 0.013076732859356713,
5
+ "train_dc": 0.007201478043490602,
6
+ "train_hc3": 0.0038408746070336145,
7
+ "dev_precision": 0.5573135292218275,
8
+ "dev_recall": 0.5510855683269477,
9
+ "dev_f1": 0.5541820516936907,
10
+ "dev_map": 0.6666678369356035,
11
+ "learning_rate": 0.0002912019213331988,
12
+ "wall_clock_seconds": 377.35128116607666
13
+ }
runs/dc_lambda010/seed_42/history.jsonl ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {"epoch": 1, "train_loss": 0.01927823839477672, "train_bce": 0.017281827372716014, "train_dc": 0.00652105152939959, "train_hc3": 0.0038408746070336145, "dev_precision": 0.6033508669394116, "dev_recall": 0.49441251596424013, "dev_f1": 0.5434763534263403, "dev_map": 0.6665085356352869, "learning_rate": 0.00029999997896208426, "wall_clock_seconds": 1665.2696824073792}
2
+ {"epoch": 2, "train_loss": 0.015141186450905479, "train_bce": 0.013076732859356713, "train_dc": 0.007201478043490602, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5573135292218275, "dev_recall": 0.5510855683269477, "dev_f1": 0.5541820516936907, "dev_map": 0.6666678369356035, "learning_rate": 0.0002912019213331988, "wall_clock_seconds": 377.35128116607666}
3
+ {"epoch": 3, "train_loss": 0.014495296075031932, "train_bce": 0.012531945475302887, "train_dc": 0.0061904477334503065, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5322264150943397, "dev_recall": 0.5628991060025543, "dev_f1": 0.5471332143688417, "dev_map": 0.6556946388504439, "learning_rate": 0.0002659256770308768, "wall_clock_seconds": 581.3262515068054}
4
+ {"epoch": 4, "train_loss": 0.014289841654040781, "train_bce": 0.012365499651170522, "train_dc": 0.005800362000973038, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5198257080610021, "dev_recall": 0.571360153256705, "dev_f1": 0.544375998174766, "dev_map": 0.6576822652564643, "learning_rate": 0.00022722925484045786, "wall_clock_seconds": 575.7329406738281}
5
+ {"epoch": 5, "train_loss": 0.014175985931480353, "train_bce": 0.0122669513011922, "train_dc": 0.005647288217739071, "train_hc3": 0.0038408746070336145, "dev_precision": 0.514005403099673, "dev_recall": 0.5771072796934866, "dev_f1": 0.5437316687974731, "dev_map": 0.6530293753344071, "learning_rate": 0.0001797942837889505, "wall_clock_seconds": 483.29707646369934}
6
+ {"epoch": 6, "train_loss": 0.01410277469829272, "train_bce": 0.012200795751861889, "train_dc": 0.005576731416304824, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5119299731752083, "dev_recall": 0.5788633461047255, "dev_f1": 0.5433430733498165, "dev_map": 0.6493538105672264, "learning_rate": 0.0001293596132272652, "wall_clock_seconds": 594.0699644088745}
7
+ {"epoch": 7, "train_loss": 0.014035929312789701, "train_bce": 0.012146426412817928, "train_dc": 0.005451970974499989, "train_hc3": 0.0038408746070336145, "dev_precision": 0.5098039215686274, "dev_recall": 0.5810983397190294, "dev_f1": 0.5431214562817069, "dev_map": 0.6434185587690961, "learning_rate": 8.202700668556269e-05, "wall_clock_seconds": 558.7890524864197}
scripts/hop_stratified_analysis.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ hop_stratified_analysis.py -- Cross-tabulate hop and relation.
4
+
5
+ Loads a trained checkpoint, scores the test set, then aggregates by
6
+ (hop, relation) pairs. This answers:
7
+
8
+ - Which relations appear at which hops? (the hop x relation count matrix)
9
+ - What is per-hop, per-relation F1?
10
+ - Is the hop3 precision drop driven by has_phenotype, or by all relations?
11
+ - How do score distributions differ across hops?
12
+
13
+ Usage:
14
+ python scripts/hop_stratified_analysis.py \
15
+ --checkpoint runs/no_dc/seed_42/best.pt \
16
+ --threshold 0.80 \
17
+ --mode autoregressive \
18
+ --output-json results/hop_stratified_seed42.json
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import logging
25
+ import sys
26
+ from collections import defaultdict
27
+ from pathlib import Path
28
+
29
+ import numpy as np
30
+ import torch
31
+
32
+ ROOT = Path(__file__).parent.parent
33
+ if str(ROOT) not in sys.path:
34
+ sys.path.insert(0, str(ROOT))
35
+
36
+ from caff import (
37
+ AblationFlags,
38
+ CAFFConfig,
39
+ CAFFEvaluator,
40
+ CAFFModel,
41
+ CAFFTripleDataset,
42
+ CachedBFSExtractor,
43
+ FrozenBioEncoder,
44
+ KnowledgeGraph,
45
+ RelationEmbeddingCache,
46
+ load_qa_split,
47
+ )
48
+ from caff.evaluator import precision_recall_f1
49
+ from caff.utils.logging import setup_logging
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+
54
+ def parse_args() -> argparse.Namespace:
55
+ p = argparse.ArgumentParser(description="Hop x relation cross-tabulation.")
56
+ p.add_argument("--checkpoint", required=True)
57
+ p.add_argument("--test-split", default=None)
58
+ p.add_argument("--cache-dir", default="cache")
59
+ p.add_argument("--mode", default="autoregressive",
60
+ choices=["teacher_forced", "autoregressive"])
61
+ p.add_argument("--threshold", type=float, default=None)
62
+ p.add_argument("--output-json", default=None)
63
+ p.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
64
+ return p.parse_args()
65
+
66
+
67
+ def load_checkpoint(ckpt_path: str, device: str, cache_dir: Path):
68
+ payload = torch.load(ckpt_path, map_location=device)
69
+ config = CAFFConfig(**payload["config"])
70
+ ablation = AblationFlags()
71
+ logger.info(f"Loading KG from {config.kg_path}...")
72
+ kg = KnowledgeGraph.from_tsv(config.kg_path, min_relation_freq=50)
73
+ encoder = FrozenBioEncoder(config.encoder_name, device=device)
74
+ rel_cache = RelationEmbeddingCache(
75
+ encoder, kg.relations,
76
+ cache_path=cache_dir / "relation_embeddings.pt",
77
+ )
78
+ model = CAFFModel(config, rel_cache, ablation=ablation).to(device)
79
+ model.load_state_dict(payload["model"])
80
+ model.eval()
81
+ logger.info(f"Restored checkpoint from {ckpt_path}")
82
+ return model, config, encoder, kg
83
+
84
+
85
+ def main() -> None:
86
+ args = parse_args()
87
+ setup_logging(level="INFO")
88
+ cache_dir = Path(args.cache_dir)
89
+
90
+ model, config, encoder, kg = load_checkpoint(args.checkpoint, args.device, cache_dir)
91
+
92
+ test_path = args.test_split or config.test_path
93
+ test_recs = load_qa_split(test_path)
94
+ bfs = CachedBFSExtractor(kg, L=config.L, K_r=config.K_r,
95
+ cache_dir=cache_dir / "bfs")
96
+ test_ds = CAFFTripleDataset(test_recs, bfs, require_gold=True)
97
+
98
+ threshold = args.threshold if args.threshold is not None else config.theta
99
+ evaluator = CAFFEvaluator(
100
+ config=config, encoder=encoder, mode=args.mode, threshold=threshold,
101
+ )
102
+ logger.info(f"Scoring test set (mode={args.mode}, theta={threshold})...")
103
+ scores, instances, _retained = evaluator._score_dataset(model, test_ds)
104
+ scores_np = scores.cpu().numpy() if torch.is_tensor(scores) else np.asarray(scores)
105
+
106
+ # Aggregate by (hop, relation)
107
+ by_key_scores: dict[tuple[int, str], list[float]] = defaultdict(list)
108
+ by_key_labels: dict[tuple[int, str], list[int]] = defaultdict(list)
109
+ # Also aggregate by hop only
110
+ by_hop_scores: dict[int, list[float]] = defaultdict(list)
111
+ by_hop_labels: dict[int, list[int]] = defaultdict(list)
112
+
113
+ for inst, sc in zip(instances, scores_np.tolist()):
114
+ key = (inst.hop, inst.relation)
115
+ by_key_scores[key].append(sc)
116
+ by_key_labels[key].append(inst.label)
117
+ by_hop_scores[inst.hop].append(sc)
118
+ by_hop_labels[inst.hop].append(inst.label)
119
+
120
+ # Per-hop metrics
121
+ hop_rows = []
122
+ print()
123
+ print("=" * 96)
124
+ print(f"Hop-stratified summary (mode={args.mode}, theta={threshold})")
125
+ print(f"Checkpoint: {args.checkpoint}")
126
+ print("=" * 96)
127
+ print(f"{'hop':>4} | {'n_total':>8} | {'n_pos':>6} | {'pos%':>6} | "
128
+ f"{'prec':>6} | {'recall':>6} | {'F1':>6} | "
129
+ f"{'score_mean':>10} | {'score_std':>9}")
130
+ print("-" * 96)
131
+ for hop in sorted(by_hop_scores.keys()):
132
+ s = np.asarray(by_hop_scores[hop])
133
+ l = np.asarray(by_hop_labels[hop])
134
+ n_total = len(l)
135
+ n_pos = int(l.sum())
136
+ pos_rate = n_pos / n_total if n_total > 0 else 0.0
137
+ m = precision_recall_f1(s, l, threshold=threshold)
138
+ hop_rows.append({
139
+ "hop": hop,
140
+ "n_total": n_total,
141
+ "n_pos": n_pos,
142
+ "pos_rate": pos_rate,
143
+ "precision": m["precision"],
144
+ "recall": m["recall"],
145
+ "f1": m["f1"],
146
+ "score_mean": float(s.mean()),
147
+ "score_std": float(s.std()),
148
+ "score_mean_pos": float(s[l == 1].mean()) if n_pos > 0 else None,
149
+ "score_mean_neg": float(s[l == 0].mean()) if (n_total - n_pos) > 0 else None,
150
+ })
151
+ print(f"{hop:>4} | {n_total:>8} | {n_pos:>6} | {pos_rate*100:>5.1f}% | "
152
+ f"{m['precision']:>6.4f} | {m['recall']:>6.4f} | {m['f1']:>6.4f} | "
153
+ f"{s.mean():>10.4f} | {s.std():>9.4f}")
154
+
155
+ # Hop x relation cross-tab (counts)
156
+ print()
157
+ print("=" * 96)
158
+ print(f"Hop x relation counts (n_total, n_positive)")
159
+ print("=" * 96)
160
+ relations_sorted = sorted({rel for (_, rel) in by_key_scores.keys()},
161
+ key=lambda r: -sum(len(by_key_labels[(h, r)])
162
+ for h in [1, 2, 3]))
163
+ print(f"{'relation':<55} | {'hop 1':>14} | {'hop 2':>14} | {'hop 3':>14}")
164
+ print("-" * 96)
165
+ cross_rows = []
166
+ for rel in relations_sorted:
167
+ cells = []
168
+ rel_row = {"relation": rel}
169
+ for hop in [1, 2, 3]:
170
+ key = (hop, rel)
171
+ n = len(by_key_labels.get(key, []))
172
+ npos = int(sum(by_key_labels.get(key, [])))
173
+ cells.append(f"{n:>6}/{npos:<6}")
174
+ rel_row[f"hop{hop}_n_total"] = n
175
+ rel_row[f"hop{hop}_n_pos"] = npos
176
+ cross_rows.append(rel_row)
177
+ rel_short = rel[:55]
178
+ print(f"{rel_short:<55} | {cells[0]:>14} | {cells[1]:>14} | {cells[2]:>14}")
179
+
180
+ # Per (hop, relation) F1 for top-2 relations
181
+ print()
182
+ print("=" * 96)
183
+ print(f"Per (hop, relation) F1 for the top-2 relations by support")
184
+ print("=" * 96)
185
+ top_relations = relations_sorted[:2]
186
+ f1_rows = []
187
+ print(f"{'relation':<25} | {'hop':>4} | {'n_total':>8} | {'n_pos':>6} | "
188
+ f"{'prec':>6} | {'recall':>6} | {'F1':>6}")
189
+ print("-" * 80)
190
+ for rel in top_relations:
191
+ for hop in [1, 2, 3]:
192
+ key = (hop, rel)
193
+ if key not in by_key_scores:
194
+ continue
195
+ s = np.asarray(by_key_scores[key])
196
+ l = np.asarray(by_key_labels[key])
197
+ if len(l) == 0:
198
+ continue
199
+ n_pos = int(l.sum())
200
+ m = precision_recall_f1(s, l, threshold=threshold)
201
+ f1_rows.append({
202
+ "relation": rel, "hop": hop,
203
+ "n_total": len(l), "n_pos": n_pos,
204
+ "precision": m["precision"], "recall": m["recall"], "f1": m["f1"],
205
+ })
206
+ print(f"{rel[:25]:<25} | {hop:>4} | {len(l):>8} | {n_pos:>6} | "
207
+ f"{m['precision']:>6.4f} | {m['recall']:>6.4f} | {m['f1']:>6.4f}")
208
+ print("=" * 96)
209
+
210
+ # Save JSON
211
+ if args.output_json:
212
+ out = {
213
+ "checkpoint": str(args.checkpoint),
214
+ "mode": args.mode,
215
+ "threshold": threshold,
216
+ "per_hop": hop_rows,
217
+ "hop_x_relation_counts": cross_rows,
218
+ "per_hop_relation_f1_top2": f1_rows,
219
+ }
220
+ out_path = Path(args.output_json)
221
+ out_path.parent.mkdir(parents=True, exist_ok=True)
222
+ with out_path.open("w", encoding="utf-8") as f:
223
+ json.dump(out, f, indent=2)
224
+ logger.info(f"Results written to {out_path}")
225
+
226
+
227
+ if __name__ == "__main__":
228
+ main()