faisalAI27 commited on
Commit
a5ebf39
·
1 Parent(s): a59e3b5

done with preprocessing

Browse files
training/01_prepare_clinvar_dataset.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
training/README.md CHANGED
@@ -6,8 +6,8 @@ Training should be run in Google Colab or another dedicated GPU notebook environ
6
 
7
  ## Contents
8
 
9
- - `01_prepare_clinvar_dataset.ipynb`: Colab notebook for downloading ClinVar GRCh38 VCF data and preparing binary SNV/small-indel CSV splits.
10
- - `colab_dnabert2_clinvar_finetune.ipynb`: notebook entry point for Colab.
11
  - `requirements-colab.txt`: Python packages for the notebook.
12
  - `scripts/prepare_clinvar_dataset.py`: converts ClinVar GRCh38 VCF records into sequence classification examples.
13
  - `scripts/train_dnabert2_classifier.py`: fine-tunes DNABERT-2 with Hugging Face Transformers.
@@ -20,17 +20,18 @@ Training should be run in Google Colab or another dedicated GPU notebook environ
20
  Use GRCh38 consistently:
21
 
22
  - ClinVar GRCh38 VCF: `https://ftp.ncbi.nlm.nih.gov/pub/clinvar/vcf_GRCh38/clinvar.vcf.gz`
23
- - GRCh38 reference FASTA: provide a local or Google Drive path in Colab.
 
24
 
25
- The preparation script validates reference alleles against the FASTA and skips records that do not match.
26
 
27
  ## Colab Flow
28
 
29
- 1. Open `colab_dnabert2_clinvar_finetune.ipynb` in Google Colab.
30
- 2. Install `requirements-colab.txt`.
31
- 3. Download ClinVar GRCh38 VCF.
32
- 4. Mount Google Drive or upload a GRCh38 FASTA.
33
- 5. Run dataset preparation.
34
  6. Run DNABERT-2 fine-tuning.
35
  7. Export the saved model directory.
36
 
 
6
 
7
  ## Contents
8
 
9
+ - `01_prepare_clinvar_dataset.ipynb`: Colab notebook for downloading ClinVar GRCh38 VCF data and preparing binary SNV/small-indel CSV splits with sequence columns.
10
+ - `colab_dnabert2_clinvar_finetune.ipynb`: Colab notebook for fine-tuning DNABERT-2 from `train_with_sequences.csv`, `val_with_sequences.csv`, and `test_with_sequences.csv`.
11
  - `requirements-colab.txt`: Python packages for the notebook.
12
  - `scripts/prepare_clinvar_dataset.py`: converts ClinVar GRCh38 VCF records into sequence classification examples.
13
  - `scripts/train_dnabert2_classifier.py`: fine-tunes DNABERT-2 with Hugging Face Transformers.
 
20
  Use GRCh38 consistently:
21
 
22
  - ClinVar GRCh38 VCF: `https://ftp.ncbi.nlm.nih.gov/pub/clinvar/vcf_GRCh38/clinvar.vcf.gz`
23
+ - Sequence extraction uses the UCSC hg38 API by default.
24
+ - Optional GRCh38 reference FASTA: provide a local or Google Drive path in Colab if using local FASTA sequence extraction.
25
 
26
+ The fine-tuning notebook expects the sequence CSV files from the preparation notebook.
27
 
28
  ## Colab Flow
29
 
30
+ 1. Open `01_prepare_clinvar_dataset.ipynb` in Google Colab.
31
+ 2. Run dataset preparation and sequence extraction.
32
+ 3. Save or download `train_with_sequences.csv`, `val_with_sequences.csv`, and `test_with_sequences.csv`.
33
+ 4. Upload those CSV files into `training/csv_files/` or `data/processed/`.
34
+ 5. Open `colab_dnabert2_clinvar_finetune.ipynb`.
35
  6. Run DNABERT-2 fine-tuning.
36
  7. Export the saved model directory.
37
 
training/colab_dnabert2_clinvar_finetune.ipynb CHANGED
@@ -4,11 +4,11 @@
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
- "# Variant Risk Explainer: ClinVar + DNABERT-2 Fine-tuning\n",
8
  "\n",
9
- "Research and education only. This notebook does not produce a clinically validated diagnostic model.\n",
10
  "\n",
11
- "Use GRCh38 for both ClinVar coordinates and reference FASTA sequence extraction."
12
  ]
13
  },
14
  {
@@ -17,7 +17,7 @@
17
  "source": [
18
  "## 1. Runtime\n",
19
  "\n",
20
- "In Colab, choose `Runtime -> Change runtime type -> GPU` before running the training cells."
21
  ]
22
  },
23
  {
@@ -34,9 +34,9 @@
34
  "cell_type": "markdown",
35
  "metadata": {},
36
  "source": [
37
- "## 2. Install dependencies\n",
38
  "\n",
39
- "Upload or clone this repository into `/content/variant-risk-explainer`, then install the Colab requirements."
40
  ]
41
  },
