convitom commited on
Commit
3248df2
·
1 Parent(s): 6c43000
.gitignore CHANGED
@@ -1,4 +1,7 @@
1
  *.docx
2
  *.png
3
  __pycache__/
4
- .claude/
 
 
 
 
1
  *.docx
2
  *.png
3
  __pycache__/
4
+ .claude/
5
+ *.pdf
6
+
7
+
data/export_split_by_pathology.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ export_split_by_pathology.py
3
+ ----------------------------
4
+ Tải 1 split (mặc định: test) của MIMIC-CXR_resized từ HF, giải nén tar shards,
5
+ rồi đổ ảnh vào thư mục theo 14 nhãn bệnh lý CheXpert.
6
+
7
+ Vì sao phải làm thế này:
8
+ - Trên HF, MIMIC-CXR_resized lưu ảnh dưới dạng tar shards (cxr-0000.tar, ...),
9
+ KHÔNG tách theo split. Ảnh của 1 split nằm rải khắp các shard.
10
+ - manifest_{train,val,test}.csv mới là nơi biết ảnh nào thuộc split nào, và
11
+ chứa 14 cột `chex_<Pathology>` (giá trị U-MultiClass: 1/0/-1/blank).
12
+ - Mỗi study là MULTI-LABEL: 1 ảnh có thể dương tính nhiều bệnh -> được copy
13
+ vào NHIỀU thư mục (mỗi nhãn positive 1 thư mục).
14
+
15
+ Quy ước nhãn (giống mimic_cxr_resized_builder._row_to_pnu):
16
+ "1" / "1.0" -> positive -> bỏ vào thư mục <Pathology>/
17
+ "-1" / "-1.0" -> uncertain -> tuỳ --uncertain
18
+ "0"/"0.0"/blank -> negative -> bỏ qua
19
+
20
+ Cấu trúc output (mặc định --uncertain separate):
21
+ OUT/
22
+ Cardiomegaly/<dicom>.jpg
23
+ Pleural_Effusion/<dicom>.jpg
24
+ No_Finding/<dicom>.jpg
25
+ _uncertain/Atelectasis/<dicom>.jpg (nếu --uncertain separate)
26
+ _summary.csv (đếm ảnh mỗi nhãn)
27
+
28
+ Chạy LOCAL (đã có data) hoặc trên Colab/Kaggle (tự tải từ HF):
29
+ # tải từ HF rồi export test
30
+ python data/export_split_by_pathology.py --out ./test_by_pathology
31
+
32
+ # đã giải nén sẵn cây files/ ở đâu đó -> bỏ qua tải/giải nén
33
+ python data/export_split_by_pathology.py --out ./test_by_pathology \
34
+ --extracted_root /content/data/MIMIC-CXR_resized \
35
+ --manifest /content/data/MIMIC-CXR_resized/manifest_test.csv
36
+
37
+ # export cả split val, dùng hardlink cho đỡ tốn ổ
38
+ python data/export_split_by_pathology.py --split val --link hardlink
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import argparse
43
+ import csv
44
+ import os
45
+ import sys
46
+ import tarfile
47
+ from collections import defaultdict
48
+ from pathlib import Path
49
+
50
+ # 14 nhãn CheXpert — single source of truth.
51
+ try:
52
+ from model.chexpert_classifier import PATHOLOGIES
53
+ except Exception:
54
+ # fallback nếu chạy ngoài project (giữ đúng thứ tự).
55
+ PATHOLOGIES = [
56
+ "No Finding", "Enlarged Cardiomediastinum", "Cardiomegaly", "Lung Opacity",
57
+ "Lung Lesion", "Edema", "Consolidation", "Pneumonia", "Atelectasis",
58
+ "Pneumothorax", "Pleural Effusion", "Pleural Other", "Fracture",
59
+ "Support Devices",
60
+ ]
61
+
62
+ _POS = {"1", "1.0"}
63
+ _UNC = {"-1", "-1.0"}
64
+
65
+
66
+ def _safe(name: str) -> str:
67
+ """Tên thư mục an toàn: 'Pleural Effusion' -> 'Pleural_Effusion'."""
68
+ return name.replace(" ", "_")
69
+
70
+
71
+ def _norm(p: str) -> str:
72
+ """Chuẩn hoá path để so khớp tar member name <-> manifest image_relpath."""
73
+ return p.replace("\\", "/").lstrip("/")
74
+
75
+
76
+ # ── Phase 1: tải từ HF (manifest + shards) ──────────────────────────────────
77
+
78
+ def download_from_hf(repo_id: str, split: str, work: Path) -> tuple[Path, list[Path]]:
79
+ """Tải manifest_<split>.csv + toàn bộ tar shards về `work`.
80
+ Trả về (manifest_path, [shard_paths])."""
81
+ from huggingface_hub import snapshot_download
82
+
83
+ manifest_name = {"train": "manifest_train.csv",
84
+ "val": "manifest_val.csv",
85
+ "validate": "manifest_val.csv",
86
+ "test": "manifest_test.csv"}[split]
87
+
88
+ print(f"[download] snapshot_download {repo_id}:MIMIC-CXR_resized "
89
+ f"(manifest + shards) -> {work}")
90
+ snapshot_download(
91
+ repo_id=repo_id,
92
+ repo_type="dataset",
93
+ local_dir=str(work),
94
+ allow_patterns=[
95
+ f"MIMIC-CXR_resized/{manifest_name}",
96
+ "MIMIC-CXR_resized/shards/*.tar",
97
+ ],
98
+ )
99
+ mr = work / "MIMIC-CXR_resized"
100
+ manifest = mr / manifest_name
101
+ shards = sorted((mr / "shards").glob("*.tar"))
102
+ if not manifest.is_file():
103
+ sys.exit(f"ERROR: không thấy manifest sau khi tải: {manifest}")
104
+ if not shards:
105
+ sys.exit(f"ERROR: không thấy tar shard nào dưới {mr/'shards'}")
106
+ print(f"[download] manifest={manifest.name} shards={len(shards)}")
107
+ return manifest, shards
108
+
109
+
110
+ # ── Phase 2: đọc manifest -> map ảnh test -> nhãn ───────────────────────────
111
+
112
+ def load_label_map(manifest: Path):
113
+ """Trả về dict: image_relpath(norm) -> {'pos': set, 'unc': set, 'report': str|None}."""
114
+ label_map: dict[str, dict] = {}
115
+ missing_cols = None
116
+ with open(manifest, encoding="utf-8", newline="") as f:
117
+ reader = csv.DictReader(f)
118
+ cols = reader.fieldnames or []
119
+ chex_cols = {p: f"chex_{p}" for p in PATHOLOGIES if f"chex_{p}" in cols}
120
+ missing_cols = [p for p in PATHOLOGIES if f"chex_{p}" not in cols]
121
+ rel_col = "image_relpath" if "image_relpath" in cols else None
122
+ if rel_col is None:
123
+ sys.exit(f"ERROR: manifest thiếu cột 'image_relpath'. Có: {cols}")
124
+ has_report = "report_relpath" in cols
125
+ for row in reader:
126
+ rel = _norm(str(row[rel_col]).strip())
127
+ pos, unc = set(), set()
128
+ for path, col in chex_cols.items():
129
+ v = str(row.get(col, "")).strip()
130
+ if v in _POS:
131
+ pos.add(path)
132
+ elif v in _UNC:
133
+ unc.add(path)
134
+ rep = _norm(str(row["report_relpath"]).strip()) if has_report else None
135
+ label_map[rel] = {"pos": pos, "unc": unc, "report": rep or None}
136
+ if missing_cols:
137
+ print(f"[labels] CẢNH BÁO: manifest thiếu cột cho: {missing_cols}")
138
+ if not has_report:
139
+ print("[labels] CẢNH BÁO: manifest không có cột 'report_relpath' → bỏ qua report")
140
+ print(f"[labels] {len(label_map):,} ảnh trong manifest")
141
+ return label_map
142
+
143
+
144
+ def gather_reports(shards: list[Path], report_set: set) -> dict:
145
+ """Pass phụ: rút text của các report cần dùng từ tar (report nằm rải, gom 1 lượt).
146
+ Report là file .txt nhỏ nên giữ trong RAM thoải mái."""
147
+ reports: dict[str, bytes] = {}
148
+ if not report_set:
149
+ return reports
150
+ for shard in shards:
151
+ with tarfile.open(shard, "r") as tf:
152
+ for m in tf:
153
+ if not m.isfile():
154
+ continue
155
+ name = _norm(m.name)
156
+ if name in report_set and name not in reports:
157
+ reports[name] = tf.extractfile(m).read()
158
+ print(f"[reports] rút được {len(reports):,} / {len(report_set):,} report")
159
+ return reports
160
+
161
+
162
+ # ── Phase 3: rút ảnh từ tar -> thư mục theo nhãn ────────────────────────────
163
+
164
+ def _place(data: bytes, dicom_name: str, paths: set, base: Path,
165
+ counts: defaultdict, link_mode: str, report: bytes | None = None):
166
+ """Ghi 1 ảnh (và report cùng tên .txt nếu có) vào nhiều thư mục nhãn."""
167
+ txt_name = Path(dicom_name).stem + ".txt"
168
+ first_written: Path | None = None
169
+ for lab in paths:
170
+ d = base / _safe(lab)
171
+ d.mkdir(parents=True, exist_ok=True)
172
+ dst = d / dicom_name
173
+ counts[lab] += 1
174
+ # report .txt đặt cạnh ảnh, cùng tên
175
+ if report is not None:
176
+ (d / txt_name).write_bytes(report)
177
+ if dst.exists():
178
+ continue
179
+ if link_mode == "copy" or first_written is None:
180
+ dst.write_bytes(data)
181
+ first_written = dst
182
+ else:
183
+ try:
184
+ if link_mode == "hardlink":
185
+ os.link(first_written, dst)
186
+ else: # symlink
187
+ os.symlink(os.path.abspath(first_written), dst)
188
+ except OSError:
189
+ dst.write_bytes(data) # fallback nếu FS không hỗ trợ link
190
+
191
+
192
+ def export(shards: list[Path], label_map: dict, out: Path,
193
+ uncertain: str, link_mode: str, with_report: bool = True):
194
+ out.mkdir(parents=True, exist_ok=True)
195
+ unc_base = out / "_uncertain"
196
+
197
+ test_set = set(label_map.keys())
198
+
199
+ # Gom report cần dùng (1 pass phụ qua tar) trước khi rút ảnh.
200
+ reports: dict = {}
201
+ if with_report:
202
+ report_set = {label_map[k]["report"] for k in test_set
203
+ if label_map[k].get("report")}
204
+ reports = gather_reports(shards, report_set)
205
+
206
+ counts_pos: defaultdict = defaultdict(int)
207
+ counts_unc: defaultdict = defaultdict(int)
208
+ n_imgs = 0
209
+ n_no_report = 0
210
+ seen: set[str] = set()
211
+
212
+ for si, shard in enumerate(shards, 1):
213
+ print(f"[extract] [{si}/{len(shards)}] {shard.name}")
214
+ with tarfile.open(shard, "r") as tf:
215
+ for m in tf:
216
+ if not m.isfile():
217
+ continue
218
+ name = _norm(m.name)
219
+ if name not in test_set:
220
+ continue
221
+ seen.add(name)
222
+ ent = label_map[name]
223
+ pos, unc = ent["pos"], ent["unc"]
224
+ if not pos and not (uncertain != "skip" and unc):
225
+ # không có nhãn positive (toàn negative) -> bỏ qua
226
+ if not pos:
227
+ continue
228
+ data = tf.extractfile(m).read()
229
+ dicom_name = Path(name).name
230
+ rep = reports.get(ent.get("report")) if with_report else None
231
+ if with_report and rep is None:
232
+ n_no_report += 1
233
+ n_imgs += 1
234
+ if pos:
235
+ _place(data, dicom_name, pos, out, counts_pos, link_mode, rep)
236
+ if unc and uncertain != "skip":
237
+ if uncertain == "merge":
238
+ _place(data, dicom_name, unc, out, counts_unc, link_mode, rep)
239
+ else: # separate
240
+ _place(data, dicom_name, unc, unc_base, counts_unc, link_mode, rep)
241
+
242
+ if with_report and n_no_report:
243
+ print(f"[reports] CẢNH BÁO: {n_no_report:,} ảnh không tìm thấy report → chỉ có .jpg")
244
+
245
+ missing = test_set - seen
246
+ print(f"\n[done] ảnh test rút được: {n_imgs:,} / {len(test_set):,} trong manifest")
247
+ if missing:
248
+ print(f"[done] CẢNH BÁO: {len(missing):,} ảnh trong manifest không thấy trong shard "
249
+ f"(ví dụ: {list(missing)[:3]})")
250
+ print(f"[done] ảnh toàn-negative (không có positive): bỏ qua")
251
+
252
+ # _summary.csv
253
+ summ = out / "_summary.csv"
254
+ with open(summ, "w", encoding="utf-8", newline="") as f:
255
+ w = csv.writer(f)
256
+ w.writerow(["pathology", "positive_images", "uncertain_images"])
257
+ for p in PATHOLOGIES:
258
+ w.writerow([p, counts_pos.get(p, 0), counts_unc.get(p, 0)])
259
+ print(f"[done] thống kê -> {summ}")
260
+ print("\n Nhãn positive uncertain")
261
+ for p in PATHOLOGIES:
262
+ print(f" {p:28s} {counts_pos.get(p,0):8d} {counts_unc.get(p,0):8d}")
263
+
264
+
265
+ # ── CLI ─────────────────────────────────────────────────────────────────────
266
+
267
+ def main():
268
+ ap = argparse.ArgumentParser(
269
+ description="Export 1 split của MIMIC-CXR_resized thành thư mục theo 14 nhãn bệnh lý.")
270
+ ap.add_argument("--out", required=True, help="Thư mục output.")
271
+ ap.add_argument("--split", default="test", choices=["train", "val", "validate", "test"])
272
+ ap.add_argument("--repo_id", default="hieu3636/cxr-vlm-data")
273
+ ap.add_argument("--work", default="./_hf_resized_dl",
274
+ help="Thư mục cache tải từ HF (khi không dùng --extracted_root).")
275
+ ap.add_argument("--extracted_root", default=None,
276
+ help="Nếu đã có shards giải nén/tar sẵn ở local: trỏ tới thư mục "
277
+ "MIMIC-CXR_resized (chứa shards/*.tar). Bỏ qua bước tải HF.")
278
+ ap.add_argument("--manifest", default=None,
279
+ help="Đường dẫn manifest_<split>.csv (mặc định lấy trong dữ liệu đã tải).")
280
+ ap.add_argument("--uncertain", default="separate",
281
+ choices=["separate", "merge", "skip"],
282
+ help="Xử lý nhãn uncertain: separate=thư mục _uncertain/<P>; "
283
+ "merge=gộp chung <P>; skip=bỏ. Mặc định separate.")
284
+ ap.add_argument("--link", default="copy", choices=["copy", "hardlink", "symlink"],
285
+ help="copy (an toàn nhất) | hardlink/symlink (tiết kiệm ổ khi 1 ảnh nhiều nhãn).")
286
+ ap.add_argument("--no_report", action="store_true",
287
+ help="Không ghi report .txt cạnh ảnh (mặc định CÓ ghi).")
288
+ a = ap.parse_args()
289
+
290
+ out = Path(a.out)
291
+
292
+ # 1) Lấy manifest + shards
293
+ if a.extracted_root:
294
+ mr = Path(a.extracted_root)
295
+ shards = sorted((mr / "shards").glob("*.tar")) or sorted(mr.glob("*.tar"))
296
+ if not shards:
297
+ sys.exit(f"ERROR: không thấy *.tar dưới {mr} hoặc {mr/'shards'}")
298
+ if a.manifest:
299
+ manifest = Path(a.manifest)
300
+ else:
301
+ mname = {"train": "manifest_train.csv", "val": "manifest_val.csv",
302
+ "validate": "manifest_val.csv", "test": "manifest_test.csv"}[a.split]
303
+ manifest = mr / mname
304
+ if not manifest.is_file():
305
+ sys.exit(f"ERROR: không thấy manifest: {manifest}")
306
+ print(f"[local] manifest={manifest} shards={len(shards)}")
307
+ else:
308
+ manifest, shards = download_from_hf(a.repo_id, a.split, Path(a.work))
309
+ if a.manifest:
310
+ manifest = Path(a.manifest)
311
+
312
+ # 2) Đọc nhãn
313
+ label_map = load_label_map(manifest)
314
+
315
+ # 3) Rút ảnh (+ report) -> thư mục nhãn
316
+ export(shards, label_map, out, a.uncertain, a.link, with_report=not a.no_report)
317
+ print(f"\nXong. Output: {out.resolve()}")
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()
data/export_test_by_pathology.ipynb ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "26283604",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Export tập test MIMIC-CXR_resized → thư mục theo 14 nhãn bệnh lý\n",
9
+ "\n",
10
+ "Chỉ cần điền **CONFIG** bên dưới rồi **Run All**.\n",
11
+ "\n",
12
+ "- Tải `manifest_test.csv` + tar shards từ HF (`hieu3636/cxr-vlm-data/MIMIC-CXR_resized/`).\n",
13
+ "- Rút ảnh test, đổ vào `OUT/<Tên_bệnh>/`. Ảnh multi-label → copy vào nhiều thư mục.\n",
14
+ "- **Kèm report**: mỗi ảnh `<dicom>.jpg` có file `<dicom>.txt` (nội dung report của study đó) đặt ngay cạnh.\n",
15
+ "- Repo **private** → cần token HF (điền vào `HF_TOKEN`, hoặc đã `huggingface-cli login` thì để trống)."
16
+ ]
17
+ },
18
+ {
19
+ "cell_type": "markdown",
20
+ "id": "c41dd18e",
21
+ "metadata": {},
22
+ "source": [
23
+ "## 1. CONFIG — chỉnh ở đây"
24
+ ]
25
+ },
26
+ {
27
+ "cell_type": "code",
28
+ "execution_count": null,
29
+ "id": "8fd81f44",
30
+ "metadata": {},
31
+ "outputs": [],
32
+ "source": [
33
+ "# ==== CHỈNH CÁC BIẾN NÀY ====\n",
34
+ "HF_TOKEN = \"\" # token HF (Read là đủ). Để \"\" nếu đã huggingface-cli login.\n",
35
+ "REPO_ID = \"hieu3636/cxr-vlm-data\"\n",
36
+ "SPLIT = \"test\" # \"train\" | \"val\" | \"test\"\n",
37
+ "\n",
38
+ "OUT = r\"D:\\USTH\\KLTN\\test_by_pathology\" # thư mục output\n",
39
+ "WORK = r\"D:\\USTH\\KLTN\\_hf_resized_dl\" # nơi cache tải từ HF\n",
40
+ "\n",
41
+ "# Nếu ĐÃ có shards giải nén/tar sẵn ở máy thì trỏ vào đây để KHỎI tải lại,\n",
42
+ "# ví dụ r\"D:\\USTH\\KLTN\\_hf_resized_dl\\MIMIC-CXR_resized\". Để None = tải từ HF.\n",
43
+ "EXTRACTED_ROOT = None\n",
44
+ "\n",
45
+ "WITH_REPORT = True # True = ghi kèm <dicom>.txt (report) cạnh mỗi ảnh\n",
46
+ "UNCERTAIN = \"separate\" # \"separate\" (_uncertain/<P>) | \"merge\" | \"skip\"\n",
47
+ "LINK = \"copy\" # \"copy\" | \"hardlink\" | \"symlink\" (hardlink đỡ tốn ổ)\n",
48
+ "# ============================\n",
49
+ "print(\"CONFIG ok | split =\", SPLIT, \"| out =\", OUT, \"| with_report =\", WITH_REPORT)"
50
+ ]
51
+ },
52
+ {
53
+ "cell_type": "markdown",
54
+ "id": "8085defe",
55
+ "metadata": {},
56
+ "source": [
57
+ "## 2. Cài thư viện (chạy 1 lần)"
58
+ ]
59
+ },
60
+ {
61
+ "cell_type": "code",
62
+ "execution_count": null,
63
+ "id": "11afbe51",
64
+ "metadata": {},
65
+ "outputs": [],
66
+ "source": [
67
+ "# Chỉ cần huggingface_hub; tarfile/csv là built-in.\n",
68
+ "try:\n",
69
+ " import huggingface_hub # noqa\n",
70
+ " print(\"huggingface_hub đã có:\", huggingface_hub.__version__)\n",
71
+ "except ImportError:\n",
72
+ " import sys, subprocess\n",
73
+ " subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"huggingface_hub\"])\n",
74
+ " print(\"đã cài huggingface_hub\")"
75
+ ]
76
+ },
77
+ {
78
+ "cell_type": "markdown",
79
+ "id": "ac53032e",
80
+ "metadata": {},
81
+ "source": [
82
+ "## 3. Logic (không cần sửa)"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "code",
87
+ "execution_count": null,
88
+ "id": "9ac60a59",
89
+ "metadata": {},
90
+ "outputs": [],
91
+ "source": [
92
+ "import os, csv, tarfile\n",
93
+ "from collections import defaultdict\n",
94
+ "from pathlib import Path\n",
95
+ "\n",
96
+ "# 14 nhãn CheXpert — đúng thứ tự dùng trong project.\n",
97
+ "PATHOLOGIES = [\n",
98
+ " \"No Finding\", \"Enlarged Cardiomediastinum\", \"Cardiomegaly\", \"Lung Opacity\",\n",
99
+ " \"Lung Lesion\", \"Edema\", \"Consolidation\", \"Pneumonia\", \"Atelectasis\",\n",
100
+ " \"Pneumothorax\", \"Pleural Effusion\", \"Pleural Other\", \"Fracture\",\n",
101
+ " \"Support Devices\",\n",
102
+ "]\n",
103
+ "_POS = {\"1\", \"1.0\"}\n",
104
+ "_UNC = {\"-1\", \"-1.0\"}\n",
105
+ "_MANIFEST = {\"train\": \"manifest_train.csv\", \"val\": \"manifest_val.csv\",\n",
106
+ " \"validate\": \"manifest_val.csv\", \"test\": \"manifest_test.csv\"}\n",
107
+ "\n",
108
+ "def _safe(n): return n.replace(\" \", \"_\")\n",
109
+ "def _norm(p): return p.replace(\"\\\\\", \"/\").lstrip(\"/\")\n",
110
+ "\n",
111
+ "def download_from_hf(repo_id, split, work):\n",
112
+ " from huggingface_hub import snapshot_download\n",
113
+ " mname = _MANIFEST[split]\n",
114
+ " print(f\"[download] {repo_id}:MIMIC-CXR_resized (manifest + shards) -> {work}\")\n",
115
+ " snapshot_download(\n",
116
+ " repo_id=repo_id, repo_type=\"dataset\", local_dir=str(work),\n",
117
+ " allow_patterns=[f\"MIMIC-CXR_resized/{mname}\", \"MIMIC-CXR_resized/shards/*.tar\"],\n",
118
+ " )\n",
119
+ " mr = Path(work) / \"MIMIC-CXR_resized\"\n",
120
+ " manifest = mr / mname\n",
121
+ " shards = sorted((mr / \"shards\").glob(\"*.tar\"))\n",
122
+ " assert manifest.is_file(), f\"không thấy manifest: {manifest}\"\n",
123
+ " assert shards, f\"không thấy tar shard dưới {mr/'shards'}\"\n",
124
+ " print(f\"[download] manifest={manifest.name} shards={len(shards)}\")\n",
125
+ " return manifest, shards\n",
126
+ "\n",
127
+ "def load_label_map(manifest):\n",
128
+ " label_map = {}\n",
129
+ " with open(manifest, encoding=\"utf-8\", newline=\"\") as f:\n",
130
+ " reader = csv.DictReader(f); cols = reader.fieldnames or []\n",
131
+ " chex_cols = {p: f\"chex_{p}\" for p in PATHOLOGIES if f\"chex_{p}\" in cols}\n",
132
+ " miss = [p for p in PATHOLOGIES if f\"chex_{p}\" not in cols]\n",
133
+ " assert \"image_relpath\" in cols, f\"manifest thiếu image_relpath. Có: {cols}\"\n",
134
+ " has_report = \"report_relpath\" in cols\n",
135
+ " for row in reader:\n",
136
+ " rel = _norm(str(row[\"image_relpath\"]).strip())\n",
137
+ " pos, unc = set(), set()\n",
138
+ " for p, c in chex_cols.items():\n",
139
+ " v = str(row.get(c, \"\")).strip()\n",
140
+ " if v in _POS: pos.add(p)\n",
141
+ " elif v in _UNC: unc.add(p)\n",
142
+ " rep = _norm(str(row[\"report_relpath\"]).strip()) if has_report else None\n",
143
+ " label_map[rel] = {\"pos\": pos, \"unc\": unc, \"report\": rep or None}\n",
144
+ " if miss: print(f\"[labels] CẢNH BÁO thiếu cột: {miss}\")\n",
145
+ " if not has_report: print(\"[labels] CẢNH BÁO: manifest không có report_relpath → bỏ qua report\")\n",
146
+ " print(f\"[labels] {len(label_map):,} ảnh trong manifest\")\n",
147
+ " return label_map\n",
148
+ "\n",
149
+ "def gather_reports(shards, report_set):\n",
150
+ " \"\"\"Gom text các report cần dùng (1 pass qua tar). Report nhỏ → giữ RAM.\"\"\"\n",
151
+ " reports = {}\n",
152
+ " if not report_set: return reports\n",
153
+ " for shard in shards:\n",
154
+ " with tarfile.open(shard, \"r\") as tf:\n",
155
+ " for m in tf:\n",
156
+ " if not m.isfile(): continue\n",
157
+ " name = _norm(m.name)\n",
158
+ " if name in report_set and name not in reports:\n",
159
+ " reports[name] = tf.extractfile(m).read()\n",
160
+ " print(f\"[reports] rút được {len(reports):,} / {len(report_set):,} report\")\n",
161
+ " return reports\n",
162
+ "\n",
163
+ "def _place(data, dicom, paths, base, counts, link, report=None):\n",
164
+ " txt = Path(dicom).stem + \".txt\"\n",
165
+ " first = None\n",
166
+ " for lab in paths:\n",
167
+ " d = base / _safe(lab); d.mkdir(parents=True, exist_ok=True)\n",
168
+ " dst = d / dicom; counts[lab] += 1\n",
169
+ " if report is not None: (d / txt).write_bytes(report)\n",
170
+ " if dst.exists(): continue\n",
171
+ " if link == \"copy\" or first is None:\n",
172
+ " dst.write_bytes(data); first = dst\n",
173
+ " else:\n",
174
+ " try:\n",
175
+ " os.link(first, dst) if link == \"hardlink\" else os.symlink(os.path.abspath(first), dst)\n",
176
+ " except OSError:\n",
177
+ " dst.write_bytes(data)\n",
178
+ "\n",
179
+ "def export(shards, label_map, out, uncertain, link, with_report=True):\n",
180
+ " out = Path(out); out.mkdir(parents=True, exist_ok=True)\n",
181
+ " unc_base = out / \"_uncertain\"\n",
182
+ " test_set = set(label_map)\n",
183
+ " reports = {}\n",
184
+ " if with_report:\n",
185
+ " rset = {label_map[k][\"report\"] for k in test_set if label_map[k].get(\"report\")}\n",
186
+ " reports = gather_reports(shards, rset)\n",
187
+ " cpos, cunc = defaultdict(int), defaultdict(int)\n",
188
+ " n_imgs = 0; n_no_rep = 0; seen = set()\n",
189
+ " for si, shard in enumerate(shards, 1):\n",
190
+ " print(f\"[extract] [{si}/{len(shards)}] {shard.name}\")\n",
191
+ " with tarfile.open(shard, \"r\") as tf:\n",
192
+ " for m in tf:\n",
193
+ " if not m.isfile(): continue\n",
194
+ " name = _norm(m.name)\n",
195
+ " if name not in test_set: continue\n",
196
+ " seen.add(name)\n",
197
+ " ent = label_map[name]; pos, unc = ent[\"pos\"], ent[\"unc\"]\n",
198
+ " if not pos and not (uncertain != \"skip\" and unc): continue\n",
199
+ " data = tf.extractfile(m).read(); dicom = Path(name).name; n_imgs += 1\n",
200
+ " rep = reports.get(ent.get(\"report\")) if with_report else None\n",
201
+ " if with_report and rep is None: n_no_rep += 1\n",
202
+ " if pos: _place(data, dicom, pos, out, cpos, link, rep)\n",
203
+ " if unc and uncertain != \"skip\":\n",
204
+ " _place(data, dicom, unc, (out if uncertain == \"merge\" else unc_base), cunc, link, rep)\n",
205
+ " if with_report and n_no_rep:\n",
206
+ " print(f\"[reports] CẢNH BÁO: {n_no_rep:,} ảnh không thấy report → chỉ có .jpg\")\n",
207
+ " missing = test_set - seen\n",
208
+ " print(f\"\\n[done] ảnh rút được: {n_imgs:,} / {len(test_set):,} trong manifest\")\n",
209
+ " if missing:\n",
210
+ " print(f\"[done] CẢNH BÁO: {len(missing):,} ảnh manifest không có trong shard (vd: {list(missing)[:2]})\")\n",
211
+ " with open(out / \"_summary.csv\", \"w\", encoding=\"utf-8\", newline=\"\") as f:\n",
212
+ " w = csv.writer(f); w.writerow([\"pathology\", \"positive_images\", \"uncertain_images\"])\n",
213
+ " for p in PATHOLOGIES: w.writerow([p, cpos.get(p, 0), cunc.get(p, 0)])\n",
214
+ " print(\"\\n Nhãn positive uncertain\")\n",
215
+ " for p in PATHOLOGIES:\n",
216
+ " print(f\" {p:28s} {cpos.get(p,0):8d} {cunc.get(p,0):8d}\")\n",
217
+ " return cpos, cunc\n",
218
+ "\n",
219
+ "print(\"logic loaded\")"
220
+ ]
221
+ },
222
+ {
223
+ "cell_type": "markdown",
224
+ "id": "f23b1e30",
225
+ "metadata": {},
226
+ "source": [
227
+ "## 4. Run"
228
+ ]
229
+ },
230
+ {
231
+ "cell_type": "code",
232
+ "execution_count": null,
233
+ "id": "df4a1338",
234
+ "metadata": {},
235
+ "outputs": [],
236
+ "source": [
237
+ "# token\n",
238
+ "if HF_TOKEN.strip():\n",
239
+ " os.environ[\"HF_TOKEN\"] = HF_TOKEN.strip()\n",
240
+ " os.environ[\"HUGGING_FACE_HUB_TOKEN\"] = HF_TOKEN.strip()\n",
241
+ "\n",
242
+ "# 1) manifest + shards\n",
243
+ "if EXTRACTED_ROOT:\n",
244
+ " mr = Path(EXTRACTED_ROOT)\n",
245
+ " shards = sorted((mr / \"shards\").glob(\"*.tar\")) or sorted(mr.glob(\"*.tar\"))\n",
246
+ " manifest = mr / _MANIFEST[SPLIT]\n",
247
+ " assert shards, f\"không thấy *.tar dưới {mr}\"\n",
248
+ " assert manifest.is_file(), f\"không thấy manifest: {manifest}\"\n",
249
+ " print(f\"[local] manifest={manifest} shards={len(shards)}\")\n",
250
+ "else:\n",
251
+ " manifest, shards = download_from_hf(REPO_ID, SPLIT, WORK)\n",
252
+ "\n",
253
+ "# 2) đọc nhãn 3) rút ảnh (+ report)\n",
254
+ "label_map = load_label_map(manifest)\n",
255
+ "cpos, cunc = export(shards, label_map, OUT, UNCERTAIN, LINK, with_report=WITH_REPORT)\n",
256
+ "print(f\"\\nXong! Output: {Path(OUT).resolve()}\")"
257
+ ]
258
+ }
259
+ ],
260
+ "metadata": {},
261
+ "nbformat": 4,
262
+ "nbformat_minor": 5
263
+ }
docs/convert_md.py CHANGED
@@ -26,7 +26,7 @@ OUTPUT_NAME = None
26
  # GOP NHIEU FILE THANH 1 BAO CAO:
