dl4h final project (NetID: yoheis2, paper:bulk rna bert) - #1067
Open
yshibata8513 wants to merge 5 commits into
Open
dl4h final project (NetID: yoheis2, paper:bulk rna bert)#1067yshibata8513 wants to merge 5 commits into
yshibata8513 wants to merge 5 commits into
Conversation
…avoid stale cache
… fast instead of hanging
…loss instead of zero
|
This PR has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contributor: Yohei Shibata (yoheis2@illinois.edu, NetID: yoheis2) — solo submission
Contribution Type: Option 4 — Full Pipeline (Dataset + Task + Model)
Paper: Gelard, M. et al. (2025). BulkRNABert: Cancer prognosis from bulk RNA-seq based language models. PMLR 259. https://proceedings.mlr.press/v259/gelard25a.html
Summary
This PR is the first PyHealth implementation of BulkRNABert (Gelard et al., 2025), submitted as an Option 4 full-pipeline contribution (Dataset + Task + Model + Example). The model was independently re-implemented in PyTorch by referencing the specifications and numerical values from the paper, its published hyperparameter specs, and the reference JAX/Haiku implementation
instadeepai/multiomics-open-research(CC BY-NC-SA 4.0). No source code, config, or YAML from the reference implementation was copied or pasted. Numerical values not published in the paper were identified from publicly available artifacts of the reference implementation and used only as numerical references, not as copied source code, in an independently written implementation.What is novel here:
f1_weightedfrom 0.9436 to 0.9641 (+2.05pt) — a cheap improvement axis that the paper did not test.PyHealth convention compliance: The downstream pipeline fully conforms to
BaseModel/BaseDataset/BaseTask/Trainer. Only the pretraining side uses a custom loop, because long step-based training requires facilities PyHealth'sTrainerdoes not currently expose (see §Design decisions).Files to Review
Core implementation
pyhealth/models/bulk_rna_bert.py—BulkRNABert/BulkRNABertConfig/BulkRNABertClassifier+encode()+ binning helperspyhealth/datasets/tcga_rnaseq_embedding.py+configs/tcga_rnaseq_embedding.yaml—TCGARNASeqEmbeddingDataset(BaseDataset)pyhealth/tasks/tcga_cancer_classification_5cohort.py—TCGACancerClassification5Cohort(BaseTask)+__call__Examples (follow the
{dataset}_{task_name}_{model}.pynaming convention)examples/bulk_rna_bert/tcga_rnaseq_mlm_bulk_rna_bert.py— MLM pretraining CLIexamples/bulk_rna_bert/tcga_rnaseq_extract_embeddings_bulk_rna_bert.py— ckpt →.npyembedding extraction CLIexamples/bulk_rna_bert/tcga_cancer_classification_5cohort_bulk_rna_bert.py— downstream classification +--ablation mode(discrete vs continuous) +--synthetic-demo(for CI / smoke tests)Tests (CPU only, synthetic data only,
tempfile.TemporaryDirectory+tearDowncleanup, 53 cases / ~6.75 s)tests/core/test_bulk_rna_bert.py— 30 cases (model / tokenizer / MLM / Trainer integration / gene_embedding.ptsave→load round-trip)tests/core/test_bulk_rna_bert_downstream.py— 23 cases (encode/ classifier / task__call__/ BaseDataset e2e)Docs
docs/api/models/pyhealth.models.bulk_rna_bert.rstdocs/api/datasets/pyhealth.datasets.tcga_rnaseq_embedding.rstdocs/api/tasks/pyhealth.tasks.tcga_cancer_classification_5cohort.rstdocs/api/{models,datasets,tasks}.rst) updatedEnd-to-end pipeline
Running the following three scripts in order produces pretrain → embedding extraction → downstream classification:
examples/bulk_rna_bert/tcga_rnaseq_mlm_bulk_rna_bert.py:tcga_preprocessed.csv→step_{N}/{params.pt, config.json}examples/bulk_rna_bert/tcga_rnaseq_extract_embeddings_bulk_rna_bert.py: ckpt + CSV →(n_samples, 256)float32.npyexamples/bulk_rna_bert/tcga_cancer_classification_5cohort_bulk_rna_bert.py:.npy+ identifier CSV + mapping CSV → test metricsEach step is independently re-runnable because data is passed via CSV /
.npy/ ckpt directory, never in-memory handoff.Ablation: discrete vs continuous expression mode
examples/bulk_rna_bert/tcga_cancer_classification_5cohort_bulk_rna_bert.py --ablation modecompares the two encodings while keeping the head MLP, split, and seed identical.Setup: TCGA 5-cohort, 11,504 samples, seed=42, stratified 70/10/20, head MLP [256, 128] with SELU, Adam lr=1e-3, 1500 epochs, early stopping disabled, best ckpt selected on validation loss.
At pretraining time, following the paper, the initial weights of gene_embedding during pretraining were initialized from the values saved in the checkpoint of the reference implementation repository.
This PR's downstream setup is encoder-frozen + head-only MLP; IA3 fine-tuning is out of scope (see §Design decisions). Under the same recipe, continuous yields +2.05pt f1_weighted / +2.20pt f1_macro / −16% loss over discrete. This ablation is not evaluated in the paper.
Fairness caveats (not an apples-to-apples comparison with the paper) — treat the numbers above as a same-recipe discrete-vs-continuous comparison, not an absolute comparison against the paper:
Design decisions
Trainer. Long step-based training needs gradient accumulation, step-based checkpoints, and a SIGTERM-safe stop path;Trainer(epochs=...)is too coarse for this. The custom loop is confined toexamples/bulk_rna_bert/tcga_rnaseq_mlm_bulk_rna_bert.py, and theforward()→{"loss": ...}contract is honored on both the pretrain and downstream sides..npy+ head-only MLP. Encoder forward is an O(L²) attention over L = 19,062, which is heavy. Running the encoder once via theextract_embeddingsCLI and caching embeddings to.npylets the head be re-trained across multiple ablations / seeds without re-encoding. On-the-fly encode + head was deliberately not adopted.MultiHeadSelfAttention(Q/K/V/O = He-uniform) + FFNTruncatedNormal(std = 1/sqrt(fan_in))+ zero bias. With PyTorch's defaultnn.MultiheadAttentioninitialization, per-dim std collapses to ≈ 0.01 and produces a representation collapse where all samples yield near-identical embeddings, so downstream cannot separate classes. The custom initialization reproduces an equivalent scale to the reference JAX/Haiku implementation.split_by_patientis not structurally applicable. The self-containedstratified_split_indicesproduces a train/val/test 3-way split (default 0.7 / 0.1 / 0.2).Reproducibility caveats
dropout=0,layer_norm=False(differ from the paper text). Optimizer is AdamW (equivalent to the paper's Adam givenweight_decay=0).gene_embeddinginitial values: only thegene_embeddinglayer (gene-ID embedding, analogous to positional encoding) is initialized from the three tensorsgene_embedding.{embed.weight, proj.weight, proj.bias}inside the reference public ckptparams.joblib(CC BY-NC-SA 4.0), after which this PR's MLM pretraining is run on top. The mechanism is the pretrain CLI's--init-gene-embedding-from <path.pt>flag (inexamples/bulk_rna_bert/tcga_rnaseq_mlm_bulk_rna_bert.py); the internal helper_load_gene_embedding_from_pt()copies only those three keys and explicitly ignores any other keys in the state_dict (attention / FFN / LM-head weights). All other parameters are trained from random initialization. The paper's reported numbers likewise assumegene_embeddingis initialized from pretrained embeddings (gene2vec in the paper's case) before MLM pretraining, so the "initializegene_embeddingfrom pretrained values" precondition is shared between the paper and this PR. The ckpt file itself is not redistributed by this PR — users fetch it from the reference repository.gene_embeddingtraining from a gene2vec corpus / redistribution of pretrained weights.Tests
.venv/bin/python -m pytest tests/core/test_bulk_rna_bert.py \ tests/core/test_bulk_rna_bert_downstream.py -q # 53 passed in 6.75s (30 pretrain + 23 downstream, all CPU, synthetic only)Coverage:
pretrain (forward/backward/mask ratio/binning), state save→load, round-trip, downstream (encode/classifier/stratified split/Trainer smoke), task & dataset e2e (
__call__edge cases / merged CSV / label ordering).All tests use
tempfile.TemporaryDirectory(), no real data / network, 2–5 patients per fixture.