42
  {
@@ -45,15 +45,43 @@
45
  "metadata": {},
46
  "outputs": [],
47
  "source": [
48
- "%cd /content/variant-risk-explainer\n",
49
- "!pip install -q -r training/requirements-colab.txt"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  ]
51
  },
52
  {
53
  "cell_type": "markdown",
54
  "metadata": {},
55
  "source": [
56
- "## 3. Download ClinVar GRCh38 VCF"
 
 
57
  ]
58
  },
59
  {
@@ -62,18 +90,26 @@
62
  "metadata": {},
63
  "outputs": [],
64
  "source": [
65
- "!mkdir -p training/data\n",
66
- "!wget -q -O training/data/clinvar_grch38.vcf.gz https://ftp.ncbi.nlm.nih.gov/pub/clinvar/vcf_GRCh38/clinvar.vcf.gz\n",
67
- "!ls -lh training/data/clinvar_grch38.vcf.gz"
68
  ]
69
  },
70
  {
71
  "cell_type": "markdown",
72
  "metadata": {},
73
  "source": [
74
- "## 4. Provide GRCh38 FASTA\n",
75
  "\n",
76
- "Mount Google Drive or upload a GRCh38 FASTA. The FASTA must match GRCh38 and should be indexed. If the `.fai` index is missing, the next cell creates it."
 
 
 
 
 
 
 
 
 
 
77
  ]
78
  },
79
  {
@@ -82,11 +118,40 @@
82
  "metadata": {},
83
  "outputs": [],
84
  "source": [
85
- "from google.colab import drive\n",
86
- "drive.mount('/content/drive')\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  "\n",
88
- "REFERENCE_FASTA = '/content/drive/MyDrive/genomics/GRCh38.primary_assembly.genome.fa.gz'\n",
89
- "print(REFERENCE_FASTA)"
90
  ]
91
  },
92
  {
@@ -95,18 +160,51 @@
95
  "metadata": {},
96
  "outputs": [],
97
  "source": [
98
- "import os\n",
99
- "if not os.path.exists(REFERENCE_FASTA + '.fai'):\n",
100
- " import pysam\n",
101
- " pysam.faidx(REFERENCE_FASTA)\n",
102
- "print('FASTA index ready')"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  ]
104
  },
105
  {
106
  "cell_type": "markdown",
107
  "metadata": {},
108
  "source": [
109
- "Optional: store the FASTA path in an environment variable for scripts or later notebook cells."
 
 
110
  ]
111
  },
112
  {
@@ -115,17 +213,26 @@
115
  "metadata": {},
116
  "outputs": [],
117
  "source": [
118
- "import os\n",
119
- "os.environ['REFERENCE_FASTA_PATH'] = REFERENCE_FASTA"
 
 
 
 
 
 
 
 
 
120
  ]
121
  },
122
  {
123
  "cell_type": "markdown",
124
  "metadata": {},
125
  "source": [
126
- "## 5. Prepare ClinVar sequence examples\n",
127
  "\n",
128
- "For a quick smoke test, keep `--max-records 2000`. For a full run, remove it."
129
  ]
130
  },
131
  {
@@ -134,21 +241,24 @@
134
  "metadata": {},
135
  "outputs": [],
136
  "source": [
137
- "!python training/scripts/prepare_clinvar_dataset.py \\\n",
138
- " --clinvar-vcf training/data/clinvar_grch38.vcf.gz \\\n",
139
- " --reference-fasta \"$REFERENCE_FASTA\" \\\n",
140
- " --output-jsonl training/data/clinvar_grch38_dnabert2.jsonl \\\n",
141
- " --window-size 251 \\\n",
142
- " --max-records 2000"
 
 
 
 
 
143
  ]
144
  },
145
  {
146
  "cell_type": "markdown",
147
  "metadata": {},
148
  "source": [
149
- "## 6. Fine-tune DNABERT-2\n",
150
- "\n",
151
- "Use a small smoke-test run first. Increase epochs, remove `--max-records` during preparation, and tune batch size based on GPU memory for real experiments."
152
  ]
153
  },
154
  {
@@ -157,21 +267,23 @@
157
  "metadata": {},
158
  "outputs": [],
159
  "source": [
160
- "!python training/scripts/train_dnabert2_classifier.py \\\n",
161
- " --dataset-jsonl training/data/clinvar_grch38_dnabert2.jsonl \\\n",
162
- " --output-dir training/output/dnabert2-clinvar-grch38 \\\n",
163
- " --epochs 1 \\\n",
164
- " --batch-size 4 \\\n",
165
- " --max-length 256"
 
 
166
  ]
167
  },
168
  {
169
  "cell_type": "markdown",
170
  "metadata": {},
171
  "source": [
172
- "## 7. Export model artifact\n",
173
  "\n",
174
- "The backend expects a Hugging Face model directory containing tokenizer and model files. Copy or zip `training/output/dnabert2-clinvar-grch38/final_model`."
175
  ]
176
  },
177
  {
@@ -180,8 +292,15 @@
180
  "metadata": {},
181
  "outputs": [],
182
  "source": [
183
- "!zip -r dnabert2-clinvar-grch38-final-model.zip training/output/dnabert2-clinvar-grch38/final_model\n",
184
- "!ls -lh dnabert2-clinvar-grch38-final-model.zip"
 
 
 
 
 
 
 
185
  ]
186
  }
