A note on the PDR2 personalization loss: paper text vs. released code
Author: Maple — independent reproduction work Date: 2026-05-22 Status: Finding for discussion; not a critique of published results Subject: Oğuztüzün et al. 2026, “Learning to take it personally” — Journal of Biomedical Informatics
TL;DR
The paper’s §“Patient-Specific Loss function” presents a loss formula that, when implemented exactly as written, is mathematically unbounded — the optimizer drives it to −∞ via weight saturation, producing a degenerate criterion that destroys downstream ranking quality. The released code at github.com/ceragoguztuzun/PDR-II implements a structurally different version of the loss that includes tanh(·) and F.normalize(·, p=2) operations not mentioned in the paper text. We verified empirically that the released code’s additional structure is load-bearing: without it, the loss has no fixed point. The release losses.py should be treated as the operational specification; the paper text is best read as a conceptual sketch and not as a literally implementable equation.
This note documents the discrepancy, the experiment that demonstrated the released code’s nonlinearities are mathematically necessary, and explicit scope on what we did not test (the paper’s reported AUPRC improvements on UK Biobank cohorts — our single test patient lacked the PRS density needed for a fair re-evaluation).
1. The discrepancy
What the paper text describes
From Oğuztüzün et al. 2026, §“Patient-Specific Loss function” (DOI: 10.1016/j.jbi.2026.105039):
where is the model’s per-triple score, are learnable scalars, and are patient features.
Followed by an explanatory paragraph (which we quote in full because the sign rationale is significant):
“When minimizing the patient-specific loss function, the key question is: ‘What behavior do we want to encourage in the model?’ We want to (i) encourage correct drug predictions (minimize prediction error ) and (ii) encourage alignment between recommendations and patient biology. […] When a drug entity is being evaluated for a patient with a high PRS for a disease that the drug treats, or when it targets proteins with abnormal expression levels in the patient, the terms and will have large positive values. Subtracting them reduces the overall loss (). This rewards the model for finding drugs that match the patient’s specific biological profile.”
The paper text therefore specifies:
- Minus signs in front of
- Per-triple multiplication inside the sums
- No explicit nonlinearity or weight-normalization step
What the released code implements
Source: reasoning/script/losses.py, lines 37-87, PersonalizedRepurposingLoss.
The structural form of the released forward():
def forward(self, pos_scores, neg_scores, prs_values, expr_values):
base_loss = StandardKGLoss()(pos_scores, neg_scores)
# PRS impact
if self.disease_weights is not None and len(prs_values) > 0:
disease_weights_norm = F.normalize(self.disease_weights, p=2, dim=0)
prs_impact = torch.sum(disease_weights_norm * prs_values)
prs_term = torch.tanh(prs_impact)
# Expression impact
if self.protein_weights is not None and len(expr_values) > 0:
protein_weights_norm = F.normalize(self.protein_weights, p=2, dim=0)
expr_impact = torch.sum(protein_weights_norm * expr_values)
expr_term = torch.tanh(expr_impact)
total_loss = (
base_loss +
self.lambda1 * prs_term +
self.lambda2 * expr_term
)
return total_lossFour observable differences from the paper text:
| Element | Paper text | Released code |
|---|---|---|
| Sign on λ terms | − (minus) | + (plus) |
s_pred coupling | Multiplied per-triple | Not present (per-patient scalar only) |
| Output transform | None | torch.tanh(·) |
| Weight normalization | None | F.normalize(·, p=2, dim=0) (unit L2 norm) |
These are the four points the literal reader will trip over.
2. Why the paper text formula is not literally implementable
Consider the released code with all four “differences” reverted to the paper text (+ → −, tanh removed, F.normalize removed, multiply each term by per-batch mean(pos_scores)). The PRS term becomes:
This is a bilinear form in and the (fixed) patient inputs , multiplied by a model-dependent scalar. The expression term is structurally identical. The total loss:
For any trained foundation model where on positive triples (i.e., almost always), and any patient with at least one non-zero or , the loss is unbounded below:
- The gradient of with respect to is , a non-zero constant in .
- The optimizer can drive to in the sign direction of to make the loss arbitrarily negative.
- There is no fixed point. Nothing in the formula stops the walk.
The released code’s tanh(·) bounds each personalization term to , and F.normalize(·, p=2, dim=0) constrains each weight vector to unit L2 norm. Together these convert an unbounded objective into a bounded one with finite extrema. They are not decorative.
3. Experimental confirmation
We added two alternative loss classes to a local fork of the codebase to test whether the discrepancies between text and code matter empirically:
PaperTextPersonalizedRepurposingLoss— strict paper-text formula: minus signs, per-triplemean(pos_scores)coupling, notanh, noF.normalize. Referenceref_j= 0.5 (TPM-percentile midpoint).MinimalPatchPersonalizedRepurposingLoss— keeps released code’stanh+F.normalizebut applies the paper-text sign ands_predcorrections.
Fine-tune setup (matches the published reproduction recipe):
- Foundation model: ULTRA backbone pretrained on PrimeKG + CIViC + PharmGKB knowledge graph composition
- Fine-tune script:
script/fine_tune_maple.py(Maple’s wrapper aroundtraining.py:fine_tune_patient_specific) - 20 epochs, batch_per_epoch=500, batch_size=128 (AdamW lr=2e-3, weight_decay=1e-5 — values from
training.py:fine_tune_patient_specific) - Single patient (oncology), filtered MRR evaluated on 138 patient-outbound edges in valid+test splits
- λ swept at {0.01, 0.1}
Wall-clock and loss-curve evidence
| Variant | λ | Final epoch | Final loss | Wall-clock | Early-stopped? |
|---|---|---|---|---|---|
paper_code (released) | 0.01 | 20 | ≈ −0.01 (bounded) | 25 min | n/a in writeup |
paper_text | 0.01 | 20 | −666.18 (unbounded) | 67 min | no (every epoch is “new best”) |
paper_text | 0.1 | 20 | similar to above | 68 min | no |
minimal_patch | 0.01 | 8 (early-stopped) | ≈ −0.005 (saturated) | 28 min | yes |
minimal_patch | 0.1 | 8 (early-stopped) | ≈ −0.005 | 28 min | yes |
paper_text loss across epochs (from stdout.log):
Epoch 5 - Loss: -186.18 → New best model
Epoch 10 - Loss: -352.58 → New best model
Epoch 15 - Loss: -532.89 → New best model
Epoch 19 - Loss: -666.18 → New best model
Every epoch produces a new best because the loss has no floor. If we ran more epochs, it would continue to decrease without limit.
Criterion weight signatures
For paper_text after 20 epochs, the top-5 protein weights by absolute magnitude:
MUC1: +4.067, EREG: +4.064, MYC: +4.061, NTRK2: +4.055, EZH2: +4.054
All five weights converge to exactly the AdamW saturation distance: 20 epochs × 100 batches × lr=2e-3 = 4.0. AdamW normalizes per-parameter gradients to roughly unit RMS, so each weight takes a step of ~lr per update regardless of the gradient’s true magnitude. The criterion did not learn protein-specific biology; the optimizer walked every weight the maximum distance allowed in whichever sign the gradient happened to point.
For minimal_patch and paper_code, criterion weights stay at random-init magnitudes (±0.02–0.03), as expected when tanh saturates the loss into the noise-scale regime and early-stopping fires.
4. Downstream ranking impact (scope-limited — see §6)
Filtered MRR on the test patient’s 138 outbound edges:
| Loss variant | λ | Baseline MRR | Personalized MRR | Δ % |
|---|---|---|---|---|
paper_code (released) | 0.01 | 0.005152 | 0.005131 | −0.4% |
paper_text (strict) | 0.01 | 0.005152 | 0.000391 | −92.4% |
paper_text (strict) | 0.1 | 0.005152 | 0.000288 | −94.4% |
minimal_patch | 0.01 | 0.005152 | 0.005215 | +1.2% |
minimal_patch | 0.1 | 0.005152 | 0.005136 | −0.3% |
The released code’s tanh + F.normalize keep the loss bounded; the criterion weights stay at modest magnitude; the inference-time score adjustment in apply_personalization is correspondingly small; ranking quality is preserved (small Δ %). When the bounded loss is removed (paper_text), the criterion weights saturate at +4.07; the inference-time adjustment per drug becomes large enough to overwhelm the foundation model’s ranking signal; every drug-related predicate loses 80-98% of MRR.
This is consistent with the mathematical analysis in §2: the unbounded loss produces a degenerate criterion that breaks downstream scoring.
5. Interpretation
Read together, §2’s mathematical analysis and §3’s empirical results both support a single interpretation: the released losses.py is the operational specification, and the tanh + F.normalize operations are intentional and load-bearing. Without them, the loss has no fixed point and the optimizer produces a saturated, degenerate criterion. With them, the loss is bounded, the criterion weights stay finite, and downstream personalization works as designed.
The paper text, taken as a literally implementable equation, omits the mathematical structure necessary to make the loss well-posed. There are several plausible reasons this might be — the text may be intended as a conceptual sketch with implementation details abstracted away, or the authors may have considered tanh and F.normalize standard enough not to need explicit mention. We do not have insight into the authors’ editorial intent.
For practitioners trying to reproduce or adapt PDR2: use the released losses.py as-is. Do not attempt to reconstruct the loss from the paper text equation alone. The signs, the absence of tanh, and the absence of F.normalize in the text are not invitations to implement those choices literally — they are a presentation simplification of a more elaborate operational form.
6. Scope and caveats
The downstream MRR numbers in §4 should be read with three caveats firmly in mind:
-
Single patient. We tested one oncology patient. Statistical generalization is not possible from N=1.
-
Severely sparse personalization input. Our test patient had zero non-zero polygenic risk scores (the test cohort used a synthetic-zero PRS pickle while genotype-derived PRS was unavailable) and only 84 protein biomarkers (vs. the paper’s reported 1461). With for all , the entire term contributes zero gradient signal regardless of loss formulation. Our experiment was effectively a biomarker-only test on ~6% of the input density the paper used. The “null lift” we observed on
paper_codeandminimal_patchcannot be generalized to the paper’s reported AUPRC improvements (1.3×-5.4×), which were computed on UK Biobank cohorts with full PRS + biomarker inputs. The §4 results should be read as descriptive of our specific input, not as a re-evaluation of the published method. -
§3’s mathematical findings are independent of patient sparsity. The unboundedness of the paper-text loss formula is a property of the formula, not of the input. Section 5’s interpretation — that the released code’s nonlinearities are load-bearing — rests on the math in §2 plus the loss-curve evidence in §3, neither of which depends on the patient’s input density.
What this note does not claim:
- That the paper’s reported numerical results are incorrect — we did not attempt to reproduce on UK Biobank cohorts.
- That the released
PersonalizedRepurposingLossis buggy — it works as intended on the published evaluation. - That the personalization approach does not work for densely-featured patients — our test patient was not the right subject for that question.
What this note does claim:
- The paper text and released code differ in four specific, structural ways.
- The differences are not cosmetic — they change the boundedness of the loss.
- The released code’s additional structure is mathematically necessary; the paper text equation cannot be implemented as written without producing a pathological optimizer.
- The released
losses.pyshould be treated as the canonical specification.
7. Reproducibility
Container image with the two alt-loss classes baked in: a private container registry image, tag 2f1e268-altloss (Vertex AI custom training).
Fine-tune + eval automation: backend/scripts/alt_loss_2026_05_22/ in the Maple repo (specs and polling scripts).
Job specs are direct gcloud beta ai custom-jobs create --config=… invocations. Total spend for the 4 fine-tunes + 4 evals: ~$10 on A100-80GB/A100-40GB.
The two alt-loss classes (verbatim) live in this fork’s lib/pdr2/code/script/losses.py alongside the unmodified PersonalizedRepurposingLoss.
References
- Paper: Oğuztüzün, Ç., Gao, Z., Li, J., Qi, X., Koyutürk, M., & Xu, R. (2026). Learning to take it personally: Precision drug repurposing through patient-specific loss on knowledge graphs using Biobank data. Journal of Biomedical Informatics. DOI: 10.1016/j.jbi.2026.105039
- Released code:
github.com/ceragoguztuzun/PDR-II— specificallyreasoning/script/losses.py(PersonalizedRepurposingLoss, lines 37-87) andreasoning/script/training.py(fine_tune_patient_specific, line 1193 onward). - Backbone: Galkin, M. et al. (2024). Towards Foundation Models for Knowledge Graph Reasoning (ULTRA). ICLR. arXiv:2310.04562.
- Inner loop: Zhu, Z. et al. (2021). Neural Bellman-Ford Networks (NBFNet). NeurIPS. arXiv:2106.06935.
Corresponding-author contact details for the PDR-II paper are in the released README and in the paper itself; they are deliberately not transcribed here, because this page is published.