27
  # - De COMBINE = [] (rong) -> dung che do INPUT o tren (convert tung file rieng).
28
  # - Liet ke file theo dung thu tu -> gop thanh 1 file COMBINE_OUTPUT duy nhat.
29
- COMBINE = ["report_part1.md", "methodology_rewrite.md", "report_part4_5.md"]
30
  COMBINE_OUTPUT = "report.docx"
31
 
32
  # =====================================================================
 
26
  # GOP NHIEU FILE THANH 1 BAO CAO:
27
  # - De COMBINE = [] (rong) -> dung che do INPUT o tren (convert tung file rieng).
28
  # - Liet ke file theo dung thu tu -> gop thanh 1 file COMBINE_OUTPUT duy nhat.
29
+ COMBINE = ["report_front.md", "report_part1.md", "methodology_rewrite.md", "report_part4_5.md"]
30
  COMBINE_OUTPUT = "report.docx"
31
 
32
  # =====================================================================
docs/export_eda_figs.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Trích các biểu đồ EDA đã render sẵn trong output cell của notebook -> file PNG.
3
+ Chay: python export_eda_figs.py (anh ra docs/figures/)
4
+
5
+ Neu ban chay lai notebook va cell index doi, sua dict PICKS ben duoi.
6
+ """
7
+ import base64, json
8
+ from pathlib import Path
9
+
10
+ HERE = Path(__file__).resolve().parent
11
+ NB_DIR = HERE.parent / "data"
12
+ OUT = HERE / "figures"
13
+ OUT.mkdir(exist_ok=True)
14
+
15
+ # (notebook, cell_index) -> ten file png
16
+ PICKS = {
17
+ ("eda_full.ipynb", 25): "eda_chexpert_labels.png", # P/U/N 14 nhan
18
+ ("eda_full.ipynb", 17): "eda_views.png", # view position bar+pie
19
+ ("eda_full.ipynb", 14): "eda_imgs_per_study.png", # so anh / study
20
+ ("eda_full.ipynb", 31): "eda_report_length.png", # do dai findings/impression
21
+ ("eda_full.ipynb", 40): "eda_vqa_types.png", # semantic + content type
22
+ ("build_subset_local.ipynb", 16): "eda_prevalence_compare.png", # full vs elig vs subset
23
+ ("build_subset_local.ipynb", 17): "eda_vqa_compare.png", # VQA full vs subset
24
+ }
25
+
26
+
27
+ def first_png(cell):
28
+ for o in cell.get("outputs", []):
29
+ img = (o.get("data", {}) or {}).get("image/png")
30
+ if img:
31
+ return img if isinstance(img, str) else "".join(img)
32
+ return None
33
+
34
+
35
+ def main():
36
+ for (nb_name, idx), out_name in PICKS.items():
37
+ nb = json.load(open(NB_DIR / nb_name, encoding="utf-8"))
38
+ if idx >= len(nb["cells"]):
39
+ print(f"[x] {nb_name}: khong co cell {idx}")
40
+ continue
41
+ png_b64 = first_png(nb["cells"][idx])
42
+ if not png_b64:
43
+ print(f"[x] {nb_name} cell {idx}: khong co anh output")
44
+ continue
45
+ (OUT / out_name).write_bytes(base64.b64decode(png_b64))
46
+ src = "".join(nb["cells"][idx].get("source", []))
47
+ print(f"[+] {out_name:<28s} <- {nb_name} cell {idx} ({src[:55].strip()}...)")
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
docs/make_figures.py CHANGED
@@ -296,12 +296,13 @@ def fig01_pipeline():
296
  # ---- cac hinh con lai (don gian, den trang) --------------------------------
297
 
298
  def fig02_data_pipeline():
299
- fig, ax = canvas(132, 24)
300
  hrow(ax, 12, [
301
- (16, 22, "[Local]\nSelection", False),
302
- (49, 26, "[Cloud VM]\nImage download", False),
303
- (84, 22, "[GPU]\nResize + shard", False),
304
- (118, 18, "Unified\nJSON", False),
 
305
  ], h=14)
306
  ax.text(49, 2, "(download from PhysioNet -> push to Hugging Face)",
307
  ha="center", fontsize=6.6)
 
296
  # ---- cac hinh con lai (don gian, den trang) --------------------------------
297
 
298
  def fig02_data_pipeline():
299
+ fig, ax = canvas(136, 24)
300
  hrow(ax, 12, [
301
+ (16, 22, "Data Sources", False),
302
+ (49, 22, "[Local]\nSelection", False),
303
+ (82, 26, "[Cloud VM]\nImage download", False),
304
+ (117, 22, "Resize + shard", False),
305
+ (151, 18, "Unified\nJSON", False),
306
  ], h=14)
307
  ax.text(49, 2, "(download from PhysioNet -> push to Hugging Face)",
308
  ha="center", fontsize=6.6)
docs/methodology_rewrite.md CHANGED
@@ -1,14 +1,14 @@
1
  # 3. Materials and Methods
2
 
3
- This chapter describes how the proposed Vision–Language Model (**CXR-VLM**) is built and trained. It is organised in six parts. Section 3.1 gives an end-to-end overview. Section 3.2 defines the key concepts used throughout the chapter. Section 3.3 documents how the MIMIC-CXR corpus is filtered and serialised into the unified instruction format. Section 3.4 describes the four modules of the model and the prompt that ties them together. Section 3.5 specifies the two-stage training schedule. Section 3.6 defines the evaluation protocol.
4
 
5
  ---
6
 
7
  ## 3.1 Overview
8
 
9
- CXR-VLM is a single vision–language network that solves three downstream tasks — **findings generation**, **impression generation**, and **visual question answering (VQA)** — through one shared backbone. The design follows the RaDialog recipe, with two modifications inspired by META-CXR's U-MultiClass and BLIP-2's image–text contrastive alignment:
10
 
11
- - a frozen 14-pathology CheXpert-style classifier whose predictions are serialised into the prompt as a **Positive / Negative / Uncertain (PNU)** string, and
12
  - an optional contrastive Stage 1 that pre-aligns the projection in a joint image–text space without ever loading the language model.
13
 
14
  The full forward path can be summarised as:
@@ -17,7 +17,7 @@ $$
17
  \mathbf{x}_{518\times518} \xrightarrow{\text{RAD-DINO}} \mathbf{P} \in \mathbb{R}^{B\times 1369\times 768} \xrightarrow{\text{MLP-Proj}} \mathbf{V} \in \mathbb{R}^{B\times 32\times 4096} \xrightarrow{\text{Vicuna-7B + LoRA}} \hat{\mathbf{y}}
18
  $$
19
 
20
- Only the MLP projection, the LoRA adapters on Vicuna, and (when enabled) the ITC head are trained. The image encoder, the CheXpert classifier, and the Vicuna base weights are kept frozen. In total, fewer than **0.3%** of the parameters are trainable (≈ 21.5 M of ≈ 7.1 B).
21
 
22
  ![Hình 3.1. Sơ đồ tổng thể quy trình của CXR-VLM: từ dữ liệu, huấn luyện hai stage, tới suy luận và đánh giá.](figures/fig15_workflow.png)
23
 
@@ -29,31 +29,31 @@ This section defines the core concepts that the rest of the chapter relies on.
29
 
30
  - **Vision–Language Model (VLM).** A model that takes an image together with a text instruction and produces a text response, by mapping visual features into the embedding space of a language model.
31
 
32
- - **Chest X-ray report.** A radiology report has two main free-text sections. **Findings** is the detailed, observation-by-observation description of the image; **Impression** is the short clinical summary written *after* the findings. We treat their generation as two separate tasks.
33
 
34
  - **Visual Question Answering (VQA).** Given an image and a natural-language question, the model returns a short answer (often a single word or phrase).
35
 
36
- - **Large Language Model (LLM).** A transformer trained on large text corpora to generate human-like text. We use **Vicuna-7B**, a LLaMA-derived instruction-tuned chat model, as the decoder.
37
 
38
- - **Vision Transformer (ViT) and self-supervision.** A ViT splits an image into fixed-size patches and processes them as a token sequence. **RAD-DINO** is a ViT-B/14 trained with DINOv2 self-supervision on chest X-rays; it requires no text labels.
39
 
40
- - **Parameter-efficient fine-tuning (LoRA / QLoRA).** Instead of updating all weights of the LLM, **LoRA** inserts small trainable low-rank matrices into selected layers and freezes the rest. **QLoRA** additionally keeps the base weights in 4-bit quantisation, which drastically reduces memory.
41
 
42
  - **Image–Text Contrastive learning (ITC / InfoNCE).** A training objective that pulls the embedding of an image and its matching text together while pushing non-matching pairs apart, using the symmetric InfoNCE loss. It is the mechanism behind Stage 1 alignment.
43
 
44
- - **CheXpert labels and U-MultiClass.** CheXpert defines 14 pathology categories. Rather than a binary present/absent label, **U-MultiClass** keeps three states per pathology — **Positive**, **Negative**, **Uncertain** — preserving the clinically important difference between a confident negative and a hedged one.
45
 
46
  ---
47
 
48
  ## 3.3 Data Preparation
49
 
50
- The data pipeline runs in three phases, each placed where it is most efficient. **(i) Selection** runs locally on the MIMIC-CXR CSV metadata to decide which studies to keep. **(ii) Image download** runs on a cloud virtual machine (Google Colab): the images chosen in the manifest are downloaded from PhysioNet onto the VM's disk and then pushed to a Hugging Face repository for reuse. This step is done in the cloud rather than on a local machine because the selected image set is too large for the local disk, and the cloud VM also has a faster, more stable connection to PhysioNet. **(iii) Resize and re-shard** runs once on a GPU host so that every training run consumes minimal-size JPEGs. The same subset is reused across all experiments, so the pipeline is run **once** — changing prompt templates or task weights only rebuilds the JSON, not the images.
51
 
52
  ![Hình 3.2. Pipeline dữ liệu 3 phase: selection (local) → image download (VM cloud → Hugging Face) → resize/shard → unified JSON.](figures/fig02_data_pipeline.png)
53
 
54
- ### 3.3.1 Sources
55
 
56
- All sources come from the public PhysioNet distribution under credentialed access; no manual annotation is performed.
57
 
58
  | Source | Version | Used for |
59
  |---|---|---|
@@ -61,50 +61,88 @@ All sources come from the public PhysioNet distribution under credentialed acces
61
  | MIMIC-CXR-JPG | 2.1.0 | Pre-converted JPEG images |
62
  | MIMIC-Ext-CXR-VQA | 1.0.0 | (image, question, answer) triples |
63
  | `mimic-cxr-2.0.0-split` | — | Official patient-disjoint train/validate/test split |
64
- | `mimic-cxr-2.0.0-metadata` | — | `ViewPosition` for frontal selection |
65
  | `mimic-cxr-2.0.0-chexpert` | — | 14 pathology labels per study |
66
 
67
- ### 3.3.2 Selection Pipeline
68
 
69
- A four-stage filter chain reduces the 227,000 MIMIC-CXR studies to a working subset of **50,000 studies (40,000 train / 5,000 validation / 5,000 test)**.
 
 
 
 
70
 
71
- - **(a) Frontal-only filtering.** Each DICOM is joined with the metadata CSV; only `ViewPosition {PA, AP}` rows are kept. A study with several frontal exposures is collapsed to one image, preferring PA over AP. After this step every retained study contributes exactly one image — the assumption behind `image_mode = frontal_only_split`.
72
- - **(b) Report parsing.** A strict regex accepts a section only if its header is exactly `FINDINGS` or `IMPRESSION`. Synonyms (`CONCLUSION`, `WET READ`, composite headers) are deliberately not merged. A study survives only if **both** sections are present and non-empty — required for `split_cascade`, where the impression prompt uses the ground-truth findings.
73
- - **(c) Length-based outlier removal.** Per-section word counts are computed; studies above `Q3 + 1.5·IQR` (multi-paragraph teaching reports) or below a small floor are dropped. This trims the long tail without shifting the median.
74
- - **(d) Stratified patient-disjoint sampling.** Each study is assigned a stratum equal to its rarest positive CheXpert label; the target counts are allocated per stratum proportionally to prevalence. Validation/test pools are filled first from the official split; any overflow is drawn from train and the affected subjects removed from train. The three sets are therefore **patient-disjoint**.
75
 
76
- Distribution preservation is checked by comparing per-pathology prevalence at the raw, eligible-pool, and final-subset levels.
77
 
78
- ![Hình 3.3. Chuỗi lọc 4 tầng rút gọn corpus MIMIC-CXR còn 50k study.](figures/fig03_selection_funnel.png)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  ### 3.3.3 Storage and Consumption Layout
81
 
82
  For each split a manifest is emitted as JSON and CSV. Every row captures one image plus the standard PhysioNet identifiers, the 14 `chex_*` U-MultiClass label columns, and a `has_vqa` flag. The PhysioNet directory layout (`files/pXX/pSUBJ/sSTUDY/<dicom>.jpg`) is preserved so a partial download can be checked against the manifest by path lookup.
83
 
84
- Because all training hosts consume the same images, we resize **once** offline. RAD-DINO centre-crops to 518×518 regardless of input, so the full-resolution JPEGs (~2–3 MP each, ~100 GB) are wasteful. Each image is resized so the shortest edge is 518, saved as JPEG quality 90, and packed into ~2 GB tar shards; the 50k subset compresses to ~5–8 GB.
85
 
86
- ### 3.3.4 Unified Instruction JSON
87
 
88
- On the training host, a single **builder** turns the resized tree plus manifests and VQA files into one per-sample JSON, with a fixed schema:
89
 
90
  ```
91
  { image_path, task, target, question, structured_findings, split, study_id, subject_id }
92
  ```
93
 
94
- Two knobs control sample creation:
95
 
96
- - **`report_mode`** controls the findings/impression decomposition. The experiments use **`split_cascade`**: findings is generated from the image alone, and impression is generated from the image **plus the ground-truth findings** as a textual context block. Studies without findings emit no impression sample.
97
- - **`image_mode`** controls how multiple views are handled. The experiments use **`frontal_only_split`** (one PA-or-AP image per study), which together with step (a) makes the image–study relationship one-to-one.
 
98
 
99
- | Mode | Behaviour |
100
- |---|---|
101
- | `split` | Two independent tasks; each sees only image + PNU string. |
102
- | `split_cascade` | As above, but impression conditions on the GT findings string (findings → impression summarisation). |
103
- | `merged` | One `report` task emitting both sections in a single pass. |
104
 
105
- The 14 CheXpert labels are formatted into the PNU string and written into `structured_findings` at build time, so the trainer loads no labels at runtime. In `split_cascade` the impression sample reuses this same field to carry `Findings: <GT findings>` instead of the PNU string.
106
 
107
- ![Hình 3.4. Cách một study sinh ra training sample dưới `split_cascade` + `frontal_only_split`.](figures/fig04_sample_creation.png)
108
 
109
  ---
110
 
@@ -112,16 +150,16 @@ The 14 CheXpert labels are formatted into the PNU string and written into `struc
112
 
113
  The model has four modules — image encoder, MLP projection, CheXpert classifier, and language model with LoRA — plus the prompt format that connects them. Each module is described below by its objective, inputs, outputs, and core logic.
114
 
115
- ![Hình 3.5. Kiến trúc tổng quan của CXR-VLM (xanh = frozen, cam = trainable).](figures/fig01_pipeline.png)
116
 
117
  ### 3.4.1 Image Encoder
118
 
119
  - **Objective.** Turn a chest X-ray into a dense grid of patch features for the projection.
120
  - **Input.** A 518×518 image (RAD-DINO's native resolution, matching the offline resize target).
121
  - **Output.** Patch features `P ∈ R^{1369×768}`; the `[CLS]` token is discarded.
122
- - **Core logic.** The backbone is Microsoft's **RAD-DINO** (ViT-B/14), self-supervised on ≈ 840k chest X-rays. It is loaded from the HuggingFace hub and kept **entirely frozen**. RAD-DINO is preferred over the original RaDialog backbone BioViL-T because it ships as a standard HuggingFace model (no legacy library pinning Python < 3.11), is trained on roughly an order of magnitude more chest-X-ray data, and produces a patch grid dense enough to capture both global pathology and small focal abnormalities. Freezing is chosen for stability and memory: with the LLM held in 4-bit, gradient flow through an 86 M-parameter ViT would exhaust the activation budget of consumer GPUs.
123
 
124
- ![Hình 3.6. Kiến trúc RAD-DINO (ViT-B/14): ảnh 518×518 → 1369 patch → patch + position embedding → 12 khối Transformer Encoder (frozen) → [CLS] + patch token (768-d).](figures/fig05_patchify.png)
125
 
126
  ### 3.4.2 MLP Projection
127
 
@@ -140,16 +178,16 @@ $$
140
  \mathbf{V} = \mathbf{W}_2 \mathbf{H}^{(1)} \in \mathbb{R}^{32\times 4096}
141
  $$
142
 
143
- with `Q₀` a learnable parameter and `CrossAttn` an 8-head attention block. The number of visual tokens (32) follows RaDialog and sits in the empirical sweet spot reported by LLaVA and BLIP-2 — fewer loses spatial detail on small pathologies, more only inflates the LLM sequence length. The 1024-d intermediate `H⁽¹⁾` is the grounding signal of the ITC head; it sits *after* the GELU so the contrastive objective sees an already-nonlinear representation.
144
 
145
- ![Hình 3.7. Module MLP Projection: 32 query → cross-attention → MLP, với nhánh 1024-d cho ITC head.](figures/fig06_projection.png)
146
 
147
  ### 3.4.3 CheXpert Abnormality Classifier
148
 
149
  - **Objective.** Provide explicit abnormality cues to the prompt as a readable 3-class string instead of a logit vector.
150
  - **Input.** The global `[CLS]` embedding of RAD-DINO.
151
  - **Output.** A 14×3 logit tensor → one of {Positive, Negative, Uncertain} per pathology → the PNU string.
152
- - **Core logic.** A small MLP head on the frozen `[CLS]` embedding, in the U-MultiClass style of META-CXR. The string injected into the prompt looks like:
153
 
154
  ```
155
  Positive Abnormalities: Cardiomegaly, Pleural Effusion
@@ -157,18 +195,18 @@ Negative Abnormalities: No Finding, Edema, Pneumothorax, ...
157
  Uncertain Abnormalities: Atelectasis
158
  ```
159
 
160
- Three properties motivate this design. (1) U-MultiClass preserves the negative-vs-uncertain distinction, which binary CheXpert mappings destroy. (2) Expressing labels as text needs no architectural change when labels are missing — the field simply becomes empty and the prompt degrades gracefully. (3) Placing the PNU string between the visual tokens and the instruction lets self-attention route freely between text and image. The classifier is trained separately in Stage 0 and then frozen. During VLM training the ground-truth CSV labels populate the PNU string (oracle setting); at evaluation the classifier predicts its own PNU.
161
 
162
- ![Hình 3.8. Từ embedding [CLS] của RAD-DINO tới PNU string đưa vào prompt.](figures/fig07_chexpert_pnu.png)
163
 
164
  ### 3.4.4 Language Model and Parameter-Efficient Adaptation
165
 
166
  - **Objective.** Generate the findings / impression / answer text conditioned on visual tokens and the prompt.
167
  - **Input.** The assembled token sequence with the `<image>` placeholder replaced by 32 visual tokens.
168
  - **Output.** The autoregressive text response.
169
- - **Core logic.** The decoder is **Vicuna-7B v1.3**, chosen to match the RaDialog baseline and for its clean `USER: … ASSISTANT: …` template (which simplifies label masking). It is loaded in **4-bit NF4 quantisation** (double-quant, compute dtype BF16 on Ampere+ / FP16 on Turing), bringing the resident footprint from ≈ 14 GB down to ≈ 4 GB. Adaptation uses **LoRA**: rank-16 adapters on the four attention projections (`q_proj`, `k_proj`, `v_proj`, `o_proj`) of every block; the feed-forward sublayers are left untouched. With `lora_alpha = 32` and `lora_dropout = 0.05`, the effective scaling is `α/r = 2`.
170
 
171
- ![Hình 3.9. Kiến trúc Vicuna-7B (decoder ×32, frozen 4-bit): masked self-attention + feed-forward MLP; LoRA (A→B, r=16) chèn vào q/k/v/o là phần trainable duy nhất.](figures/fig08_lora.png)
172
 
173
  ### 3.4.5 Prompt Assembly
174
 
@@ -181,25 +219,25 @@ All three tasks share one prompt skeleton, following Vicuna's v1.1 chat template
181
  {instruction} ASSISTANT: {target}
182
  ```
183
 
184
- The `<image>` placeholder is a special token (id 32000). At forward time the model finds this single token, replaces its embedding with the 32 visual tokens, and **expands the attention mask, position ids, and label tensor by 31 positions** so the causal mask stays consistent. Visual-token positions in the label tensor are set to **−100** so they are excluded from the loss.
185
 
186
- The task-specific context block is the main difference between `split` and `split_cascade`:
187
 
188
- - **Findings** — block empty; produce the findings from image + PNU labels.
189
- - **Impression** — block is the literal `Findings: <GT findings>`; the model conditions on the ground-truth findings and summarises it. (In the code this string is carried through the same `structured_findings` field, replacing the PNU string for the impression sample.)
190
- - **VQA** — block empty; the question becomes the instruction.
191
 
192
- Each of findings, impression, and the merged report has ten hand-written instruction paraphrases sampled at training time; at evaluation the first variant is used deterministically. A sample is tokenised with `cutoff_len = 512` and **right-truncation** (the response sits at the right end; left-truncation would destroy the system prompt and PNU block). The label tensor is masked with −100 on every prompt, padding, and visual token, so loss is computed strictly on the assistant response.
193
 
194
- ![Hình 3.10. Cấu trúc prompt và cơ chế mở rộng token `<image>` thành 32 visual token.](figures/fig09_prompt.png)
195
 
196
  ---
197
 
198
  ## 3.5 Training Strategy
199
 
200
- Training follows a two-stage curriculum modelled on RaDialog, with Stage 1 reformulated as explicit image–text contrastive alignment. The split follows the classic representation-then-instruction division: it is wasteful to drive the LoRA adapters while the projection still emits ill-conditioned visual tokens, and the projection cannot be trained efficiently against the LM loss without paying for a full Vicuna forward at every step. A Stage 0 classifier training precedes both.
201
 
202
- ![Hình 3.11. Lịch huấn luyện hai stage (kèm Stage 0 classifier).](figures/fig10_curriculum.png)
203
 
204
  ### 3.5.1 Stage 0 — CheXpert Classifier Head
205
 
@@ -210,7 +248,7 @@ The PNU classifier is fitted before Stages 1 and 2. It is a small MLP on the fro
210
  The goal is to specialise the projection (and only the projection) so its visual tokens are linearly aligned with the text representation of the matching report, before any language modelling.
211
 
212
  - **Image side.** The 32 intermediate 1024-d tokens are mean-pooled, projected to 128-d, and L2-normalised by the ITC head.
213
- - **Text side.** The canonical reference sentence per study (findings, falling back to impression) is encoded **once, offline** with `microsoft/BiomedVLP-CXR-BERT-specialized` into a 128-d L2-normalised vector. These are cached as `{study_id → tensor[128]}` and published to the data repo so any host can pull them in seconds.
214
 
215
  Stage 1 minimises the symmetric InfoNCE loss:
216
 
@@ -218,11 +256,11 @@ $$
218
  \mathcal{L}_{\text{ITC}} = -\tfrac{1}{2}\Big[ \sum_{i}\log\frac{\exp(\mathbf{v}_i^\top\mathbf{t}_i/\tau)}{\sum_j \exp(\mathbf{v}_i^\top\mathbf{t}_j/\tau)} + \sum_{i}\log\frac{\exp(\mathbf{t}_i^\top\mathbf{v}_i/\tau)}{\sum_j \exp(\mathbf{t}_i^\top\mathbf{v}_j/\tau)} \Big]
219
  $$
220
 
221
- **Where:** `vᵢ` is the image embedding from projection + ITC head, `tᵢ` the cached text embedding for the same study, and `τ = 0.07` the temperature (following CLIP / CXR-BERT).
222
 
223
- The dataset is de-duplicated to one image per `study_id` (the text embedding is study-level). Crucially, Stage 1 loads the model with **`load_llm = False`** — Vicuna is simply not instantiated. Freeing the ≈ 13 GB of Vicuna weights lifts the per-device batch from 8 (Stage 2 budget) to 64–96, which directly enlarges the InfoNCE negative pool. Stage 1 runs for 2 epochs at peak LR `1e-3` with a 5% cosine warm-up; the saved checkpoint is the **projection-only** state dict (the ITC head is discarded, as it has no role at generation time).
224
 
225
- ![Hình 3.12. Căn chỉnh ảnh–văn bản ở Stage 1 (text embedding precompute offline, loss InfoNCE).](figures/fig11_contrastive.png)
226
 
227
  ### 3.5.3 Stage 2 — Instruction Tuning
228
 
@@ -238,15 +276,15 @@ Trainable parameters are the projection's MLP and the LoRA adapters; the encoder
238
 
239
  ### 3.5.4 Loss Masking and Image-Token Accounting
240
 
241
- The bookkeeping around the `<image>` placeholder deserves explicit mention. The tokenised prompt contains exactly **one** `<image>` token, replaced by 32 visual tokens at forward time. To keep the attention mask, position ids, and labels consistent, the forward pass expands all three by 31 entries at the placeholder: each visual-token mask entry is set to 1, position ids are made contiguous, and the visual span in the labels is filled with −100. The same expansion is applied at inference. This is the most error-prone part of the pipeline — an off-by-one silently shifts the labels and produces a degenerate loss curve — so an integration test asserts that the count of non-−100 label entries is preserved before and after expansion.
242
 
243
- ![Hình 3.13. Masking nhãn — chỉ vùng câu trả lời (response) đóng góp vào loss.](figures/fig12_loss_mask.png)
244
 
245
  ---
246
 
247
  ## 3.6 Evaluation Protocol
248
 
249
- Evaluation reflects the three downstream tasks. For findings and impression we report **NLG metrics** (lexical, fluency, semantic) plus the **clinical-accuracy** metric standard in chest-X-ray report generation. For VQA we report a short-answer suite plus an optional **LLM-as-judge** evaluation.
250
 
251
  | Family | Metric | Tasks |
252
  |---|---|---|
@@ -256,26 +294,25 @@ Evaluation reflects the three downstream tasks. For findings and impression we r
256
  | Semantic embedding | BERTScore F1 | findings, impression, VQA |
257
  | Clinical accuracy | CheXbert macro-F1, P, R | findings, impression |
258
  | Exact answer | Exact match, token F1 | VQA |
259
- | Judge model | GPT-4o-mini score (0–5) | VQA (optional) |
260
 
261
  ### 3.6.1 Tasks and Held-Out Data
262
 
263
- All metrics are computed on the patient-disjoint **test split** (5,000 studies). Inference uses greedy decoding (`do_sample=False`, `num_beams=1`) and the canonical instruction variant (index 0). For findings and impression the model receives the test image plus the PNU string **predicted** by the frozen classifier; the impression prompt additionally receives the ground-truth findings, preserving `split_cascade` consistency. For VQA the question replaces the instruction slot. Maximum new tokens: 300 (findings), 200 (impression), 64 (VQA), matched to the 99th-percentile reference length.
264
 
265
- ![Hình 3.14. Luồng đánh giá theo từng task.](figures/fig13_eval_flow.png)
266
 
267
  ### 3.6.2 Natural Language Generation Metrics
268
 
269
  - **BLEU.** Corpus-level BLEU-1 and BLEU-4 with NLTK smoothing method 1. Reported for comparability with the RRG literature, but treated as a fluency floor — it correlates weakly with clinical correctness.
270
- - **ROUGE.** ROUGE-1/2/L F-measures with Porter stemming. ROUGE-L is the most commonly reported single number in prior work (R2Gen, KGAE, RaDialog).
271
  - **METEOR.** A weighted token-level F-measure that credits stems and WordNet synonyms (e.g. *cardiomegaly* ↔ *enlarged heart*), with a fragmentation penalty. Of the n-gram-style metrics it correlates best with human judgement on radiology.
272
  - **BERTScore.** Greedy-aligned cosine similarity between contextual embeddings, aggregated to F1. Captures semantic equivalence the n-gram metrics miss, but has no notion of clinical correctness — it can reward a paraphrase that flips a finding's polarity.
273
 
274
  ### 3.6.3 Clinical Correctness: CheXbert F1
275
 
276
- CheXbert is a BERT-based labeler mapping a free-text report onto the 14 CheXpert categories. Running it on both the generated and reference reports and comparing the label vectors gives a **factual** correctness measure invariant to paraphrasing. This **Clinical F1** is the primary clinical-accuracy metric used by RaDialog, CheXagent, MAIRA-2 and most RRG work. We report macro-averaged F1, precision, and recall over the 14 pathologies, binarising the labeler's {−1, 0, 1} output by collapsing −1 and 0. When the CheXbert weights are unavailable on a host, the metric degrades gracefully to 0.0 with a warning rather than failing the run.
277
 
278
- ![Hình 3.15. Tính Clinical F1 bằng CheXbert trên report sinh ra và report tham chiếu.](figures/fig14_chexbert_f1.png)
279
 
280
  ### 3.6.4 Visual Question Answering Metrics
281
 
@@ -284,8 +321,7 @@ VQA targets are short, so the suite differs from generation.
284
  - **Exact match.** Lower-cased, punctuation-stripped, whitespace-collapsed string equality — the lower bound on correctness; harsh on phrasing but rewards the closed-form yes/no and quantitative questions that dominate the dataset.
285
  - **Token F1.** F1 between the bags of normalised tokens — the most diagnostic single number for short-answer correctness.
286
  - **BLEU-1, METEOR, BERTScore.** Reported for symmetry with the generation tasks. BLEU-4 and ROUGE-L are omitted (rarely meaningful / subsumed by token F1).
287
- - **LLM-as-judge (optional).** For free-form answers where the above misalign with clinical equivalence (e.g. *cardiomegaly* vs *enlarged cardiac silhouette*), GPT-4o-mini is prompted with the question, reference, and prediction and returns an integer 0–5 plus a rationale (G-Eval rubric). We report the mean and its [0, 1] normalisation. The judge runs at temperature 0 with `response_format=json_object`; at most 500 VQA items are sampled to bound cost.
288
 
289
  ### 3.6.5 Model Selection and Reporting
290
 
291
- Model selection uses `eval_loss` on the validation split (the same causal cross-entropy minimised in training); the best checkpoint by this criterion is evaluated on the test split. We deliberately do **not** select on downstream metrics, to avoid the optimistic bias of optimising the reported signal — and CheXbert F1 in particular is expensive and unstable on small validation samples. Each run writes per-task predictions to `results/{run_id}/predictions_{task}.json` and an aggregated summary to `results/{run_id}/metrics_summary.json`.
 
1
  # 3. Materials and Methods
2
 
3
+ This chapter describes how the proposed Vision–Language Model (CXR-VLM) is built and trained. It is organised in six parts. Section 3.1 gives an end-to-end overview. Section 3.2 defines the key concepts used throughout the chapter. Section 3.3 documents how the MIMIC-CXR corpus is analysed, filtered, and serialised into the training format. Section 3.4 describes the four modules of the model and the prompt that ties them together. Section 3.5 specifies the two-stage training schedule. Section 3.6 defines the evaluation protocol.
4
 
5
  ---
6
 
7
  ## 3.1 Overview
8
 
9
+ CXR-VLM is a single vision–language network that solves three downstream tasks — findings generation, impression generation, and visual question answering (VQA) — through one shared backbone. It couples a frozen image encoder, a trainable projection, and a LoRA-adapted language model, with two design choices aimed at clinical reliability and at fitting a limited compute budget:
10
 
11
+ - a frozen 14-pathology CheXpert-style classifier whose predictions are serialised into the prompt as a Positive / Negative / Uncertain (PNU) string, and
12
  - an optional contrastive Stage 1 that pre-aligns the projection in a joint image–text space without ever loading the language model.
13
 
14
  The full forward path can be summarised as:
 
17
  \mathbf{x}_{518\times518} \xrightarrow{\text{RAD-DINO}} \mathbf{P} \in \mathbb{R}^{B\times 1369\times 768} \xrightarrow{\text{MLP-Proj}} \mathbf{V} \in \mathbb{R}^{B\times 32\times 4096} \xrightarrow{\text{Vicuna-7B + LoRA}} \hat{\mathbf{y}}
18
  $$
19
 
20
+ Only the MLP projection, the LoRA adapters on Vicuna, and (when enabled) the ITC head are trained. The image encoder, the CheXpert classifier, and the Vicuna base weights are kept frozen. In total, fewer than 0.3% of the parameters are trainable (≈ 21.5 M of ≈ 7.1 B).
21
 
22
  ![Hình 3.1. Sơ đồ tổng thể quy trình của CXR-VLM: từ dữ liệu, huấn luyện hai stage, tới suy luận và đánh giá.](figures/fig15_workflow.png)
23
 
 
29
 
30
  - **Vision–Language Model (VLM).** A model that takes an image together with a text instruction and produces a text response, by mapping visual features into the embedding space of a language model.
31
 
32
+ - **Chest X-ray report.** A radiology report has two main free-text sections. Findings is the detailed, observation-by-observation description of the image; Impression is the short clinical summary written *after* the findings. We treat their generation as two separate tasks.
33
 
34
  - **Visual Question Answering (VQA).** Given an image and a natural-language question, the model returns a short answer (often a single word or phrase).
35
 
36
+ - **Large Language Model (LLM).** A transformer trained on large text corpora to generate human-like text. We use Vicuna-7B, a LLaMA-derived instruction-tuned chat model, as the decoder.
37
 
38
+ - **Vision Transformer (ViT) and self-supervision.** A ViT splits an image into fixed-size patches and processes them as a token sequence. RAD-DINO is a ViT-B/14 trained with DINOv2 self-supervision on chest X-rays; it requires no text labels.
39
 
40
+ - **Parameter-efficient fine-tuning (LoRA / QLoRA).** Instead of updating all weights of the LLM, LoRA inserts small trainable low-rank matrices into selected layers and freezes the rest. QLoRA additionally keeps the base weights in 4-bit quantisation, which drastically reduces memory.
41
 
42
  - **Image–Text Contrastive learning (ITC / InfoNCE).** A training objective that pulls the embedding of an image and its matching text together while pushing non-matching pairs apart, using the symmetric InfoNCE loss. It is the mechanism behind Stage 1 alignment.
43
 
44
+ - **CheXpert labels and U-MultiClass.** CheXpert defines 14 pathology categories. Rather than a binary present/absent label, U-MultiClass keeps three states per pathology — Positive, Negative, Uncertain — preserving the clinically important difference between a confident negative and a hedged one.
45
 
46
  ---
47
 
48
  ## 3.3 Data Preparation
49
 
50
+ The data pipeline runs in three phases, each placed where it is most efficient. (i) Selection runs locally on the MIMIC-CXR CSV metadata to decide which studies to keep. (ii) Image download runs on a cloud virtual machine (Google Colab): the images chosen in the manifest are downloaded from PhysioNet onto the VM's disk and then pushed to a Hugging Face repository for reuse. This step is done in the cloud rather than on a local machine because the selected image set is too large for the local disk, and the cloud VM also has a faster, more stable connection to PhysioNet. (iii) Resize and re-shard runs once on a GPU host so that every training run consumes minimal-size JPEGs. The same subset is reused across all experiments, so the pipeline is run once — changing prompt templates or task weights only rebuilds the JSON, not the images.
51
 
52
  ![Hình 3.2. Pipeline dữ liệu 3 phase: selection (local) → image download (VM cloud → Hugging Face) → resize/shard → unified JSON.](figures/fig02_data_pipeline.png)
53
 
54
+ ### 3.3.1 Sources and dataset overview
55
 
56
+ All data come from the public PhysioNet distribution under credentialed access; no manual annotation is performed.
57
 
58
  | Source | Version | Used for |
59
  |---|---|---|
 
61
  | MIMIC-CXR-JPG | 2.1.0 | Pre-converted JPEG images |
62
  | MIMIC-Ext-CXR-VQA | 1.0.0 | (image, question, answer) triples |
63
  | `mimic-cxr-2.0.0-split` | — | Official patient-disjoint train/validate/test split |
64
+ | `mimic-cxr-2.0.0-metadata` | — | `ViewPosition` (used for frontal selection) |
65
  | `mimic-cxr-2.0.0-chexpert` | — | 14 pathology labels per study |
66
 
67
+ An exploratory analysis of the full corpus motivates the design choices below. MIMIC-CXR contains 377,110 images from 227,835 studies of 65,379 patients (on average 1.66 images per study and 3.48 studies per patient).
68
 
69
+ - **Label imbalance (Hình 3.3).** The 14 CheXpert labels are highly imbalanced: *No Finding* (33% positive), *Support Devices* (29%), *Pleural Effusion* (24%), and *Lung Opacity* (23%) dominate, while *Fracture* (1.9%) and *Pleural Other* (0.9%) are rare — which directly affects the abnormality classifier in Section 4.2.2.
70
+ - **Views (Hình 3.4).** Each study can contain several views. Frontal projections — AP (147,173) and PA (96,161) — together make up the majority; the rest are lateral (LATERAL 82,853, LL 35,133) or have no recorded `ViewPosition` (15,769).
71
+ - **Images per study (Hình 3.5).** Many studies contain two or more images (mean 1.66, up to 11), even though they share a single report.
72
+ - **Report sections (Hình 3.6).** Parsing all reports, a clean Findings section is present in 149,060 studies (65%) and an Impression in 186,865 (82%); many reports therefore lack one section. Findings are longer (median ≈ 45 words) than Impression (median ≈ 16 words).
73
+ - **VQA (Hình 3.7).** MIMIC-Ext-CXR-VQA provides 377,391 (image, question, answer) triples over these images, organised by semantic type (verify / choose / query) and content type (presence, anatomy, attribute, size, …).
74
 
75
+ ![Hình 3.3. Phân bố 14 nhãn CheXpert (Positive / Uncertain / Negative) trên toàn bộ MIMIC-CXR mất cân bằng mạnh: No Finding Support Devices áp đảo, Fracture Pleural Other rất hiếm.](figures/eda_chexpert_labels.png)
 
 
 
76
 
77
+ ![Hình 3.4. Phân bố View Position: hai view frontal (AP, PA) chiếm đa số; phần còn lại là lateral (LATERAL, LL).](figures/eda_views.png)
78
 
79
+ ![Hình 3.5. Số ảnh mỗi study (trung bình 1.66) rất nhiều study có từ hai ảnh trở lên dù chỉ có một report.](figures/eda_imgs_per_study.png)
80
+
81
+ ![Hình 3.6. Phân bố độ dài (số từ) của Findings và Impression: Impression ngắn hơn nhiều (median ≈ 16 từ so với ≈ 45 từ của Findings).](figures/eda_report_length.png)
82
+
83
+ ![Hình 3.7. Phân bố câu hỏi VQA theo semantic type và content type.](figures/eda_vqa_types.png)
84
+
85
+ ### 3.3.2 Selecting the working subset
86
+
87
+ We do not train on the full corpus. With no dedicated GPU available, training is done in the cloud, where compute is billed by the hour; repeatedly processing all ≈ 227k studies (≈ 377k images) would be both slow and expensive. A subset of 50,000 studies (40,000 train / 5,000 validation / 5,000 test) is large enough to train and evaluate the model while keeping cloud time and cost manageable, and — as Table 3.1 shows — it preserves the pathology distribution of the full dataset, so results on it remain representative.
88
+
89
+ A four-stage filter chain produces this subset:
90
+
91
+ - **(a) One frontal image per study.** Each DICOM is joined with the metadata CSV and only frontal views (`ViewPosition ∈ {PA, AP}`) are kept; if a study has several, it is collapsed to one image, preferring PA over AP. We use a single frontal image per study for two reasons. First, the frontal projection carries the most diagnostic information, and PA is the standard reference view (AP is reserved for bedside/portable exams), so PA is preferred when both exist. Second, we deliberately avoid multi-view training: besides its higher per-study compute, it is noisy here — a study has a single report that does not state which finding belongs to which view, so pairing several images with one report can mislead the model. Multi-view training is left as future work (Section 5).
92
+ - **(b) Both report sections present.** A strict regex accepts a section only if its header is exactly `FINDINGS` or `IMPRESSION`; synonyms (`CONCLUSION`, `WET READ`, composite headers) are not merged. A study survives only if both sections are present and non-empty, because the impression task later conditions on the ground-truth findings (Section 3.4.5).
93
+ - **(c) Length-based outlier removal.** Per-section word counts are computed; studies above `Q3 + 1.5·IQR` (multi-paragraph teaching reports) or below a small floor are dropped, trimming the long tail without shifting the median.
94
+ - **(d) Stratified patient-disjoint sampling.** Each study is assigned a stratum equal to its rarest positive CheXpert label; target counts are allocated per stratum proportionally to prevalence. The validation/test pools are filled first from the official split, any overflow drawn from train with the affected subjects removed from train, so the three sets are patient-disjoint.
95
+
96
+ After steps (a)–(c) the eligible pool is 108,783 studies (from the 227,835 total), and step (d) samples the final 50,000.
97
+
98
+ ![Hình 3.8. Chuỗi lọc 4 tầng rút gọn corpus MIMIC-CXR còn 50,000 study.](figures/fig03_selection_funnel.png)
99
+
100
+ **Distribution preserved after selection.** To confirm the subset is representative, Table 3.1 and Hình 3.9 compare the per-pathology positive rate of the full dataset, the eligible pool, and the final subset. The subset tracks the eligible pool almost exactly (largest gap 1.25 pp, on *No Finding*). The eligible pool itself differs from the raw corpus — mainly a higher *No Finding* rate and lower *Support Devices* / *Pleural Effusion* — which is expected, since requiring a clean Findings + Impression section and a frontal view removes many device-heavy ICU portables.
101
+
102
+ **Table 3.1.** CheXpert positive rate (%) at three stages of selection (largest 8 labels shown; |Δ| = subset − eligible).
103
+
104
+ | Pathology | Full (%) | Eligible (%) | Subset (%) | \|Δ\| |
105
+ |---|---|---|---|---|
106
+ | No Finding | 33.1 | 51.6 | 52.8 | 1.25 |
107
+ | Support Devices | 29.2 | 8.7 | 8.5 | 0.24 |
108
+ | Pleural Effusion | 23.8 | 12.3 | 11.6 | 0.65 |
109
+ | Lung Opacity | 22.6 | 14.9 | 14.5 | 0.34 |
110
+ | Atelectasis | 20.1 | 11.0 | 10.7 | 0.29 |
111
+ | Cardiomegaly | 19.7 | 6.6 | 6.3 | 0.30 |
112
+ | Edema | 11.9 | 8.0 | 7.6 | 0.46 |
113
+ | Pneumonia | 7.3 | 5.6 | 5.6 | 0.01 |
114
+
115
+ ![Hình 3.9. Tỉ lệ positive của 14 nhãn CheXpert ở ba mức — full dataset, eligible pool, và subset 50k — subset bám sát phân phối gốc.](figures/eda_prevalence_compare.png)
116
+
117
+ The VQA distribution is likewise preserved: Hình 3.10 shows that the shares of question semantic types, content types, and answer types in the subset closely match the full dataset, so the VQA evaluation on the subset is not biased towards any question category.
118
+
119
+ ![Hình 3.10. Phân bố câu hỏi VQA (semantic type, content type, answer type) — full dataset vs subset 50k: subset bám sát phân phối gốc.](figures/eda_vqa_compare.png)
120
 
121
  ### 3.3.3 Storage and Consumption Layout
122
 
123
  For each split a manifest is emitted as JSON and CSV. Every row captures one image plus the standard PhysioNet identifiers, the 14 `chex_*` U-MultiClass label columns, and a `has_vqa` flag. The PhysioNet directory layout (`files/pXX/pSUBJ/sSTUDY/<dicom>.jpg`) is preserved so a partial download can be checked against the manifest by path lookup.
124
 
125
+ Because all training hosts consume the same images, we resize once offline. RAD-DINO centre-crops to 518×518 regardless of input, so the full-resolution JPEGs (~2–3 MP each, ~100 GB) are wasteful. Each image is resized so the shortest edge is 518, saved as JPEG quality 90, and packed into ~2 GB tar shards; the 50k subset compresses to ~5–8 GB.
126
 
127
+ ### 3.3.4 Training data format
128
 
129
+ On the training host, a single builder turns the resized images, the manifests, and the VQA files into one JSON file — one entry per training sample with a fixed schema:
130
 
131
  ```
132
  { image_path, task, target, question, structured_findings, split, study_id, subject_id }
133
  ```
134
 
135
+ Each selected study (one frontal image) yields up to three kinds of sample:
136
 
137
+ - a findings sample target is the Findings paragraph, generated from the image and the abnormality (PNU) string;
138
+ - an impression sample target is the Impression, generated from the image plus the study's ground-truth Findings as context. The impression is thus produced as a short summary of the findings (a findings impression cascade), which mirrors clinical practice, where the impression is written after the findings; this is also why step (b) requires a clean Findings section;
139
+ - **VQA** samples (when the study has associated questions) — target is the answer, with the question itself acting as the instruction.
140
 
141
+ The 14 CheXpert labels are formatted into the PNU string (Section 3.4.3) and written into the `structured_findings` field at build time, so the trainer loads no labels at runtime. For the impression sample, this same field instead carries the ground-truth Findings text.
 
 
 
 
142
 
143
+ Because there is exactly one frontal image per study, the findings and impression tasks each contribute one sample per study (about 50,000 samples each). VQA, however, is larger 127,010 question–answer samples in total (102,941 train / 12,189 validation / 11,880 test) because a single image can be paired with several different questions in MIMIC-Ext-CXR-VQA.
144
 
145
+ ![Hình 3.11. Một study (một ảnh frontal) sinh ra các training sample: findings (ảnh → findings), impression (ảnh + GT findings → impression), và VQA.](figures/fig04_sample_creation.png)
146
 
147
  ---
148
 
 
150
 
151
  The model has four modules — image encoder, MLP projection, CheXpert classifier, and language model with LoRA — plus the prompt format that connects them. Each module is described below by its objective, inputs, outputs, and core logic.
152
 
153
+ ![Hình 3.12. Kiến trúc tổng quan của CXR-VLM (xanh = frozen, cam = trainable).](figures/fig01_pipeline.png)
154
 
155
  ### 3.4.1 Image Encoder
156
 
157
  - **Objective.** Turn a chest X-ray into a dense grid of patch features for the projection.
158
  - **Input.** A 518×518 image (RAD-DINO's native resolution, matching the offline resize target).
159
  - **Output.** Patch features `P ∈ R^{1369×768}`; the `[CLS]` token is discarded.
160
+ - **Core logic.** The backbone is Microsoft's RAD-DINO (ViT-B/14), self-supervised on ≈ 840k chest X-rays. It is loaded from the HuggingFace hub and kept entirely frozen. RAD-DINO is chosen because it ships as a standard HuggingFace model, is trained on a large amount of chest-X-ray data, and produces a patch grid dense enough to capture both global pathology and small focal abnormalities. Freezing is chosen for stability and memory: with the LLM held in 4-bit, gradient flow through an 86 M-parameter ViT would exhaust the activation budget of the GPUs available for this work.
161
 
162
+ ![Hình 3.13. Kiến trúc RAD-DINO (ViT-B/14): ảnh 518×518 → 1369 patch → patch + position embedding → 12 khối Transformer Encoder (frozen) → [CLS] + patch token (768-d).](figures/fig05_patchify.png)
163
 
164
  ### 3.4.2 MLP Projection
165
 
 
178
  \mathbf{V} = \mathbf{W}_2 \mathbf{H}^{(1)} \in \mathbb{R}^{32\times 4096}
179
  $$
180
 
181
+ with `Q₀` a learnable parameter and `CrossAttn` an 8-head attention block. We use 32 visual tokens: enough to retain spatial detail on small pathologies without inflating the LLM's sequence length. The 1024-d intermediate `H⁽¹⁾` is the grounding signal of the ITC head; it sits *after* the GELU so the contrastive objective sees an already-nonlinear representation.
182
 
183
+ ![Hình 3.14. Module MLP Projection: 32 query → cross-attention → MLP (768 → 1024 → 4096), với nhánh 1024-d cho ITC head.](figures/fig06_projection.png)
184
 
185
  ### 3.4.3 CheXpert Abnormality Classifier
186
 
187
  - **Objective.** Provide explicit abnormality cues to the prompt as a readable 3-class string instead of a logit vector.
188
  - **Input.** The global `[CLS]` embedding of RAD-DINO.
189
  - **Output.** A 14×3 logit tensor → one of {Positive, Negative, Uncertain} per pathology → the PNU string.
190
+ - **Core logic.** A small MLP head on the frozen `[CLS]` embedding, predicting three states (positive / negative / uncertain) per pathology. The string injected into the prompt looks like:
191
 
192
  ```
193
  Positive Abnormalities: Cardiomegaly, Pleural Effusion
 
195
  Uncertain Abnormalities: Atelectasis
196
  ```
197
 
198
+ Three properties motivate this design. (1) The three-state form preserves the negative-vs-uncertain distinction, which binary CheXpert mappings destroy. (2) Expressing labels as text needs no architectural change when labels are missing — the field simply becomes empty and the prompt degrades gracefully. (3) Placing the PNU string between the visual tokens and the instruction lets self-attention route freely between text and image. The classifier is trained separately in Stage 0 and then frozen. During VLM training the ground-truth CSV labels populate the PNU string (oracle setting); at evaluation the classifier predicts its own PNU.
199
 
200
+ ![Hình 3.15. Từ embedding [CLS] của RAD-DINO tới PNU string đưa vào prompt.](figures/fig07_chexpert_pnu.png)
201
 
202
  ### 3.4.4 Language Model and Parameter-Efficient Adaptation
203
 
204
  - **Objective.** Generate the findings / impression / answer text conditioned on visual tokens and the prompt.
205
  - **Input.** The assembled token sequence with the `<image>` placeholder replaced by 32 visual tokens.
206
  - **Output.** The autoregressive text response.
207
+ - **Core logic.** The decoder is Vicuna-7B v1.3, chosen for its clean `USER: … ASSISTANT: …` chat template (which simplifies label masking) and for offering strong instruction-following at a 7B size that fits the available hardware budget. It is loaded in 4-bit NF4 quantisation (double-quant, compute dtype BF16 on Ampere+ / FP16 on Turing), bringing the resident footprint from ≈ 14 GB down to ≈ 4 GB. Adaptation uses LoRA: rank-16 adapters on the four attention projections (`q_proj`, `k_proj`, `v_proj`, `o_proj`) of every block; the feed-forward sublayers are left untouched. With `lora_alpha = 32` and `lora_dropout = 0.05`, the effective scaling is `α/r = 2`.
208
 
209
+ ![H��nh 3.16. Kiến trúc Vicuna-7B (decoder ×32, frozen 4-bit): masked self-attention + feed-forward MLP; LoRA (A→B, r=16) chèn vào q/k/v/o là phần trainable duy nhất.](figures/fig08_lora.png)
210
 
211
  ### 3.4.5 Prompt Assembly
212
 
 
219
  {instruction} ASSISTANT: {target}
220
  ```
221
 
222
+ The `<image>` placeholder is a special token (id 32000). At forward time the model finds this single token, replaces its embedding with the 32 visual tokens, and expands the attention mask, position ids, and label tensor by 31 positions so the causal mask stays consistent. Visual-token positions in the label tensor are set to −100 so they are excluded from the loss.
223
 
224
+ The task-specific context block differs per task:
225
 
226
+ - **Findings** — block empty; the findings are produced from the image plus the PNU labels.
227
+ - **Impression** — block is the literal `Findings: <ground-truth findings>`; the model conditions on the findings and summarises them.
228
+ - **VQA** — block empty; the question itself becomes the instruction.
229
 
230
+ Findings and impression each have ten hand-written instruction paraphrases, sampled at training time; at evaluation the first variant is used deterministically. A sample is tokenised with `cutoff_len = 512` and right-truncation (the response sits at the right end; left-truncation would destroy the system prompt and PNU block). The label tensor is masked with −100 on every prompt, padding, and visual token, so loss is computed strictly on the assistant response.
231
 
232
+ ![Hình 3.17. Cấu trúc prompt và cơ chế mở rộng token `<image>` thành 32 visual token.](figures/fig09_prompt.png)
233
 
234
  ---
235
 
236
  ## 3.5 Training Strategy
237
 
238
+ Training uses a two-stage curriculum. Stage 1 first aligns the projection with the report text through explicit image–text contrastive learning; Stage 2 then instruction-tunes the projection together with the LLM's LoRA adapters. The split follows a representation-then-instruction logic: it is wasteful to drive the LoRA adapters while the projection still emits ill-conditioned visual tokens, and the projection cannot be trained efficiently against the language-modelling loss without paying for a full Vicuna forward at every step. A Stage 0 classifier training precedes both.
239
 
240
+ ![Hình 3.18. Lịch huấn luyện hai stage (kèm Stage 0 classifier).](figures/fig10_curriculum.png)
241
 
242
  ### 3.5.1 Stage 0 — CheXpert Classifier Head
243
 
 
248
  The goal is to specialise the projection (and only the projection) so its visual tokens are linearly aligned with the text representation of the matching report, before any language modelling.
249
 
250
  - **Image side.** The 32 intermediate 1024-d tokens are mean-pooled, projected to 128-d, and L2-normalised by the ITC head.
251
+ - **Text side.** The canonical reference sentence per study (findings, falling back to impression) is encoded once, offline with `microsoft/BiomedVLP-CXR-BERT-specialized` into a 128-d L2-normalised vector. These are cached as `{study_id → tensor[128]}` and published to the data repo so any host can pull them in seconds.
252
 
253
  Stage 1 minimises the symmetric InfoNCE loss:
254
 
 
256
  \mathcal{L}_{\text{ITC}} = -\tfrac{1}{2}\Big[ \sum_{i}\log\frac{\exp(\mathbf{v}_i^\top\mathbf{t}_i/\tau)}{\sum_j \exp(\mathbf{v}_i^\top\mathbf{t}_j/\tau)} + \sum_{i}\log\frac{\exp(\mathbf{t}_i^\top\mathbf{v}_i/\tau)}{\sum_j \exp(\mathbf{t}_i^\top\mathbf{v}_j/\tau)} \Big]
257
  $$
258
 
259
+ **Where:** `vᵢ` is the image embedding from projection + ITC head, `tᵢ` the cached text embedding for the same study, and `τ = 0.07` the temperature.
260
 
261
+ The dataset is de-duplicated to one image per `study_id` (the text embedding is study-level). Crucially, Stage 1 loads the model with `load_llm = False` — Vicuna is simply not instantiated. Freeing the ≈ 13 GB of Vicuna weights lifts the per-device batch from 8 (Stage 2 budget) to 64–96, which directly enlarges the InfoNCE negative pool. Stage 1 runs for 2 epochs at peak LR `1e-3` with a 5% cosine warm-up; the saved checkpoint is the projection-only state dict (the ITC head is discarded, as it has no role at generation time).
262
 
263
+ ![Hình 3.19. Căn chỉnh ảnh–văn bản ở Stage 1 (text embedding precompute offline, loss InfoNCE).](figures/fig11_contrastive.png)
264
 
265
  ### 3.5.3 Stage 2 — Instruction Tuning
266
 
 
276
 
277
  ### 3.5.4 Loss Masking and Image-Token Accounting
278
 
279
+ The bookkeeping around the `<image>` placeholder deserves explicit mention. The tokenised prompt contains exactly one `<image>` token, replaced by 32 visual tokens at forward time. To keep the attention mask, position ids, and labels consistent, the forward pass expands all three by 31 entries at the placeholder: each visual-token mask entry is set to 1, position ids are made contiguous, and the visual span in the labels is filled with −100. The same expansion is applied at inference. This is the most error-prone part of the pipeline — an off-by-one silently shifts the labels and produces a degenerate loss curve — so an integration test asserts that the count of non-−100 label entries is preserved before and after expansion.
280
 
281
+ ![Hình 3.20. Masking nhãn — chỉ vùng câu trả lời (response) đóng góp vào loss.](figures/fig12_loss_mask.png)
282
 
283
  ---
284
 
285
  ## 3.6 Evaluation Protocol
286
 
287
+ Evaluation reflects the three downstream tasks. For findings and impression we report NLG metrics (lexical, fluency, semantic) plus the clinical-accuracy metric standard in chest-X-ray report generation. For VQA we report a short-answer suite.
288
 
289
  | Family | Metric | Tasks |
290
  |---|---|---|
 
294
  | Semantic embedding | BERTScore F1 | findings, impression, VQA |
295
  | Clinical accuracy | CheXbert macro-F1, P, R | findings, impression |
296
  | Exact answer | Exact match, token F1 | VQA |
 
297
 
298
  ### 3.6.1 Tasks and Held-Out Data
299
 
300
+ All metrics are computed on the patient-disjoint test split (5,000 studies). Inference uses greedy decoding (`do_sample=False`, `num_beams=1`) and the canonical instruction variant (index 0). For findings and impression the model receives the test image plus the PNU string predicted by the frozen classifier; the impression prompt additionally receives the ground-truth findings, matching the training setup. For VQA the question replaces the instruction slot. Maximum new tokens: 300 (findings), 200 (impression), 64 (VQA), matched to the 99th-percentile reference length.
301
 
302
+ ![Hình 3.21. Luồng đánh giá theo từng task.](figures/fig13_eval_flow.png)
303
 
304
  ### 3.6.2 Natural Language Generation Metrics
305
 
306
  - **BLEU.** Corpus-level BLEU-1 and BLEU-4 with NLTK smoothing method 1. Reported for comparability with the RRG literature, but treated as a fluency floor — it correlates weakly with clinical correctness.
307
+ - **ROUGE.** ROUGE-1/2/L F-measures with Porter stemming. ROUGE-L is the most commonly reported single number for this task.
308
  - **METEOR.** A weighted token-level F-measure that credits stems and WordNet synonyms (e.g. *cardiomegaly* ↔ *enlarged heart*), with a fragmentation penalty. Of the n-gram-style metrics it correlates best with human judgement on radiology.
309
  - **BERTScore.** Greedy-aligned cosine similarity between contextual embeddings, aggregated to F1. Captures semantic equivalence the n-gram metrics miss, but has no notion of clinical correctness — it can reward a paraphrase that flips a finding's polarity.
310
 
311
  ### 3.6.3 Clinical Correctness: CheXbert F1
312
 
313
+ CheXbert is a BERT-based labeler mapping a free-text report onto the 14 CheXpert categories. Running it on both the generated and reference reports and comparing the label vectors gives a factual correctness measure invariant to paraphrasing. This Clinical F1 is the de-facto standard clinical-accuracy metric for chest-X-ray report generation. We report macro-averaged F1, precision, and recall over the 14 pathologies, binarising the labeler's {−1, 0, 1} output by collapsing −1 and 0. When the CheXbert weights are unavailable on a host, the metric degrades gracefully to 0.0 with a warning rather than failing the run.
314
 
315
+ ![Hình 3.22. Tính Clinical F1 bằng CheXbert trên report sinh ra và report tham chiếu.](figures/fig14_chexbert_f1.png)
316
 
317
  ### 3.6.4 Visual Question Answering Metrics
318
 
 
321
  - **Exact match.** Lower-cased, punctuation-stripped, whitespace-collapsed string equality — the lower bound on correctness; harsh on phrasing but rewards the closed-form yes/no and quantitative questions that dominate the dataset.
322
  - **Token F1.** F1 between the bags of normalised tokens — the most diagnostic single number for short-answer correctness.
323
  - **BLEU-1, METEOR, BERTScore.** Reported for symmetry with the generation tasks. BLEU-4 and ROUGE-L are omitted (rarely meaningful / subsumed by token F1).
 
324
 
325
  ### 3.6.5 Model Selection and Reporting
326
 
327
+ Model selection uses `eval_loss` on the validation split (the same causal cross-entropy minimised in training); the best checkpoint by this criterion is evaluated on the test split. We deliberately do not select on downstream metrics, to avoid the optimistic bias of optimising the reported signal — and CheXbert F1 in particular is expensive and unstable on small validation samples. Each run writes per-task predictions to `results/{run_id}/predictions_{task}.json` and an aggregated summary to `results/{run_id}/metrics_summary.json`.
docs/report_front.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Acknowledgements
2
+
3
+ This thesis would not have been possible without the help of many people. My deepest thanks go to my supervisor, [Supervisor name], whose questions, patience, and detailed feedback shaped both the direction of this project and the way I approached it. I am grateful to the lecturers of the Department of Information and Communication Technology at the University of Science and Technology of Hanoi for the foundation they gave me over the past years. Finally, I thank my family and friends, who kept me motivated through the long nights of training runs and debugging that this work demanded.
4
+
5
+ > *(Điền tên giảng viên hướng dẫn vào `[Supervisor name]`; thêm internal/external supervisor nếu cần.)*
6
+
7
+ ---
8
+
9
+ # Abstract
10
+
11
+ This thesis presents CXR-VLM, a vision–language model for chest X-ray interpretation that performs findings generation, impression generation, and visual question answering with a single shared backbone. The model couples a frozen RAD-DINO image encoder with a trainable projection and a Vicuna-7B language model adapted through low-rank (LoRA) adapters, and is explicitly guided by a 14-pathology Positive / Negative / Uncertain (PNU) abnormality signal produced by a CheXpert-style classifier. It is trained with a parameter-efficient two-stage schedule — an image–text contrastive alignment followed by instruction tuning — on a 50,000-study subset of MIMIC-CXR, chosen so that training remains feasible on a limited cloud-compute budget while preserving the pathology distribution of the full dataset. On the held-out test split the model is competitive with established systems on semantic report-generation metrics (ROUGE-L 0.292 and a high METEOR and BERTScore for findings), reaches a visual-question-answering exact match of 0.306 (stronger on closed-ended questions), and its abnormality classifier attains a macro F1 of 0.328. These results show that a frozen-encoder, frozen-LLM design adapted only through a small projection and LoRA adapters can drive three chest-X-ray tasks at once on modest hardware, and point to abnormality classification and clinical-efficacy evaluation as the main directions for further improvement.
12
+
13
+ ---
14
+
15
+ # List of Abbreviations
16
+
17
+ | Abbreviation | Meaning |
18
+ |---|---|
19
+ | AI | Artificial Intelligence |
20
+ | AP | Anteroposterior (X-ray view) |
21
+ | BERTScore | BERT-based similarity Score |
22
+ | BLEU | Bilingual Evaluation Understudy |
23
+ | CE | Clinical Efficacy (F1) |
24
+ | CheXbert | BERT-based CheXpert report labeler |
25
+ | CheXpert | Chest eXpert — 14-label chest X-ray labeler |
26
+ | CXR | Chest X-Ray |
27
+ | DINO | self-Distillation with NO labels (DINOv2 self-supervision) |
28
+ | EHR | Electronic Health Record |
29
+ | F1 | F1-score (harmonic mean of precision and recall) |
30
+ | GPU | Graphics Processing Unit |
31
+ | InfoNCE | Info Noise-Contrastive Estimation (loss) |
32
+ | IQR | Interquartile Range |
33
+ | ITC | Image–Text Contrastive (learning) |
34
+ | LLM | Large Language Model |
35
+ | LoRA | Low-Rank Adaptation |
36
+ | METEOR | Metric for Evaluation of Translation with Explicit ORdering |
37
+ | MIMIC-CXR | Medical Information Mart for Intensive Care — Chest X-Ray |
38
+ | MLP | Multi-Layer Perceptron |
39
+ | NF4 | 4-bit NormalFloat quantisation |
40
+ | NLG | Natural Language Generation |
41
+ | PA | Posteroanterior (X-ray view) |
42
+ | PNU | Positive / Negative / Uncertain |
43
+ | QLoRA | Quantised Low-Rank Adaptation |
44
+ | RAD-DINO | Chest-X-ray ViT image encoder (DINOv2-based) |
45
+ | ROUGE | Recall-Oriented Understudy for Gisting Evaluation |
46
+ | RRG | Radiology Report Generation |
47
+ | ViT | Vision Transformer |
48
+ | VLM | Vision–Language Model |
49
+ | VQA | Visual Question Answering |
50
+
51
+ ---
docs/report_part1.md CHANGED
@@ -2,15 +2,15 @@
2
 
3
  ## 1.1 Context and Motivation
4
 
5
- The chest X-ray (CXR) is the most frequently performed medical imaging examination in the world, with hundreds of millions of studies acquired every year for the screening and diagnosis of cardiothoracic conditions. Each examination must be read by a radiologist, who then writes a structured free-text report — typically a detailed **Findings** section followed by a concise **Impression**. As imaging volume keeps growing while the number of trained radiologists remains limited, report turnaround time, reader fatigue, and inter-reader variability have become real bottlenecks in clinical workflows.
6
 
7
- Automatic **radiology report generation (RRG)** aims to ease this burden by drafting a report directly from the image, which the radiologist can then verify and edit. The field has evolved quickly: from early CNN–RNN captioning models, to transformer-based architectures, and most recently to **large vision–language models (VLMs)** such as LLaVA and BLIP-2, whose medical adaptations (RaDialog, LLaVA-Med, CheXagent, MAIRA-2) couple a frozen image encoder with a large language model (LLM) to produce fluent, instruction-following text. In parallel, clinicians often need to ask focused questions about an image — "is there a pleural effusion?", "what is the size of the cardiac silhouette?" — which motivates **visual question answering (VQA)** on chest X-rays.
8
 
9
- A recurring difficulty is that fluent text is not necessarily *clinically correct*: a model can produce a well-formed report that misses or flips a finding. Abnormality-guided approaches such as META-CXR address this by feeding explicit pathology labels to the language model. This thesis follows that direction: it builds a single VLM that performs findings generation, impression generation, and VQA, guided by an explicit 14-pathology **Positive / Negative / Uncertain (PNU)** signal, so that generation is anchored to detected abnormalities rather than to language priors alone.
10
 
11
  ## 1.2 Internship objectives
12
 
13
- The objective of this internship is to design, implement, and evaluate **CXR-VLM**, a unified vision–language model for chest X-ray interpretation built on a shared RAD-DINO + projection + Vicuna-7B (LoRA) backbone. Concretely, the work targets the following goals:
14
 
15
  - **Data pipeline.** Build a reproducible pipeline that filters the MIMIC-CXR corpus and serialises it into a unified instruction format covering findings, impression, and VQA samples.
16
  - **Abnormality-guided prompting.** Train a CheXpert-style classifier that emits a PNU abnormality string, and inject it into the language-model prompt as clinical guidance.
@@ -32,14 +32,16 @@ The remainder of this report is organised as follows:
32
 
33
  ## 2.1 Medical report generation
34
 
35
- Early radiology report generation systems adapted natural-image captioning architectures, pairing a CNN image encoder with an RNN/LSTM decoder. These models produced fluent sentences but struggled with the long, multi-sentence structure of radiology reports and with rare but clinically important findings. The introduction of **R2Gen** (Chen et al., 2020) replaced the recurrent decoder with a memory-driven transformer that records report patterns across studies, and **R2GenCMN** (Chen et al., 2021) added a shared cross-modal memory to better align visual and textual features; both became standard baselines on the MIMIC-CXR benchmark. A parallel line of work injected medical prior knowledge through knowledge graphs (e.g. PPKED, KGAE) or retrieval, and emphasised **clinical-efficacy** metrics — comparing the pathology labels extracted from generated and reference reports with the CheXbert labeler — rather than n-gram overlap alone.
36
 
37
- More recently, the field has shifted to **LLM-based** generators. **RaDialog** (Pellegrini et al., 2023) instruction-tunes a Vicuna LLM on chest X-rays, conditions it on a structured list of CheXpert findings, and adapts it with low-rank (LoRA) adapters, enabling both report generation and conversational interaction. Related foundation-style efforts include **CheXagent**, **MAIRA-2**, **LLaVA-Med**, and **XrayGPT**. To improve clinical grounding, **META-CXR** introduces a *U-MultiClass* formulation that preserves three states per pathology — positive, negative, and uncertain — instead of collapsing them to a binary present/absent label.
38
 
39
- The model proposed here follows the RaDialog recipe but differs in three respects: it uses the chest-X-ray-specific **RAD-DINO** encoder instead of BioViL-T; it serialises abnormalities as a META-CXR-style **PNU** string injected into the prompt; and it adds an optional **contrastive Stage 1** that pre-aligns the visual projection with the report text before any language modelling.
40
 
41
  ## 2.2 Visual question answering
42
 
43
- Visual question answering combines image understanding with natural-language reasoning to answer a question about an image. In the medical domain, dedicated datasets such as **VQA-RAD**, **SLAKE**, and **PathVQA** spurred early methods that fuse a CNN image embedding with a question embedding and classify over a fixed answer vocabulary, often with co-attention or meta-learning of the visual features (e.g. MEVF, MMQ). Chest-X-ray-specific resources followed, including **MIMIC-Diff-VQA**, **EHRXQA**, and the large-scale **MIMIC-Ext-CXR-VQA** used in this work, which organises questions by semantic type (verify, choose, query) and by content (presence, anatomy, attribute, size, and others).
44
 
45
- Whereas most medical VQA systems treat the task as closed-set classification, recent instruction-tuned VLMs answer in free-form text and can share a single backbone across tasks. CXR-VLM adopts this unified view: VQA is handled by the **same** encoder, projection, and LLM used for report generation, with the question taking the place of the instruction and the same PNU abnormality guidance supplied in the prompt. This lets a single set of weights serve findings, impression, and VQA, and lets the abnormality signal benefit short-answer questions as well as long-form generation.
 
 
 
2
 
3
  ## 1.1 Context and Motivation
4
 
5
+ The chest X-ray (CXR) is the most frequently performed medical imaging examination in the world, with hundreds of millions of studies acquired every year for the screening and diagnosis of cardiothoracic conditions. Each examination must be read by a radiologist, who then writes a structured free-text report — typically a detailed Findings section followed by a concise Impression. As imaging volume keeps growing while the number of trained radiologists remains limited, report turnaround time, reader fatigue, and inter-reader variability have become real bottlenecks in clinical workflows.
6
 
7
+ Automatic radiology report generation (RRG) aims to ease this burden by drafting a report directly from the image, which the radiologist can then verify and edit. The field has evolved quickly: from early CNN–RNN captioning models, to transformer-based architectures, and most recently to large vision–language models (VLMs) such as LLaVA and BLIP-2, whose medical adaptations (RaDialog, LLaVA-Med, CheXagent, MAIRA-2) couple a frozen image encoder with a large language model (LLM) to produce fluent, instruction-following text. In parallel, clinicians often need to ask focused questions about an image — "is there a pleural effusion?", "what is the size of the cardiac silhouette?" — which motivates visual question answering (VQA) on chest X-rays.
8
 
9
+ A recurring difficulty is that fluent text is not necessarily *clinically correct*: a model can produce a well-formed report that misses or flips a finding. A natural way to mitigate this is to feed the language model explicit pathology labels, so that generation stays anchored to detected abnormalities rather than to language priors alone. Motivated by this idea, this thesis builds a single VLM that performs findings generation, impression generation, and VQA, guided by an explicit 14-pathology Positive / Negative / Uncertain (PNU) signal.
10
 
11
  ## 1.2 Internship objectives
12
 
13
+ The objective of this internship is to design, implement, and evaluate CXR-VLM, a unified vision–language model for chest X-ray interpretation built on a shared RAD-DINO + projection + Vicuna-7B (LoRA) backbone. Concretely, the work targets the following goals:
14
 
15
  - **Data pipeline.** Build a reproducible pipeline that filters the MIMIC-CXR corpus and serialises it into a unified instruction format covering findings, impression, and VQA samples.
16
  - **Abnormality-guided prompting.** Train a CheXpert-style classifier that emits a PNU abnormality string, and inject it into the language-model prompt as clinical guidance.
 
32
 
33
  ## 2.1 Medical report generation
34
 
35
+ Early radiology report generation systems adapted natural-image captioning architectures, pairing a CNN image encoder with an RNN/LSTM decoder. These models produced fluent sentences but struggled with the long, multi-sentence structure of radiology reports and with rare but clinically important findings. The introduction of R2Gen (Chen et al., 2020) replaced the recurrent decoder with a memory-driven transformer that records report patterns across studies, and R2GenCMN (Chen et al., 2021) added a shared cross-modal memory to better align visual and textual features; both became standard baselines on the MIMIC-CXR benchmark. A parallel line of work injected medical prior knowledge through knowledge graphs (e.g. PPKED, KGAE) or retrieval, and emphasised clinical-efficacy metrics — comparing the pathology labels extracted from generated and reference reports with the CheXbert labeler — rather than n-gram overlap alone.
36
 
37
+ More recently, the field has shifted to LLM-based generators. RaDialog (Pellegrini et al., 2023) instruction-tunes a Vicuna LLM on chest X-rays, conditions it on a structured list of CheXpert findings, and adapts it with low-rank (LoRA) adapters, enabling both report generation and conversational interaction. Related foundation-style efforts include CheXagent, MAIRA-2, LLaVA-Med, and XrayGPT. To improve clinical grounding, META-CXR introduces a *U-MultiClass* formulation that preserves three states per pathology — positive, negative, and uncertain — instead of collapsing them to a binary present/absent label.
38
 
39
+ Taken together, these advances abnormality-aware generation, LLM-based decoding, and parameter-efficient adaptation motivate the approach taken in this thesis: a single vision–language model that generates findings and impression and answers questions about a chest X-ray, explicitly guided by detected abnormalities. The components and training of this model are described in Section 3.
40
 
41
  ## 2.2 Visual question answering
42
 
43
+ Visual question answering combines image understanding with natural-language reasoning to answer a question about an image. In the medical domain, general benchmarks span radiology and pathology: VQA-RAD (Lau et al., *Scientific Data*, 2018) and SLAKE (Liu et al., *IEEE ISBI*, 2021) cover radiology images of several modalities and body regions — including, but not limited to, the chest — while PathVQA targets pathology microscopy rather than radiographs. Early methods on these benchmarks fuse a CNN image embedding with a question embedding and classify over a fixed answer vocabulary, often with co-attention or meta-learning of the visual features (e.g. MEVF, *MICCAI* 2019; MMQ, *MICCAI* 2021).
44
 
45
+ Dedicated chest-X-ray VQA resources are more recent. Medical-Diff-VQA / MIMIC-Diff-VQA (Hu et al., *ACM KDD*, 2023) poses difference questions over pairs of MIMIC-CXR images; MIMIC-CXR-VQA, introduced together with EHRXQA (Bae et al., *NeurIPS Datasets & Benchmarks*, 2023), pairs chest X-rays with structured electronic-health-record question answering; and the large-scale MIMIC-Ext-CXR-VQA used in this work organises questions by semantic type (verify, choose, query) and content type (presence, anatomy, attribute, size, and others).
46
+
47
+ Compared with chest-X-ray report generation, dedicated VQA for chest X-rays is a younger and less standardised area: most existing work contributes datasets or specialised settings (difference questions, EHR-linked QA) and still treats VQA as a standalone closed-set classification problem. This motivates the approach taken here: rather than building a separate VQA classifier, VQA is handled by the same encoder, projection, and language model used for report generation, with the question taking the place of the instruction and the same PNU abnormality guidance supplied in the prompt. A single set of weights therefore serves findings, impression, and VQA, and the abnormality signal benefits short-answer questions as well as long-form generation.
docs/report_part4_5.md CHANGED
@@ -6,7 +6,7 @@
6
 
7
  ### 4.1.1 Dataset and configuration
8
 
9
- All experiments use the **MIMIC-CXR_resized** subset described in Section 3.3: 50,000 patient-disjoint studies split into 40,000 train / 5,000 validation / 5,000 test, under the configuration `report_mode = split_cascade` and `image_mode = frontal_only_split` (one PA/AP frontal image per study). Findings and impression targets come from the MIMIC-CXR reports; VQA samples come from **MIMIC-Ext-CXR-VQA**, attached by study. The reported run is `MIMIC-CXR_resized_run_3`, evaluated on the held-out test split with **predicted** PNU guidance (`pnu_source = predicted`), i.e. the abnormality string comes from the frozen CheXpert classifier rather than from ground-truth labels the realistic inference setting. The VQA test set contains **11,880** question–answer items.
10
 
11
  ### 4.1.2 Implementation and hyperparameters
12
 
@@ -34,9 +34,9 @@ The model is implemented in PyTorch with HuggingFace `transformers`, `peft` (LoR
34
 
35
  ### 4.1.3 Evaluation protocol
36
 
37
- Metrics follow Section 3.6. Generation uses greedy decoding with the canonical instruction variant; the maximum number of new tokens is 300 (findings), 200 (impression), and 64 (VQA). For report generation we report BLEU-1/4, ROUGE-1/2/L, METEOR, and BERTScore-F1; for VQA we report exact match, token-F1, micro-F1, BLEU-1, METEOR, and BERTScore-F1. **Clinical correctness** is assessed through the dedicated CheXpert abnormality classifier (Section 4.2.2), which predicts the 14 pathologies directly from the image; the alternative report-level CheXbert-F1 (which re-labels the *generated text*) was not available on the evaluation host and is therefore not reported.
38
 
39
- > **Note on cross-paper comparison.** The baseline numbers in Tables 4.2 and 4.5 are quoted as compiled in the META-CXR paper (Edirisinghe et al., 2025, Tables 2–3), which is the closest prior work to ours. NLG scores (BLEU, METEOR, ROUGE, BERTScore) depend on the tokeniser, smoothing, and the specific BERTScore backbone/baseline, so absolute values are only **approximately** comparable across papers; the ordering matters more than the exact digits. In particular our BERTScore-F1 (DistilBERT, no baseline rescaling) is **not** comparable to META-CXR's rescaled BERTScore of 0.426 and is therefore omitted from the comparison table.
40
 
41
  ## 4.2 Results
42
 
@@ -68,7 +68,7 @@ The full breakdown for both report tasks is given in Table 4.3. Impression score
68
 
69
  ### 4.2.2 Clinical correctness: abnormality classification
70
 
71
- Clinical correctness is measured by the CheXpert abnormality classifier (Stage 0), which predicts the 14 pathologies — with an explicit *uncertain* state — directly from the image. This is the same role that META-CXR's MHCAC classifier plays. Table 4.4 gives our per-pathology results. Performance is strongest on common, visually salient categories (No Finding F1 0.785, Pleural Effusion 0.622, Support Devices 0.552, Edema 0.508) and collapses to zero on rare categories with few positives (Enlarged Cardiomediastinum, Pleural Other, Fracture), reflecting the strong class imbalance of MIMIC-CXR. The macro averages over the 14 labels are **precision 0.319, recall 0.377, F1 0.328**; over the 11 labels the model actually predicts (excluding the three degenerate classes) the macro-F1 rises to about **0.42**.
72
 
73
  **Table 4.4.** CheXpert classifier per-pathology results (test split).
74
 
@@ -90,7 +90,7 @@ Clinical correctness is measured by the CheXpert abnormality classifier (Stage 0
90
  | Support Devices | 0.445 | 0.727 | 0.552 |
91
  | **Macro average (14)** | **0.319** | **0.377** | **0.328** |
92
 
93
- Table 4.5 places this in context. Two families of "clinical" F1 appear in the literature: (i) a **dedicated image classifier** F1, as in META-CXR's MHCAC, and (ii) a **report-derived Clinical-Efficacy (CE) F1**, obtained by running the CheXbert labeler on the generated report and comparing label vectors. Our classifier (macro-F1 0.328, ≈ 0.42 on predictable classes) is well below META-CXR's multi-encoder MHCAC classifier (weighted F1 0.73), but lands in the same range as the report-derived CE F1 of recent generation systems (0.31–0.43). These quantities are **not** strictly comparable (image-level vs report-derived; macro vs weighted averaging), so Table 4.5 is a positioning reference, not a like-for-like ranking.
94
 
95
  **Table 4.5.** Clinical / abnormality F1 in context (MIMIC-CXR test; values as compiled in Edirisinghe et al., 2025, Tables 2–3). †image-level classifier; ‡report-derived CheXbert CE F1.
96
 
@@ -109,7 +109,7 @@ For cross-domain reference, META-CXR reports a mean F1 of 0.699 on the CheXpert
109
 
110
  ### 4.2.3 Visual question answering
111
 
112
- Table 4.6 gives the overall VQA results on the 11,880-item test set. Tables 4.7 and 4.8 break the score down by answer type and by question semantic/content type. The model is clearly stronger on **closed-ended** questions (exact match 0.385) than on **open-ended** ones (0.204), and within content types it does best on **size** and **plane** questions and worst on **anatomy** and **attribute** questions.
113
 
114
  **Table 4.6.** Overall VQA results (test, 11,880 items).
115
 
@@ -148,24 +148,26 @@ Table 4.6 gives the overall VQA results on the 11,880-item test set. Tables 4.7
148
 
149
  **Report generation.** On findings, CXR-VLM is competitive with established systems on the recall/semantics metrics (ROUGE-L 0.292, strong METEOR) while trailing on BLEU-4 (0.083), where it sits close to RaDialog (0.095). This is the expected profile of an instruction-tuned LLM that rephrases findings rather than reproducing reference n-grams, so BLEU acts as a fluency floor rather than a quality measure. We deliberately do not claim a BLEU/BERTScore state-of-the-art, because those metrics are sensitive to tokenisation and to the BERTScore backbone and are only approximately comparable across papers.
150
 
151
- **Impression weakness.** Impression scores are much lower than findings on every metric, even though `split_cascade` feeds the ground-truth findings to the impression prompt and should make the task easier. Likely factors: impressions are short and abstractive (harsh for exact-overlap metrics), the impression task gets a smaller training share, and the model may carry findings-style phrasing into the impression. This is the clearest area for improvement (Section 5).
152
 
153
  **Clinical correctness.** The Stage-0 CheXpert classifier — our clinical metric — reaches macro-F1 0.328 over 14 labels (≈ 0.42 over the 11 labels it can predict). It is well below META-CXR's dedicated multi-encoder MHCAC classifier (weighted F1 0.73), but comparable to the report-derived clinical-efficacy F1 of recent generation systems (0.31–0.43). The gap to META-CXR is consistent with their heavier design (three fused encoders CNN+ViT+Swin, expert-token cross-attention, class-balanced and contrastive losses) versus our single-encoder MLP head. Because the test-time PNU string is produced by this classifier (`pnu_source = predicted`), its errors propagate into the prompt, so improving it is the highest-leverage next step. Its weakness on rare classes (zero F1 on Enlarged Cardiomediastinum, Pleural Other, Fracture) is a direct symptom of class imbalance.
154
 
155
  **VQA.** The model handles closed-ended verification and presence questions far better than open-ended query and attribute questions, mirroring the general difficulty ordering in medical VQA. The strong results on size and plane questions suggest the visual tokens retain coarse geometric information well.
156
 
157
- **Limitations.** The evaluation is on a single corpus (MIMIC-CXR); the report-level CheXbert CE F1 was not computed; the PNU guidance is bounded by an imbalanced classifier; impression generation underperforms; and compute constraints fix a small effective batch and a 7B LLM. Cross-paper metric comparison is approximate due to differing implementations.
 
 
158
 
159
  ---
160
 
161
  # 5. Conclusion and future work
162
 
163
- This thesis presented **CXR-VLM**, a unified vision–language model that performs chest-X-ray findings generation, impression generation, and visual question answering with a single RAD-DINO + projection + Vicuna-7B (LoRA) backbone, guided by an explicit Positive/Negative/Uncertain abnormality signal and trained with a parameter-efficient two-stage schedule. On the MIMIC-CXR test split the model is competitive with established systems on semantic report-generation metrics (ROUGE-L 0.292, strong METEOR), its abnormality classifier reaches macro-F1 0.328 (≈ 0.42 on predictable classes) — in the range of report-derived clinical-efficacy F1 of recent work — and it achieves a VQA exact match of 0.306, markedly stronger on closed-ended questions. These results show that a frozen-encoder, frozen-LLM design adapted only through a small projection and LoRA adapters can drive three chest-X-ray tasks at once on modest hardware.
164
 
165
  Several directions would strengthen the work:
166
 
167
  - **Stronger abnormality classifier.** The clearest lever: adopt a META-CXR-style design (multi-encoder fusion, class-balancing or focal loss, contrastive/uncertainty objectives) so the predicted PNU string is more reliable and propagates fewer errors into generation and VQA.
168
- - **Report-level clinical efficacy.** Run the CheXbert labeler on generated reports to report the standard CE F1 and enable a like-for-like comparison with the literature, plus radiologist or LLM-as-judge assessment.
169
  - **Impression generation.** Investigate and close the gap on impression (dedicated decoding budget, task-specific tuning, or a true end-to-end findings→impression cascade).
170
  - **Scale and backbones.** Train on more data and views (multi-image studies) and evaluate stronger LLMs (e.g. Llama-3).
171
  - **Cross-dataset validation.** Evaluate on IU X-ray and CheXpert to measure generalisation beyond MIMIC-CXR.
@@ -191,3 +193,7 @@ Several directions would strengthen the work:
191
  15. A. E. W. Johnson et al. *MIMIC-CXR-JPG, a Large Publicly Available Database of Labeled Chest Radiographs.* arXiv:1901.07042, 2019.
192
  16. T. Zhang, V. Kishore, F. Wu, K. Q. Weinberger, Y. Artzi. *BERTScore: Evaluating Text Generation with BERT.* ICLR, 2020.
193
  17. S. Banerjee, A. Lavie. *METEOR: An Automatic Metric for MT Evaluation with Improved Correlation with Human Judgments.* ACL Workshop, 2005.
 
 
 
 
 
6
 
7
  ### 4.1.1 Dataset and configuration
8
 
9
+ All experiments use the 50,000-study subset described in Section 3.3: 40,000 train / 5,000 validation / 5,000 test, patient-disjoint, one frontal image per study, with the impression task conditioned on the ground-truth findings. Findings and impression targets come from the MIMIC-CXR reports; VQA samples come from MIMIC-Ext-CXR-VQA, attached by study. The reported run is evaluated on the held-out test split with predicted PNU guidance the abnormality string comes from the frozen CheXpert classifier rather than from ground-truth labels, which is the realistic inference setting. The VQA data contain 127,010 question–answer items in total (102,941 train / 12,189 validation / 11,880 test); there are far more VQA samples than studies because a single image can be paired with several questions, whereas the findings and impression tasks contribute one sample per study. All metrics below are on the 11,880-item test set.
10
 
11
  ### 4.1.2 Implementation and hyperparameters
12
 
 
34
 
35
  ### 4.1.3 Evaluation protocol
36
 
37
+ Metrics follow Section 3.6. Generation uses greedy decoding with the canonical instruction variant; the maximum number of new tokens is 300 (findings), 200 (impression), and 64 (VQA). For report generation we report BLEU-1/4, ROUGE-1/2/L, METEOR, and BERTScore-F1; for VQA we report exact match, token-F1, micro-F1, BLEU-1, METEOR, and BERTScore-F1. Clinical correctness is assessed through the dedicated CheXpert abnormality classifier (Section 4.2.2), which predicts the 14 pathologies directly from the image; the alternative report-level CheXbert-F1 (which re-labels the *generated text*) was not available on the evaluation host and is therefore not reported.
38
 
39
+ > **Note on cross-paper comparison.** The baseline numbers in Tables 4.2 and 4.5 are quoted as compiled in the META-CXR paper (Edirisinghe et al., 2025, Tables 2–3), which is the closest prior work to ours. NLG scores (BLEU, METEOR, ROUGE, BERTScore) depend on the tokeniser, smoothing, and the specific BERTScore backbone/baseline, so absolute values are only approximately comparable across papers; the ordering matters more than the exact digits. In particular our BERTScore-F1 (DistilBERT, no baseline rescaling) is not comparable to META-CXR's rescaled BERTScore of 0.426 and is therefore omitted from the comparison table.
40
 
41
  ## 4.2 Results
42
 
 
68
 
69
  ### 4.2.2 Clinical correctness: abnormality classification
70
 
71
+ Clinical correctness is measured by the CheXpert abnormality classifier (Stage 0), which predicts the 14 pathologies — with an explicit *uncertain* state — directly from the image. This is the same role that META-CXR's MHCAC classifier plays. Table 4.4 gives our per-pathology results. Performance is strongest on common, visually salient categories (No Finding F1 0.785, Pleural Effusion 0.622, Support Devices 0.552, Edema 0.508) and collapses to zero on rare categories with few positives (Enlarged Cardiomediastinum, Pleural Other, Fracture), reflecting the strong class imbalance of MIMIC-CXR. The macro averages over the 14 labels are precision 0.319, recall 0.377, F1 0.328; over the 11 labels the model actually predicts (excluding the three degenerate classes) the macro-F1 rises to about 0.42.
72
 
73
  **Table 4.4.** CheXpert classifier per-pathology results (test split).
74
 
 
90
  | Support Devices | 0.445 | 0.727 | 0.552 |
91
  | **Macro average (14)** | **0.319** | **0.377** | **0.328** |
92
 
93
+ Table 4.5 places this in context. Two families of "clinical" F1 appear in the literature: (i) a dedicated image classifier F1, as in META-CXR's MHCAC, and (ii) a report-derived Clinical-Efficacy (CE) F1, obtained by running the CheXbert labeler on the generated report and comparing label vectors. Our classifier (macro-F1 0.328, ≈ 0.42 on predictable classes) is well below META-CXR's multi-encoder MHCAC classifier (weighted F1 0.73), but lands in the same range as the report-derived CE F1 of recent generation systems (0.31–0.43). These quantities are not strictly comparable (image-level vs report-derived; macro vs weighted averaging), so Table 4.5 is a positioning reference, not a like-for-like ranking.
94
 
95
  **Table 4.5.** Clinical / abnormality F1 in context (MIMIC-CXR test; values as compiled in Edirisinghe et al., 2025, Tables 2–3). †image-level classifier; ‡report-derived CheXbert CE F1.
96
 
 
109
 
110
  ### 4.2.3 Visual question answering
111
 
112
+ Table 4.6 gives the overall VQA results on the 11,880-item test set. Tables 4.7 and 4.8 break the score down by answer type and by question semantic/content type. The model is clearly stronger on closed-ended questions (exact match 0.385) than on open-ended ones (0.204), and within content types it does best on size and plane questions and worst on anatomy and attribute questions.
113
 
114
  **Table 4.6.** Overall VQA results (test, 11,880 items).
115
 
 
148
 
149
  **Report generation.** On findings, CXR-VLM is competitive with established systems on the recall/semantics metrics (ROUGE-L 0.292, strong METEOR) while trailing on BLEU-4 (0.083), where it sits close to RaDialog (0.095). This is the expected profile of an instruction-tuned LLM that rephrases findings rather than reproducing reference n-grams, so BLEU acts as a fluency floor rather than a quality measure. We deliberately do not claim a BLEU/BERTScore state-of-the-art, because those metrics are sensitive to tokenisation and to the BERTScore backbone and are only approximately comparable across papers.
150
 
151
+ **Impression weakness.** Impression scores are much lower than findings on every metric, even though the impression prompt is given the ground-truth findings and should make the task easier. Likely factors: impressions are short and abstractive (harsh for exact-overlap metrics), the impression task gets a smaller training share, and the model may carry findings-style phrasing into the impression. This is the clearest area for improvement (Section 5).
152
 
153
  **Clinical correctness.** The Stage-0 CheXpert classifier — our clinical metric — reaches macro-F1 0.328 over 14 labels (≈ 0.42 over the 11 labels it can predict). It is well below META-CXR's dedicated multi-encoder MHCAC classifier (weighted F1 0.73), but comparable to the report-derived clinical-efficacy F1 of recent generation systems (0.31–0.43). The gap to META-CXR is consistent with their heavier design (three fused encoders CNN+ViT+Swin, expert-token cross-attention, class-balanced and contrastive losses) versus our single-encoder MLP head. Because the test-time PNU string is produced by this classifier (`pnu_source = predicted`), its errors propagate into the prompt, so improving it is the highest-leverage next step. Its weakness on rare classes (zero F1 on Enlarged Cardiomediastinum, Pleural Other, Fracture) is a direct symptom of class imbalance.
154
 
155
  **VQA.** The model handles closed-ended verification and presence questions far better than open-ended query and attribute questions, mirroring the general difficulty ordering in medical VQA. The strong results on size and plane questions suggest the visual tokens retain coarse geometric information well.
156
 
157
+ **Comparison with prior work.** The same profile appears in recent LLM-based chest-X-ray systems: META-CXR, for instance, also reports a modest BLEU-4 (0.102) while leading on METEOR and BERTScore, which matches our pattern of low n-gram overlap but competitive semantic scores. Our abnormality classifier (macro F1 0.328) is clearly weaker than META-CXR's (weighted F1 0.73), but that system reaches its score with a much heavier design — three fused encoders (CNN, ViT, Swin) plus several auxiliary losses — and its authors themselves note the resulting inference-time complexity as a drawback. Our single-encoder MLP head trades accuracy for a far smaller and simpler model, consistent with the limited-compute goal of this thesis.
158
+
159
+ **Limitations.** Several limitations qualify these results. (i) Evaluation uses a single corpus (MIMIC-CXR) from one institution, so generalisation to other scanners and patient populations is untested. (ii) The report-level CheXbert clinical-efficacy F1 — the metric that best reflects diagnostic usefulness — was not computed for this run. (iii) The PNU guidance comes from an imbalanced, only moderate classifier, so its errors propagate into generation and VQA. (iv) Like any LLM-based generator, the model can hallucinate fluent but unsupported statements, which lexical metrics do not penalise. (v) Impression generation clearly underperforms findings. (vi) Compute constraints fix a small effective batch, a 7B LLM, and a single-view, single-image setup, with no multi-view or longitudinal (prior-study) context. Cross-paper metric comparison is also only approximate, given differing tokenisers and BERTScore backbones.
160
 
161
  ---
162
 
163
  # 5. Conclusion and future work
164
 
165
+ This thesis presented CXR-VLM, a unified vision–language model that performs chest-X-ray findings generation, impression generation, and visual question answering with a single RAD-DINO + projection + Vicuna-7B (LoRA) backbone, guided by an explicit Positive/Negative/Uncertain abnormality signal and trained with a parameter-efficient two-stage schedule. On the MIMIC-CXR test split the model is competitive with established systems on semantic report-generation metrics (ROUGE-L 0.292, strong METEOR), its abnormality classifier reaches macro-F1 0.328 (≈ 0.42 on predictable classes) — in the range of report-derived clinical-efficacy F1 of recent work — and it achieves a VQA exact match of 0.306, markedly stronger on closed-ended questions. These results show that a frozen-encoder, frozen-LLM design adapted only through a small projection and LoRA adapters can drive three chest-X-ray tasks at once on modest hardware.
166
 
167
  Several directions would strengthen the work:
168
 
169
  - **Stronger abnormality classifier.** The clearest lever: adopt a META-CXR-style design (multi-encoder fusion, class-balancing or focal loss, contrastive/uncertainty objectives) so the predicted PNU string is more reliable and propagates fewer errors into generation and VQA.
170
+ - **Report-level clinical efficacy.** Run the CheXbert labeler on generated reports to report the standard CE F1 and enable a like-for-like comparison with the literature, complemented by expert radiologist assessment.
171
  - **Impression generation.** Investigate and close the gap on impression (dedicated decoding budget, task-specific tuning, or a true end-to-end findings→impression cascade).
172
  - **Scale and backbones.** Train on more data and views (multi-image studies) and evaluate stronger LLMs (e.g. Llama-3).
173
  - **Cross-dataset validation.** Evaluate on IU X-ray and CheXpert to measure generalisation beyond MIMIC-CXR.
 
193
  15. A. E. W. Johnson et al. *MIMIC-CXR-JPG, a Large Publicly Available Database of Labeled Chest Radiographs.* arXiv:1901.07042, 2019.
194
  16. T. Zhang, V. Kishore, F. Wu, K. Q. Weinberger, Y. Artzi. *BERTScore: Evaluating Text Generation with BERT.* ICLR, 2020.
195
  17. S. Banerjee, A. Lavie. *METEOR: An Automatic Metric for MT Evaluation with Improved Correlation with Human Judgments.* ACL Workshop, 2005.
196
+ 18. J. J. Lau, S. Gayen, A. Ben Abacha, D. Demner-Fushman. *A Dataset of Clinically Generated Visual Questions and Answers about Radiology Images (VQA-RAD).* Scientific Data, 2018.
197
+ 19. B. Liu, L.-M. Zhan, L. Xu, L. Ma, Y. Yang, X.-M. Wu. *SLAKE: A Semantically-Labeled Knowledge-Enhanced Dataset for Medical Visual Question Answering.* IEEE ISBI, 2021.
198
+ 20. X. Hu et al. *Expert Knowledge-Aware Image Difference Graph Representation Learning for Difference-Aware Medical Visual Question Answering (Medical-Diff-VQA / MIMIC-Diff-VQA).* ACM SIGKDD (KDD), 2023.
199
+ 21. S. Bae et al. *EHRXQA: A Multi-Modal Question Answering Dataset for Electronic Health Records with Chest X-ray Images (incl. MIMIC-CXR-VQA).* NeurIPS Datasets & Benchmarks, 2023.
scripts/cxrvlm_colab_inference.ipynb CHANGED
@@ -745,6 +745,251 @@
745
  "- For deterministic outputs keep `DO_SAMPLE=False`. For diversity, set `DO_SAMPLE=True, TEMPERATURE=0.7`.\n",
746
  "- `predict_report()` is the realistic end-to-end pipeline (no GT leakage). `predict(task='report')` only works on runs trained with `report_mode='merged'`.\n"
747
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  }
749
  ],
750
  "metadata": {
 
745
  "- For deterministic outputs keep `DO_SAMPLE=False`. For diversity, set `DO_SAMPLE=True, TEMPERATURE=0.7`.\n",
746
  "- `predict_report()` is the realistic end-to-end pipeline (no GT leakage). `predict(task='report')` only works on runs trained with `report_mode='merged'`.\n"
747
  ]
748
+ },
749
+ {
750
+ "cell_type": "markdown",
751
+ "id": "qual-md",
752
+ "metadata": {},
753
+ "source": [
754
+ "## 10. Qualitative eval theo nhãn bệnh (GT cạnh prediction)\n",
755
+ "\n",
756
+ "Với **mỗi nhãn bệnh** lấy random `QUAL_N_PER_CLASS` mẫu test (study dương tính nhãn đó), rồi:\n",
757
+ "- Tạo folder `QUAL_OUT/<Tên_bệnh>/<study>/` chứa: ảnh, **GT report** (`report_gt.txt`),\n",
758
+ " **GT VQA** (`vqa_gt.json`), và **so sánh** GT↔prediction (`comparison.md`).\n",
759
+ "- Chạy inference (cascade findings→impression + trả lời các câu VQA của ảnh đó).\n",
760
+ "- Hiển thị inline: ảnh + GT report vs PRED, GT VQA vs PRED, và **GT PNU** (nhãn CheXpert).\n",
761
+ "\n",
762
+ "> Cần tải `manifest`, `vqa`, và **tar shards** của MIMIC-CXR_resized từ HF (shards ~vài GB —\n",
763
+ "> phần lâu nhất; chỉ tải 1 lần, lần sau cache lại). Nếu chỉ muốn xem nhanh, giảm\n",
764
+ "> `QUAL_N_PER_CLASS` hoặc rút gọn `QUAL_PATHOLOGIES`."
765
+ ]
766
+ },
767
+ {
768
+ "cell_type": "code",
769
+ "execution_count": null,
770
+ "id": "qual-cfg",
771
+ "metadata": {},
772
+ "outputs": [],
773
+ "source": [
774
+ "# ===== CONFIG qualitative eval =====\n",
775
+ "QUAL_SPLIT = 'test' # 'test' | 'val' | 'train'\n",
776
+ "QUAL_N_PER_CLASS = 3 # số mẫu random / nhãn\n",
777
+ "QUAL_VQA_PER_IMG = 3 # số câu VQA tối đa hỏi mỗi ảnh (None = tất cả)\n",
778
+ "QUAL_USE_ORACLE_PNU = False # True: ĐƯA GT PNU vào model làm structured_findings (oracle)\n",
779
+ "QUAL_SEED = 42\n",
780
+ "QUAL_PATHOLOGIES = None # None = đủ 14 nhãn; hoặc list con, vd ['Cardiomegaly','Edema']\n",
781
+ "\n",
782
+ "QUAL_DATA_REPO = f'{HF_USER}/cxr-vlm-data'\n",
783
+ "QUAL_WORK = WORK / 'qual_data' # cache tải/giải nén\n",
784
+ "QUAL_OUT = WORK / 'qual_by_pathology' # folder output để duyệt\n",
785
+ "print('QUAL config ok |', QUAL_SPLIT, '| n/class =', QUAL_N_PER_CLASS,\n",
786
+ " '| oracle PNU =', QUAL_USE_ORACLE_PNU)"
787
+ ]
788
+ },
789
+ {
790
+ "cell_type": "markdown",
791
+ "id": "qual-pick-md",
792
+ "metadata": {},
793
+ "source": [
794
+ "### 10a. Tải manifest + VQA, chọn mẫu random theo nhãn"
795
+ ]
796
+ },
797
+ {
798
+ "cell_type": "code",
799
+ "execution_count": null,
800
+ "id": "qual-pick",
801
+ "metadata": {},
802
+ "outputs": [],
803
+ "source": [
804
+ "import csv, json, random, tarfile, shutil\n",
805
+ "from pathlib import Path\n",
806
+ "from huggingface_hub import snapshot_download\n",
807
+ "from data.mimic_cxr_resized_builder import _row_to_pnu # GT CheXpert -> PNU string\n",
808
+ "from data.mimic_cxr_builder import _parse_report # report .txt -> (findings, impression)\n",
809
+ "from model.chexpert_classifier import PATHOLOGIES\n",
810
+ "\n",
811
+ "def _norm(s): return str(s).replace('\\\\', '/').lstrip('/')\n",
812
+ "def _safe(s): return s.replace(' ', '_')\n",
813
+ "\n",
814
+ "_MANI = {'test':'manifest_test.csv','val':'manifest_val.csv',\n",
815
+ " 'validate':'manifest_val.csv','train':'manifest_train.csv'}[QUAL_SPLIT]\n",
816
+ "_VQA = {'test':'vqa_test.json','val':'vqa_val.json',\n",
817
+ " 'validate':'vqa_val.json','train':'vqa.json'}[QUAL_SPLIT]\n",
818
+ "\n",
819
+ "# 1) manifest + vqa (nhẹ)\n",
820
+ "snapshot_download(repo_id=QUAL_DATA_REPO, repo_type='dataset', token=os.environ['HF_TOKEN'],\n",
821
+ " local_dir=str(QUAL_WORK),\n",
822
+ " allow_patterns=[f'MIMIC-CXR_resized/{_MANI}', f'MIMIC-CXR_resized/vqa/{_VQA}'])\n",
823
+ "_mr = QUAL_WORK / 'MIMIC-CXR_resized'\n",
824
+ "manifest = _mr / _MANI\n",
825
+ "vqa_path = _mr / 'vqa' / _VQA\n",
826
+ "\n",
827
+ "# 1 row / study (giữ row đầu)\n",
828
+ "rows_by_study = {}\n",
829
+ "with open(manifest, encoding='utf-8', newline='') as f:\n",
830
+ " for row in csv.DictReader(f):\n",
831
+ " rows_by_study.setdefault(str(row['study_id']).strip(), row)\n",
832
+ "print(f'{len(rows_by_study):,} studies trong manifest')\n",
833
+ "\n",
834
+ "# VQA gom theo dicom (image_id)\n",
835
+ "vqa_by_dicom = {}\n",
836
+ "if vqa_path.is_file():\n",
837
+ " for v in json.load(open(vqa_path, encoding='utf-8')):\n",
838
+ " vqa_by_dicom.setdefault(str(v.get('image_id')).strip(), []).append(v)\n",
839
+ "print(f'VQA: {sum(len(v) for v in vqa_by_dicom.values()):,} câu / {len(vqa_by_dicom):,} ảnh')\n",
840
+ "\n",
841
+ "# 2) chọn random N study dương tính / nhãn\n",
842
+ "random.seed(QUAL_SEED)\n",
843
+ "target_labels = QUAL_PATHOLOGIES or PATHOLOGIES\n",
844
+ "picks = {}\n",
845
+ "print('\\nNhãn #positive picked')\n",
846
+ "for lab in target_labels:\n",
847
+ " col = f'chex_{lab}'\n",
848
+ " pos = [r for r in rows_by_study.values()\n",
849
+ " if str(r.get(col, '')).strip() in ('1', '1.0')]\n",
850
+ " random.shuffle(pos)\n",
851
+ " picks[lab] = pos[:QUAL_N_PER_CLASS]\n",
852
+ " print(f' {lab:28s} {len(pos):6d} {len(picks[lab])}')"
853
+ ]
854
+ },
855
+ {
856
+ "cell_type": "markdown",
857
+ "id": "qual-extract-md",
858
+ "metadata": {},
859
+ "source": [
860
+ "### 10b. Tải shards + rút ảnh/report của các mẫu đã chọn"
861
+ ]
862
+ },
863
+ {
864
+ "cell_type": "code",
865
+ "execution_count": null,
866
+ "id": "qual-extract",
867
+ "metadata": {},
868
+ "outputs": [],
869
+ "source": [
870
+ "# files cần rút (ảnh + report của các study đã chọn)\n",
871
+ "need = set()\n",
872
+ "for rs in picks.values():\n",
873
+ " for r in rs:\n",
874
+ " need.add(_norm(r['image_relpath']))\n",
875
+ " if r.get('report_relpath'):\n",
876
+ " need.add(_norm(r['report_relpath']))\n",
877
+ "print(f'cần rút {len(need)} file (ảnh + report)')\n",
878
+ "\n",
879
+ "# tải toàn bộ shards (ảnh nằm rải) — lâu nhất, cache lại lần sau\n",
880
+ "print('Tải tar shards (vài GB)…')\n",
881
+ "snapshot_download(repo_id=QUAL_DATA_REPO, repo_type='dataset', token=os.environ['HF_TOKEN'],\n",
882
+ " local_dir=str(QUAL_WORK), allow_patterns=['MIMIC-CXR_resized/shards/*.tar'])\n",
883
+ "shards = sorted((_mr / 'shards').glob('*.tar'))\n",
884
+ "print(f'{len(shards)} shards')\n",
885
+ "\n",
886
+ "# rút đúng các file cần (1 pass)\n",
887
+ "extract_root = QUAL_WORK / 'extracted'\n",
888
+ "extracted = {}\n",
889
+ "for shard in shards:\n",
890
+ " with tarfile.open(shard, 'r') as tf:\n",
891
+ " for m in tf:\n",
892
+ " if not m.isfile(): continue\n",
893
+ " name = _norm(m.name)\n",
894
+ " if name in need and name not in extracted:\n",
895
+ " dst = extract_root / name\n",
896
+ " dst.parent.mkdir(parents=True, exist_ok=True)\n",
897
+ " dst.write_bytes(tf.extractfile(m).read())\n",
898
+ " extracted[name] = dst\n",
899
+ "print(f'rút được {len(extracted)} / {len(need)} file')"
900
+ ]
901
+ },
902
+ {
903
+ "cell_type": "markdown",
904
+ "id": "qual-run-md",
905
+ "metadata": {},
906
+ "source": [
907
+ "### 10c. Inference + hiển thị GT↔PRED + lưu folder"
908
+ ]
909
+ },
910
+ {
911
+ "cell_type": "code",
912
+ "execution_count": null,
913
+ "id": "qual-run",
914
+ "metadata": {},
915
+ "outputs": [],
916
+ "source": [
917
+ "def _fmt_answer(a):\n",
918
+ " return ', '.join(map(str, a)) if isinstance(a, list) else str(a)\n",
919
+ "\n",
920
+ "def _cmp_block(lab, study, gt_pnu, gt_find, gt_impr, pred, vqa_rows, oracle_used):\n",
921
+ " L = [f'PATHOLOGY (picked for): {lab}', f'STUDY: {study}',\n",
922
+ " f'oracle PNU fed to model: {oracle_used}', '',\n",
923
+ " '### GT CheXpert labels (PNU)', gt_pnu or '(none)', '',\n",
924
+ " '### FINDINGS',\n",
925
+ " f'[GT] {gt_find or \"(none)\"}',\n",
926
+ " f'[PRED] {pred[\"findings\"]}', '',\n",
927
+ " '### IMPRESSION',\n",
928
+ " f'[GT] {gt_impr or \"(none)\"}',\n",
929
+ " f'[PRED] {pred[\"impression\"]}']\n",
930
+ " if vqa_rows:\n",
931
+ " L += ['', '### VQA']\n",
932
+ " for i, (q, a_gt, a_pred) in enumerate(vqa_rows, 1):\n",
933
+ " L += [f'Q{i}: {q}', f' [GT] {a_gt}', f' [PRED] {a_pred}']\n",
934
+ " return '\\n'.join(L)\n",
935
+ "\n",
936
+ "QUAL_OUT = Path(QUAL_OUT)\n",
937
+ "index, n_done, n_skip = [], 0, 0\n",
938
+ "for lab, rs in picks.items():\n",
939
+ " for r in rs:\n",
940
+ " img_rel = _norm(r['image_relpath'])\n",
941
+ " img_p = extracted.get(img_rel)\n",
942
+ " if img_p is None:\n",
943
+ " n_skip += 1; continue\n",
944
+ " study = str(r.get('study_id'))\n",
945
+ " dicom = Path(img_rel).stem\n",
946
+ "\n",
947
+ " gt_pnu = _row_to_pnu(r)\n",
948
+ " rep_p = extracted.get(_norm(r.get('report_relpath', '')))\n",
949
+ " gt_find, gt_impr = _parse_report(rep_p) if rep_p else (None, None)\n",
950
+ "\n",
951
+ " oracle = gt_pnu if QUAL_USE_ORACLE_PNU else None\n",
952
+ " pred = predict_report(img_p, structured_findings=oracle)\n",
953
+ "\n",
954
+ " vqas = vqa_by_dicom.get(dicom, [])\n",
955
+ " if QUAL_VQA_PER_IMG is not None:\n",
956
+ " vqas = vqas[:QUAL_VQA_PER_IMG]\n",
957
+ " vqa_rows = []\n",
958
+ " for v in vqas:\n",
959
+ " a_pred = predict(img_p, task='vqa', question=v['question'],\n",
960
+ " structured_findings=oracle)\n",
961
+ " vqa_rows.append((v['question'], _fmt_answer(v.get('answer')), a_pred))\n",
962
+ "\n",
963
+ " block = _cmp_block(lab, study, gt_pnu, gt_find, gt_impr, pred,\n",
964
+ " vqa_rows, QUAL_USE_ORACLE_PNU)\n",
965
+ "\n",
966
+ " # hiển thị inline\n",
967
+ " show(img_p, title=f'{lab} | {study} | {r.get(\"view\",\"\")}')\n",
968
+ " print('=' * 80); print(block); print('=' * 80, '\\n')\n",
969
+ "\n",
970
+ " # lưu folder\n",
971
+ " d = QUAL_OUT / _safe(lab) / study\n",
972
+ " d.mkdir(parents=True, exist_ok=True)\n",
973
+ " shutil.copy(img_p, d / f'{dicom}.jpg')\n",
974
+ " if rep_p: shutil.copy(rep_p, d / 'report_gt.txt')\n",
975
+ " (d / 'comparison.md').write_text(block, encoding='utf-8')\n",
976
+ " json.dump([{'question': q, 'answer_gt': a, 'answer_pred': p_}\n",
977
+ " for q, a, p_ in vqa_rows],\n",
978
+ " open(d / 'vqa_gt.json', 'w', encoding='utf-8'),\n",
979
+ " ensure_ascii=False, indent=2)\n",
980
+ " json.dump({'pathology': lab, 'study_id': study, 'dicom': dicom,\n",
981
+ " 'gt_pnu': gt_pnu, 'gt_findings': gt_find, 'gt_impression': gt_impr,\n",
982
+ " 'pred_findings': pred['findings'], 'pred_impression': pred['impression'],\n",
983
+ " 'oracle_pnu': QUAL_USE_ORACLE_PNU},\n",
984
+ " open(d / 'prediction.json', 'w', encoding='utf-8'),\n",
985
+ " ensure_ascii=False, indent=2)\n",
986
+ " index.append({'pathology': lab, 'study_id': study, 'dir': str(d)})\n",
987
+ " n_done += 1\n",
988
+ "\n",
989
+ "json.dump(index, open(QUAL_OUT / '_index.json', 'w', encoding='utf-8'),\n",
990
+ " ensure_ascii=False, indent=2)\n",
991
+ "print(f'\\nXong: {n_done} mẫu (skip {n_skip}). Folder: {QUAL_OUT.resolve()}')"
992
+ ]
993
  }
994
  ],
995
  "metadata": {
scripts/vertex_eval_job.yaml CHANGED
@@ -61,7 +61,7 @@ workerPoolSpecs:
61
  - name: DATASET_NAME
62
  value: MIMIC-CXR_resized # 'IU-Xray' | 'MIMIC-CXR' | 'MIMIC-CXR_resized'
63
  - name: RUN_ID
64
- value: MIMIC-CXR_resized_run_1 # which run on HF_RUNS_REPO to evaluate
65
  # ── Optional ────────────────────────────────────────────────────────────
66
  - name: HF_RUNS_REPO
67
  value: hieu3636/cxr-vlm-runs
@@ -87,10 +87,10 @@ workerPoolSpecs:
87
  # PNU prompt-condition source. 'oracle' = GT chex_* labels from JSON;
88
  # 'predicted' = run the Stage-0 CheXpert classifier per image (realistic).
89
  - name: PNU_SOURCE
90
- value: oracle # 'oracle' | 'predicted'
91
  # Classifier checkpoint on HF_RUNS_REPO (only used when PNU_SOURCE=predicted).
92
- # - name: CHEXPERT_CKPT_PATH
93
- # value: chexpert_classifier/chexpert_mimic_resized.pt
94
  - name: MAX_NEW_TOKENS
95
  value: "300"
96
  - name: UPLOAD_RESULTS_TO_HF
 
61
  - name: DATASET_NAME
62
  value: MIMIC-CXR_resized # 'IU-Xray' | 'MIMIC-CXR' | 'MIMIC-CXR_resized'
63
  - name: RUN_ID
64
+ value: MIMIC-CXR_resized_run_3 # which run on HF_RUNS_REPO to evaluate
65
  # ── Optional ────────────────────────────────────────────────────────────
66
  - name: HF_RUNS_REPO
67
  value: hieu3636/cxr-vlm-runs
 
87
  # PNU prompt-condition source. 'oracle' = GT chex_* labels from JSON;
88
  # 'predicted' = run the Stage-0 CheXpert classifier per image (realistic).
89
  - name: PNU_SOURCE
90
+ value: predicted # 'oracle' | 'predicted'
91
  # Classifier checkpoint on HF_RUNS_REPO (only used when PNU_SOURCE=predicted).
92
+ - name: CHEXPERT_CKPT_PATH
93
+ value: chexpert_classifier/chexpert_mimic_resized.pt
94
  - name: MAX_NEW_TOKENS
95
  value: "300"
96
  - name: UPLOAD_RESULTS_TO_HF