187
  ],
@@ -195,7 +314,8 @@
195
  "name": "python3"
196
  },
197
  "language_info": {
198
- "name": "python"
 
199
  }
200
  },
201
  "nbformat": 4,
 
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
+ "# Variant Risk Explainer: DNABERT-2 Fine-tuning\n",
8
  "\n",
9
+ "This notebook trains from the sequence CSV files produced by `01_prepare_clinvar_dataset.ipynb`.\n",
10
  "\n",
11
+ "Research and education only. This does not produce a clinically validated diagnostic model."
12
  ]
13
  },
14
  {
 
17
  "source": [
18
  "## 1. Runtime\n",
19
  "\n",
20
+ "In Colab, choose `Runtime -> Change runtime type -> GPU` before running training."
21
  ]
22
  },
23
  {
 
34
  "cell_type": "markdown",
35
  "metadata": {},
36
  "source": [
37
+ "## 2. Find Repository Root\n",
38
  "\n",
39
+ "Clone or upload the full repository first. The notebook will search `/content` for the training script, so the GitHub repo folder name can be different."
40
  ]
41
  },
42
  {
 
45
  "metadata": {},
46
  "outputs": [],
47
  "source": [
48
+ "from pathlib import Path\n",
49
+ "import sys\n",
50
+ "\n",
51
+ "\n",
52
+ "def find_project_root() -> Path:\n",
53
+ " candidates = [Path.cwd(), Path('/content/variant-risk-explainer')]\n",
54
+ " for candidate in candidates:\n",
55
+ " script = candidate / 'training' / 'scripts' / 'train_dnabert2_classifier.py'\n",
56
+ " if script.exists():\n",
57
+ " return candidate\n",
58
+ "\n",
59
+ " content_root = Path('/content')\n",
60
+ " if content_root.exists():\n",
61
+ " matches = list(content_root.glob('*/training/scripts/train_dnabert2_classifier.py'))\n",
62
+ " if matches:\n",
63
+ " return matches[0].parents[2]\n",
64
+ "\n",
65
+ " raise FileNotFoundError(\n",
66
+ " 'Could not find training/scripts/train_dnabert2_classifier.py. Clone or upload the full repository first.'\n",
67
+ " )\n",
68
+ "\n",
69
+ "\n",
70
+ "PROJECT_ROOT = find_project_root()\n",
71
+ "if str(PROJECT_ROOT) not in sys.path:\n",
72
+ " sys.path.insert(0, str(PROJECT_ROOT))\n",
73
+ "\n",
74
+ "%cd {PROJECT_ROOT}\n",
75
+ "print(f'Project root: {PROJECT_ROOT}')"
76
  ]
77
  },
78
  {
79
  "cell_type": "markdown",
80
  "metadata": {},
81
  "source": [
82
+ "## 3. Install Dependencies\n",
83
+ "\n",
84
+ "This installs the Colab training dependencies. If Colab asks you to restart the runtime after installation, restart and run the notebook again from the top."
85
  ]
86
  },
87
  {
 
90
  "metadata": {},
91
  "outputs": [],
92
  "source": [
93
+ "!pip install -q -r training/requirements-colab.txt"
 
 
94
  ]
95
  },
96
  {
97
  "cell_type": "markdown",
98
  "metadata": {},
99
  "source": [
100
+ "## 4. Locate Sequence CSV Files\n",
101
  "\n",
102
+ "Upload your CSVs into one of these locations before running this cell:\n",
103
+ "\n",
104
+ "- `training/csv_files/`\n",
105
+ "- `data/processed/`\n",
106
+ "- `training/data/processed/`\n",
107
+ "\n",
108
+ "Required file names:\n",
109
+ "\n",
110
+ "- `train_with_sequences.csv`\n",
111
+ "- `val_with_sequences.csv`\n",
112
+ "- `test_with_sequences.csv`"
113
  ]
114
  },
115
  {
 
118
  "metadata": {},
119
  "outputs": [],
120
  "source": [
121
+ "import pandas as pd\n",
122
+ "\n",
123
+ "CSV_SEARCH_DIRS = [\n",
124
+ " PROJECT_ROOT / 'training' / 'csv_files',\n",
125
+ " PROJECT_ROOT / 'data' / 'processed',\n",
126
+ " PROJECT_ROOT / 'training' / 'data' / 'processed',\n",
127
+ "]\n",
128
+ "\n",
129
+ "\n",
130
+ "def find_csv(filename: str) -> Path:\n",
131
+ " for directory in CSV_SEARCH_DIRS:\n",
132
+ " candidate = directory / filename\n",
133
+ " if candidate.exists():\n",
134
+ " return candidate\n",
135
+ " searched = '\\n'.join(str(directory / filename) for directory in CSV_SEARCH_DIRS)\n",
136
+ " raise FileNotFoundError(f'Could not find {filename}. Searched:\\n{searched}')\n",
137
+ "\n",
138
+ "\n",
139
+ "TRAIN_CSV = find_csv('train_with_sequences.csv')\n",
140
+ "VAL_CSV = find_csv('val_with_sequences.csv')\n",
141
+ "TEST_CSV = find_csv('test_with_sequences.csv')\n",
142
+ "\n",
143
+ "print(f'Train CSV: {TRAIN_CSV}')\n",
144
+ "print(f'Val CSV: {VAL_CSV}')\n",
145
+ "print(f'Test CSV: {TEST_CSV}')"
146
+ ]
147
+ },
148
+ {
149
+ "cell_type": "markdown",
150
+ "metadata": {},
151
+ "source": [
152
+ "## 5. Check CSV Quality\n",
153
  "\n",
154
+ "This checks required columns, sequence lengths, bad sequence characters, and labels. The training script will also defensively drop uncertain, conflicting, risk, association, drug-response, protective, and not-provided CLNSIG rows."
 
155
  ]
156
  },
157
  {
 
160
  "metadata": {},
161
  "outputs": [],
162
  "source": [
163
+ "from training.utils.label_utils import assign_binary_label\n",
164
+ "\n",
165
+ "REQUIRED_COLUMNS = {'sequence', 'label'}\n",
166
+ "\n",
167
+ "\n",
168
+ "def inspect_split(name: str, path: Path) -> pd.DataFrame:\n",
169
+ " df = pd.read_csv(path)\n",
170
+ " missing = REQUIRED_COLUMNS - set(df.columns)\n",
171
+ " if missing:\n",
172
+ " raise ValueError(f'{name} is missing columns: {sorted(missing)}')\n",
173
+ "\n",
174
+ " bad_sequence_chars = int((~df['sequence'].astype(str).str.upper().str.match(r'^[ACGTN]+$')).sum())\n",
175
+ " cleanable_rows = None\n",
176
+ " if 'CLNSIG' in df.columns:\n",
177
+ " clean_labels = df['CLNSIG'].apply(assign_binary_label)\n",
178
+ " cleanable_rows = int(clean_labels.notna().sum())\n",
179
+ "\n",
180
+ " print(f'\\n{name}')\n",
181
+ " print(f'rows: {len(df):,}')\n",
182
+ " print(f'label counts: {df[\"label\"].value_counts(dropna=False).sort_index().to_dict()}')\n",
183
+ " print(\n",
184
+ " 'sequence length min/mean/max:',\n",
185
+ " int(df['sequence'].astype(str).str.len().min()),\n",
186
+ " round(float(df['sequence'].astype(str).str.len().mean()), 2),\n",
187
+ " int(df['sequence'].astype(str).str.len().max()),\n",
188
+ " )\n",
189
+ " print(f'bad sequence rows: {bad_sequence_chars:,}')\n",
190
+ " if cleanable_rows is not None:\n",
191
+ " print(f'rows after defensive CLNSIG cleaning: {cleanable_rows:,}')\n",
192
+ " display(df.head(3))\n",
193
+ " return df\n",
194
+ "\n",
195
+ "\n",
196
+ "train_preview = inspect_split('train', TRAIN_CSV)\n",
197
+ "val_preview = inspect_split('val', VAL_CSV)\n",
198
+ "test_preview = inspect_split('test', TEST_CSV)"
199
  ]
200
  },
201
  {
202
  "cell_type": "markdown",
203
  "metadata": {},
204
  "source": [
205
+ "## 6. Training Settings\n",
206
+ "\n",
207
+ "Use conservative settings first. Increase epochs or batch size later if the run is stable. For small sample CSVs, expect overfitting; this is only a research demo."
208
  ]
209
  },
210
  {
 
213
  "metadata": {},
214
  "outputs": [],
215
  "source": [
216
+ "MODEL_NAME = 'zhihan1996/DNABERT-2-117M'\n",
217
+ "OUTPUT_DIR = PROJECT_ROOT / 'training' / 'output' / 'dnabert2-clinvar-grch38'\n",
218
+ "\n",
219
+ "MAX_LENGTH = 512\n",
220
+ "EPOCHS = 1\n",
221
+ "BATCH_SIZE = 4\n",
222
+ "LEARNING_RATE = 2e-5\n",
223
+ "MIN_SEQUENCE_LENGTH = 200\n",
224
+ "\n",
225
+ "print(f'Model: {MODEL_NAME}')\n",
226
+ "print(f'Output dir: {OUTPUT_DIR}')"
227
  ]
228
  },
229
  {
230
  "cell_type": "markdown",
231
  "metadata": {},
232
  "source": [
233
+ "## 7. Fine-tune DNABERT-2\n",
234
  "\n",
235
+ "This calls the repository training script using the CSV files. The script saves the best checkpoint and final model under `training/output/dnabert2-clinvar-grch38/final_model`."
236
  ]
237
  },
238
  {
 
241
  "metadata": {},
242
  "outputs": [],
243
  "source": [
244
+ "!python training/scripts/train_dnabert2_classifier.py \\\n",
245
+ " --train-csv \"{TRAIN_CSV}\" \\\n",
246
+ " --val-csv \"{VAL_CSV}\" \\\n",
247
+ " --test-csv \"{TEST_CSV}\" \\\n",
248
+ " --output-dir \"{OUTPUT_DIR}\" \\\n",
249
+ " --model-name \"{MODEL_NAME}\" \\\n",
250
+ " --max-length {MAX_LENGTH} \\\n",
251
+ " --min-sequence-length {MIN_SEQUENCE_LENGTH} \\\n",
252
+ " --epochs {EPOCHS} \\\n",
253
+ " --batch-size {BATCH_SIZE} \\\n",
254
+ " --learning-rate {LEARNING_RATE}"
255
  ]
256
  },
257
  {
258
  "cell_type": "markdown",
259
  "metadata": {},
260
  "source": [
261
+ "## 8. Inspect Saved Model Metadata"
 
 
262
  ]
263
  },
264
  {
 
267
  "metadata": {},
268
  "outputs": [],
269
  "source": [
270
+ "import json\n",
271
+ "\n",
272
+ "metadata_path = OUTPUT_DIR / 'final_model' / 'variant_risk_metadata.json'\n",
273
+ "if metadata_path.exists():\n",
274
+ " metadata = json.loads(metadata_path.read_text())\n",
275
+ " print(json.dumps(metadata, indent=2))\n",
276
+ "else:\n",
277
+ " print(f'Metadata not found yet: {metadata_path}')"
278
  ]
279
  },
280
  {
281
  "cell_type": "markdown",
282
  "metadata": {},
283
  "source": [
284
+ "## 9. Export Model Artifact\n",
285
  "\n",
286
+ "Zip the final model directory so you can download it from Colab and use it with the FastAPI backend later."
287
  ]
288
  },
289
  {
 
292
  "metadata": {},
293
  "outputs": [],
294
  "source": [
295
+ "ZIP_PATH = PROJECT_ROOT / 'dnabert2-clinvar-grch38-final-model.zip'\n",
296
+ "!cd \"{PROJECT_ROOT}\" && zip -qr \"{ZIP_PATH}\" training/output/dnabert2-clinvar-grch38/final_model\n",
297
+ "print(f'Wrote {ZIP_PATH}')\n",
298
+ "\n",
299
+ "try:\n",
300
+ " from google.colab import files\n",
301
+ " files.download(str(ZIP_PATH))\n",
302
+ "except Exception:\n",
303
+ " print('Download helper is only available inside Google Colab.')"
304
  ]
305
  }
306
  ],
 
314
  "name": "python3"
315
  },
316
  "language_info": {
317
+ "name": "python",
318
+ "version": "3.x"
319
  }
320
  },
321
  "nbformat": 4,
training/csv_files/README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sequence CSV Files
2
+
3
+ Place the sequence-enriched ClinVar split files here for Colab training:
4
+
5
+ - `train_with_sequences.csv`
6
+ - `val_with_sequences.csv`
7
+ - `test_with_sequences.csv`
8
+
9
+ These files are generated by `training/01_prepare_clinvar_dataset.ipynb`.
10
+
11
+ Large generated CSVs should usually be stored outside Git unless you intentionally want to version the sample dataset.
training/csv_files/test_with_sequences.csv ADDED
The diff for this file is too large to render. See raw diff
 
training/csv_files/train_with_sequences.csv ADDED
The diff for this file is too large to render. See raw diff
 
training/csv_files/val_with_sequences.csv ADDED
The diff for this file is too large to render. See raw diff
 
training/requirements-colab.txt CHANGED
@@ -1,6 +1,7 @@
1
  accelerate>=0.30.0
2
  biopython>=1.83
3
  datasets>=2.19.0
 
4
  evaluate>=0.4.2
5
  numpy>=1.26.0
6
  pandas>=2.2.0
 
1
  accelerate>=0.30.0
2
  biopython>=1.83
3
  datasets>=2.19.0
4
+ einops>=0.8.0
5
  evaluate>=0.4.2
6
  numpy>=1.26.0
7
  pandas>=2.2.0
training/scripts/train_dnabert2_classifier.py CHANGED
@@ -7,10 +7,17 @@ Run this script in Google Colab or another GPU notebook environment.
7
  from __future__ import annotations
8
 
9
  import argparse
 
10
  import json
 
11
  from pathlib import Path
12
 
 
 
 
 
13
  import numpy as np
 
14
  from datasets import Dataset
15
  from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
16
  from transformers import (
@@ -21,26 +28,36 @@ from transformers import (
21
  TrainingArguments,
22
  )
23
 
 
 
24
 
25
  ID_TO_LABEL = {
26
- 0: "likely_benign",
27
- 1: "likely_pathogenic",
28
  }
29
  LABEL_TO_ID = {label: idx for idx, label in ID_TO_LABEL.items()}
 
30
 
31
 
32
  def parse_args() -> argparse.Namespace:
33
  parser = argparse.ArgumentParser(description=__doc__)
34
- parser.add_argument("--dataset-jsonl", required=True, help="Prepared JSONL from prepare_clinvar_dataset.py.")
 
 
 
35
  parser.add_argument("--output-dir", required=True, help="Directory for saved model artifacts.")
36
  parser.add_argument("--model-name", default="zhihan1996/DNABERT-2-117M", help="Hugging Face base model.")
37
- parser.add_argument("--max-length", type=int, default=256)
 
 
 
38
  parser.add_argument("--test-size", type=float, default=0.1)
39
  parser.add_argument("--eval-size", type=float, default=0.1)
40
  parser.add_argument("--epochs", type=float, default=2.0)
41
  parser.add_argument("--batch-size", type=int, default=8)
42
  parser.add_argument("--learning-rate", type=float, default=2e-5)
43
  parser.add_argument("--seed", type=int, default=42)
 
44
  return parser.parse_args()
45
 
46
 
@@ -64,6 +81,95 @@ def load_examples(path: str) -> Dataset:
64
  return Dataset.from_list(records)
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def split_dataset(dataset: Dataset, test_size: float, eval_size: float, seed: int):
68
  train_eval = dataset.train_test_split(test_size=test_size, seed=seed, stratify_by_column="labels")
69
  eval_fraction = eval_size / (1.0 - test_size)
@@ -76,7 +182,11 @@ def split_dataset(dataset: Dataset, test_size: float, eval_size: float, seed: in
76
 
77
 
78
  def compute_metrics(eval_pred):
79
- logits, labels = eval_pred
 
 
 
 
80
  predictions = np.argmax(logits, axis=-1)
81
  return {
82
  "accuracy": accuracy_score(labels, predictions),
@@ -86,18 +196,53 @@ def compute_metrics(eval_pred):
86
  }
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def main() -> None:
90
  args = parse_args()
91
  output_dir = Path(args.output_dir)
92
  output_dir.mkdir(parents=True, exist_ok=True)
93
 
94
- dataset = load_examples(args.dataset_jsonl)
95
- train_dataset, eval_dataset, test_dataset = split_dataset(
96
- dataset,
97
- test_size=args.test_size,
98
- eval_size=args.eval_size,
99
- seed=args.seed,
100
- )
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  tokenizer = AutoTokenizer.from_pretrained(args.model_name, trust_remote_code=True)
103
 
@@ -116,30 +261,23 @@ def main() -> None:
116
  trust_remote_code=True,
117
  )
118
 
119
- training_args = TrainingArguments(
120
- output_dir=str(output_dir / "checkpoints"),
121
- learning_rate=args.learning_rate,
122
- per_device_train_batch_size=args.batch_size,
123
- per_device_eval_batch_size=args.batch_size,
124
- num_train_epochs=args.epochs,
125
- evaluation_strategy="epoch",
126
- save_strategy="epoch",
127
- load_best_model_at_end=True,
128
- metric_for_best_model="f1",
129
- greater_is_better=True,
130
- report_to="none",
131
- seed=args.seed,
132
- )
133
 
134
- trainer = Trainer(
135
- model=model,
136
- args=training_args,
137
- train_dataset=train_dataset,
138
- eval_dataset=eval_dataset,
139
- tokenizer=tokenizer,
140
- data_collator=DataCollatorWithPadding(tokenizer=tokenizer),
141
- compute_metrics=compute_metrics,
142
- )
 
 
 
 
 
 
143
 
144
  trainer.train()
145
  test_metrics = trainer.evaluate(test_dataset, metric_key_prefix="test")
@@ -152,6 +290,11 @@ def main() -> None:
152
  "base_model": args.model_name,
153
  "genome_build": "GRCh38",
154
  "labels": ID_TO_LABEL,
 
 
 
 
 
155
  "test_metrics": test_metrics,
156
  "research_only": True,
157
  "disclaimer": "For research and education only. Not for medical diagnosis.",
 
7
  from __future__ import annotations
8
 
9
  import argparse
10
+ import inspect
11
  import json
12
+ import sys
13
  from pathlib import Path
14
 
15
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
16
+ if str(PROJECT_ROOT) not in sys.path:
17
+ sys.path.insert(0, str(PROJECT_ROOT))
18
+
19
  import numpy as np
20
+ import pandas as pd
21
  from datasets import Dataset
22
  from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
23
  from transformers import (
 
28
  TrainingArguments,
29
  )
30
 
31
+ from training.utils.label_utils import assign_binary_label
32
+
33
 
34
  ID_TO_LABEL = {
35
+ 0: "benign_or_likely_benign",
36
+ 1: "pathogenic",
37
  }
38
  LABEL_TO_ID = {label: idx for idx, label in ID_TO_LABEL.items()}
39
+ VALID_BASES = set("ACGTN")
40
 
41
 
42
  def parse_args() -> argparse.Namespace:
43
  parser = argparse.ArgumentParser(description=__doc__)
44
+ parser.add_argument("--dataset-jsonl", help="Legacy prepared JSONL from prepare_clinvar_dataset.py.")
45
+ parser.add_argument("--train-csv", help="CSV containing sequence and label columns for training.")
46
+ parser.add_argument("--val-csv", help="CSV containing sequence and label columns for validation.")
47
+ parser.add_argument("--test-csv", help="CSV containing sequence and label columns for final evaluation.")
48
  parser.add_argument("--output-dir", required=True, help="Directory for saved model artifacts.")
49
  parser.add_argument("--model-name", default="zhihan1996/DNABERT-2-117M", help="Hugging Face base model.")
50
+ parser.add_argument("--sequence-column", default="sequence")
51
+ parser.add_argument("--label-column", default="label")
52
+ parser.add_argument("--min-sequence-length", type=int, default=200)
53
+ parser.add_argument("--max-length", type=int, default=512)
54
  parser.add_argument("--test-size", type=float, default=0.1)
55
  parser.add_argument("--eval-size", type=float, default=0.1)
56
  parser.add_argument("--epochs", type=float, default=2.0)
57
  parser.add_argument("--batch-size", type=int, default=8)
58
  parser.add_argument("--learning-rate", type=float, default=2e-5)
59
  parser.add_argument("--seed", type=int, default=42)
60
+ parser.add_argument("--no-clean-clnsig", action="store_true", help="Do not re-filter CLNSIG labels in CSV inputs.")
61
  return parser.parse_args()
62
 
63
 
 
81
  return Dataset.from_list(records)
82
 
83
 
84
+ def clean_sequence(value: object) -> str:
85
+ sequence = "".join(base for base in str(value).upper() if base in VALID_BASES)
86
+ return sequence
87
+
88
+
89
+ def load_csv_split(
90
+ path: str,
91
+ sequence_column: str,
92
+ label_column: str,
93
+ min_sequence_length: int,
94
+ clean_clnsig: bool,
95
+ ) -> Dataset:
96
+ df = pd.read_csv(path)
97
+ missing_columns = [column for column in [sequence_column, label_column] if column not in df.columns]
98
+ if missing_columns:
99
+ raise ValueError(f"{path} is missing required columns: {missing_columns}")
100
+
101
+ start_rows = len(df)
102
+ df = df.copy()
103
+ df[sequence_column] = df[sequence_column].map(clean_sequence)
104
+ df = df[df[sequence_column].str.len() >= min_sequence_length].copy()
105
+
106
+ if clean_clnsig and "CLNSIG" in df.columns:
107
+ df["_clean_label"] = df["CLNSIG"].apply(assign_binary_label)
108
+ df = df[df["_clean_label"].notna()].copy()
109
+ df[label_column] = df["_clean_label"].astype(int)
110
+
111
+ df[label_column] = pd.to_numeric(df[label_column], errors="coerce")
112
+ df = df[df[label_column].isin([0, 1])].copy()
113
+ df[label_column] = df[label_column].astype(int)
114
+ if df.empty:
115
+ raise ValueError(f"No usable rows remain after cleaning {path}.")
116
+
117
+ records = []
118
+ for _, row in df.iterrows():
119
+ records.append(
120
+ {
121
+ "sequence": row[sequence_column],
122
+ "labels": int(row[label_column]),
123
+ "variant_id": row.get("variant_id"),
124
+ "chromosome": row.get("CHROM"),
125
+ "position": row.get("POS"),
126
+ "gene": row.get("gene_symbol"),
127
+ "clnsig": row.get("CLNSIG"),
128
+ }
129
+ )
130
+
131
+ print(
132
+ f"{path}: kept {len(records):,}/{start_rows:,} rows "
133
+ f"with label counts {df[label_column].value_counts().sort_index().to_dict()}"
134
+ )
135
+ return Dataset.from_list(records)
136
+
137
+
138
+ def load_csv_splits(args: argparse.Namespace) -> tuple[Dataset, Dataset, Dataset]:
139
+ required = {
140
+ "--train-csv": args.train_csv,
141
+ "--val-csv": args.val_csv,
142
+ "--test-csv": args.test_csv,
143
+ }
144
+ missing = [name for name, value in required.items() if not value]
145
+ if missing:
146
+ raise ValueError(f"CSV training requires: {', '.join(missing)}")
147
+
148
+ clean_clnsig = not args.no_clean_clnsig
149
+ train_dataset = load_csv_split(
150
+ args.train_csv,
151
+ sequence_column=args.sequence_column,
152
+ label_column=args.label_column,
153
+ min_sequence_length=args.min_sequence_length,
154
+ clean_clnsig=clean_clnsig,
155
+ )
156
+ eval_dataset = load_csv_split(
157
+ args.val_csv,
158
+ sequence_column=args.sequence_column,
159
+ label_column=args.label_column,
160
+ min_sequence_length=args.min_sequence_length,
161
+ clean_clnsig=clean_clnsig,
162
+ )
163
+ test_dataset = load_csv_split(
164
+ args.test_csv,
165
+ sequence_column=args.sequence_column,
166
+ label_column=args.label_column,
167
+ min_sequence_length=args.min_sequence_length,
168
+ clean_clnsig=clean_clnsig,
169
+ )
170
+ return train_dataset, eval_dataset, test_dataset
171
+
172
+
173
  def split_dataset(dataset: Dataset, test_size: float, eval_size: float, seed: int):
174
  train_eval = dataset.train_test_split(test_size=test_size, seed=seed, stratify_by_column="labels")
175
  eval_fraction = eval_size / (1.0 - test_size)
 
182
 
183
 
184
  def compute_metrics(eval_pred):
185
+ if hasattr(eval_pred, "predictions"):
186
+ logits = eval_pred.predictions
187
+ labels = eval_pred.label_ids
188
+ else:
189
+ logits, labels = eval_pred
190
  predictions = np.argmax(logits, axis=-1)
191
  return {
192
  "accuracy": accuracy_score(labels, predictions),
 
196
  }
197
 
198
 
199
+ def build_training_args(args: argparse.Namespace, output_dir: Path) -> TrainingArguments:
200
+ kwargs = {
201
+ "output_dir": str(output_dir / "checkpoints"),
202
+ "learning_rate": args.learning_rate,
203
+ "per_device_train_batch_size": args.batch_size,
204
+ "per_device_eval_batch_size": args.batch_size,
205
+ "num_train_epochs": args.epochs,
206
+ "save_strategy": "epoch",
207
+ "load_best_model_at_end": True,
208
+ "metric_for_best_model": "f1",
209
+ "greater_is_better": True,
210
+ "report_to": "none",
211
+ "seed": args.seed,
212
+ }
213
+
214
+ signature = inspect.signature(TrainingArguments.__init__)
215
+ if "eval_strategy" in signature.parameters:
216
+ kwargs["eval_strategy"] = "epoch"
217
+ else:
218
+ kwargs["evaluation_strategy"] = "epoch"
219
+
220
+ return TrainingArguments(**kwargs)
221
+
222
+
223
  def main() -> None:
224
  args = parse_args()
225
  output_dir = Path(args.output_dir)
226
  output_dir.mkdir(parents=True, exist_ok=True)
227
 
228
+ if args.train_csv or args.val_csv or args.test_csv:
229
+ train_dataset, eval_dataset, test_dataset = load_csv_splits(args)
230
+ dataset_source = {
231
+ "train_csv": args.train_csv,
232
+ "val_csv": args.val_csv,
233
+ "test_csv": args.test_csv,
234
+ }
235
+ elif args.dataset_jsonl:
236
+ dataset = load_examples(args.dataset_jsonl)
237
+ train_dataset, eval_dataset, test_dataset = split_dataset(
238
+ dataset,
239
+ test_size=args.test_size,
240
+ eval_size=args.eval_size,
241
+ seed=args.seed,
242
+ )
243
+ dataset_source = {"dataset_jsonl": args.dataset_jsonl}
244
+ else:
245
+ raise ValueError("Provide either --dataset-jsonl or all of --train-csv, --val-csv, and --test-csv.")
246
 
247
  tokenizer = AutoTokenizer.from_pretrained(args.model_name, trust_remote_code=True)
248
 
 
261
  trust_remote_code=True,
262
  )
263
 
264
+ training_args = build_training_args(args, output_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
+ trainer_kwargs = {
267
+ "model": model,
268
+ "args": training_args,
269
+ "train_dataset": train_dataset,
270
+ "eval_dataset": eval_dataset,
271
+ "data_collator": DataCollatorWithPadding(tokenizer=tokenizer),
272
+ "compute_metrics": compute_metrics,
273
+ }
274
+ trainer_signature = inspect.signature(Trainer.__init__)
275
+ if "processing_class" in trainer_signature.parameters:
276
+ trainer_kwargs["processing_class"] = tokenizer
277
+ else:
278
+ trainer_kwargs["tokenizer"] = tokenizer
279
+
280
+ trainer = Trainer(**trainer_kwargs)
281
 
282
  trainer.train()
283
  test_metrics = trainer.evaluate(test_dataset, metric_key_prefix="test")
 
290
  "base_model": args.model_name,
291
  "genome_build": "GRCh38",
292
  "labels": ID_TO_LABEL,
293
+ "dataset_source": dataset_source,
294
+ "train_rows": len(train_dataset),
295
+ "eval_rows": len(eval_dataset),
296
+ "test_rows": len(test_dataset),
297
+ "max_length": args.max_length,
298
  "test_metrics": test_metrics,
299
  "research_only": True,
300
  "disclaimer": "For research and education only. Not for medical diagnosis.",
training/utils/label_utils.py CHANGED
@@ -6,10 +6,15 @@ from urllib.parse import unquote
6
 
7
 
8
  DROP_LABEL_TERMS = (
 
9
  "conflicting interpretations",
 
 
10
  "uncertain significance",
 
11
  "risk factor",
12
  "association",
 
13
  "drug response",
14
  "protective",
15
  "not provided",
 
6
 
7
 
8
  DROP_LABEL_TERMS = (
9
+ "conflicting",
10
  "conflicting interpretations",
11
+ "conflicting classifications",
12
+ "uncertain",
13
  "uncertain significance",
14
+ "risk",
15
  "risk factor",
16
  "association",
17
+ "drug",
18
  "drug response",
19
  "protective",
20
  "not provided",