convitom commited on
Commit
215d2b1
·
1 Parent(s): 7326eb9
data/eda_reports.ipynb CHANGED
@@ -22,7 +22,7 @@
22
  },
23
  {
24
  "cell_type": "code",
25
- "execution_count": 27,
26
  "id": "b60b7147",
27
  "metadata": {},
28
  "outputs": [
@@ -50,7 +50,7 @@
50
  },
51
  {
52
  "cell_type": "code",
53
- "execution_count": 28,
54
  "id": "b666d219",
55
  "metadata": {},
56
  "outputs": [
@@ -91,7 +91,7 @@
91
  },
92
  {
93
  "cell_type": "code",
94
- "execution_count": 29,
95
  "id": "09774f4c",
96
  "metadata": {},
97
  "outputs": [
@@ -220,7 +220,7 @@
220
  },
221
  {
222
  "cell_type": "code",
223
- "execution_count": 30,
224
  "id": "f1441bde",
225
  "metadata": {},
226
  "outputs": [
@@ -252,7 +252,7 @@
252
  },
253
  {
254
  "cell_type": "code",
255
- "execution_count": 31,
256
  "id": "325c65bc",
257
  "metadata": {},
258
  "outputs": [
@@ -322,7 +322,7 @@
322
  },
323
  {
324
  "cell_type": "code",
325
- "execution_count": 32,
326
  "id": "a0258afb",
327
  "metadata": {},
328
  "outputs": [
@@ -407,7 +407,7 @@
407
  },
408
  {
409
  "cell_type": "code",
410
- "execution_count": 33,
411
  "id": "eb43a4e7",
412
  "metadata": {},
413
  "outputs": [
@@ -469,7 +469,7 @@
469
  },
470
  {
471
  "cell_type": "code",
472
- "execution_count": 34,
473
  "id": "b77d7356",
474
  "metadata": {},
475
  "outputs": [
@@ -556,7 +556,7 @@
556
  },
557
  {
558
  "cell_type": "code",
559
- "execution_count": 35,
560
  "id": "6751ebf0",
561
  "metadata": {},
562
  "outputs": [
@@ -599,7 +599,7 @@
599
  },
600
  {
601
  "cell_type": "code",
602
- "execution_count": 36,
603
  "id": "b9725823",
604
  "metadata": {},
605
  "outputs": [
@@ -635,7 +635,7 @@
635
  },
636
  {
637
  "cell_type": "code",
638
- "execution_count": 37,
639
  "id": "3eda946d",
640
  "metadata": {},
641
  "outputs": [
@@ -681,7 +681,7 @@
681
  },
682
  {
683
  "cell_type": "code",
684
- "execution_count": 38,
685
  "id": "cb90ae2f",
686
  "metadata": {},
687
  "outputs": [
@@ -722,7 +722,7 @@
722
  "name": "stderr",
723
  "output_type": "stream",
724
  "text": [
725
- "C:\\Users\\admin\\AppData\\Local\\Temp\\ipykernel_19432\\723391630.py:1: DeprecationWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n",
726
  " subset_stats = df.groupby(\"subset\").apply(lambda g: pd.Series({\n"
727
  ]
728
  }
@@ -746,7 +746,7 @@
746
  },
747
  {
748
  "cell_type": "code",
749
- "execution_count": 39,
750
  "id": "6c67fbf2",
751
  "metadata": {},
752
  "outputs": [
@@ -800,7 +800,7 @@
800
  },
801
  {
802
  "cell_type": "code",
803
- "execution_count": 40,
804
  "id": "98cf393e",
805
  "metadata": {},
806
  "outputs": [
@@ -820,7 +820,7 @@
820
  "name": "stderr",
821
  "output_type": "stream",
822
  "text": [
823
- "C:\\Users\\admin\\AppData\\Local\\Temp\\ipykernel_19432\\2479082378.py:2: DeprecationWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n",
824
  " split_stats = df.groupby(\"split\").apply(lambda g: pd.Series({\n"
825
  ]
826
  }
@@ -849,7 +849,7 @@
849
  },
850
  {
851
  "cell_type": "code",
852
- "execution_count": 41,
853
  "id": "7d26ee8f",
854
  "metadata": {},
855
  "outputs": [
@@ -888,7 +888,7 @@
888
  },
889
  {
890
  "cell_type": "code",
891
- "execution_count": 42,
892
  "id": "a647d42b",
893
  "metadata": {},
894
  "outputs": [
@@ -930,7 +930,7 @@
930
  },
931
  {
932
  "cell_type": "code",
933
- "execution_count": 43,
934
  "id": "6f420beb",
935
  "metadata": {},
936
  "outputs": [
@@ -938,7 +938,7 @@
938
  "name": "stderr",
939
  "output_type": "stream",
940
  "text": [
941
- "C:\\Users\\admin\\AppData\\Local\\Temp\\ipykernel_19432\\441297132.py:8: FutureWarning: \n",
942
  "\n",
943
  "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
944
  "\n",
@@ -974,7 +974,7 @@
974
  },
975
  {
976
  "cell_type": "code",
977
- "execution_count": 44,
978
  "id": "2d4f87ba",
979
  "metadata": {},
980
  "outputs": [
@@ -1006,7 +1006,7 @@
1006
  },
1007
  {
1008
  "cell_type": "code",
1009
- "execution_count": 45,
1010
  "id": "9e7ac126",
1011
  "metadata": {},
1012
  "outputs": [
@@ -1077,7 +1077,7 @@
1077
  },
1078
  {
1079
  "cell_type": "code",
1080
- "execution_count": 46,
1081
  "id": "d3fa9af1",
1082
  "metadata": {},
1083
  "outputs": [
@@ -1151,7 +1151,7 @@
1151
  },
1152
  {
1153
  "cell_type": "code",
1154
- "execution_count": 47,
1155
  "id": "6f5ba746",
1156
  "metadata": {},
1157
  "outputs": [
@@ -1218,7 +1218,7 @@
1218
  },
1219
  {
1220
  "cell_type": "code",
1221
- "execution_count": 48,
1222
  "id": "47f751e4",
1223
  "metadata": {},
1224
  "outputs": [
@@ -1282,6 +1282,86 @@
1282
  " print(f\" {report_path(row)} sections: {row['section_headers']}\")"
1283
  ]
1284
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1285
  {
1286
  "cell_type": "markdown",
1287
  "id": "40d7d2b0",
@@ -1292,7 +1372,7 @@
1292
  },
1293
  {
1294
  "cell_type": "code",
1295
- "execution_count": 49,
1296
  "id": "6043d581",
1297
  "metadata": {},
1298
  "outputs": [
@@ -1350,7 +1430,7 @@
1350
  },
1351
  {
1352
  "cell_type": "code",
1353
- "execution_count": 50,
1354
  "id": "941cd135",
1355
  "metadata": {},
1356
  "outputs": [
@@ -1413,7 +1493,7 @@
1413
  },
1414
  {
1415
  "cell_type": "code",
1416
- "execution_count": 51,
1417
  "id": "158a6ce2",
1418
  "metadata": {},
1419
  "outputs": [
@@ -1480,7 +1560,7 @@
1480
  },
1481
  {
1482
  "cell_type": "code",
1483
- "execution_count": 52,
1484
  "id": "7fd31f29",
1485
  "metadata": {},
1486
  "outputs": [
 
22
  },
23
  {
24
  "cell_type": "code",
25
+ "execution_count": 1,
26
  "id": "b60b7147",
27
  "metadata": {},
28
  "outputs": [
 
50
  },
51
  {
52
  "cell_type": "code",
53
+ "execution_count": 2,
54
  "id": "b666d219",
55
  "metadata": {},
56
  "outputs": [
 
91
  },
92
  {
93
  "cell_type": "code",
94
+ "execution_count": 3,
95
  "id": "09774f4c",
96
  "metadata": {},
97
  "outputs": [
 
220
  },
221
  {
222
  "cell_type": "code",
223
+ "execution_count": 4,
224
  "id": "f1441bde",
225
  "metadata": {},
226
  "outputs": [
 
252
  },
253
  {
254
  "cell_type": "code",
255
+ "execution_count": 5,
256
  "id": "325c65bc",
257
  "metadata": {},
258
  "outputs": [
 
322
  },
323
  {
324
  "cell_type": "code",
325
+ "execution_count": 6,
326
  "id": "a0258afb",
327
  "metadata": {},
328
  "outputs": [
 
407
  },
408
  {
409
  "cell_type": "code",
410
+ "execution_count": 7,
411
  "id": "eb43a4e7",
412
  "metadata": {},
413
  "outputs": [
 
469
  },
470
  {
471
  "cell_type": "code",
472
+ "execution_count": 8,
473
  "id": "b77d7356",
474
  "metadata": {},
475
  "outputs": [
 
556
  },
557
  {
558
  "cell_type": "code",
559
+ "execution_count": 9,
560
  "id": "6751ebf0",
561
  "metadata": {},
562
  "outputs": [
 
599
  },
600
  {
601
  "cell_type": "code",
602
+ "execution_count": 10,
603
  "id": "b9725823",
604
  "metadata": {},
605
  "outputs": [
 
635
  },
636
  {
637
  "cell_type": "code",
638
+ "execution_count": 11,
639
  "id": "3eda946d",
640
  "metadata": {},
641
  "outputs": [
 
681
  },
682
  {
683
  "cell_type": "code",
684
+ "execution_count": 12,
685
  "id": "cb90ae2f",
686
  "metadata": {},
687
  "outputs": [
 
722
  "name": "stderr",
723
  "output_type": "stream",
724
  "text": [
725
+ "C:\\Users\\admin\\AppData\\Local\\Temp\\ipykernel_6580\\723391630.py:1: DeprecationWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n",
726
  " subset_stats = df.groupby(\"subset\").apply(lambda g: pd.Series({\n"
727
  ]
728
  }
 
746
  },
747
  {
748
  "cell_type": "code",
749
+ "execution_count": 13,
750
  "id": "6c67fbf2",
751
  "metadata": {},
752
  "outputs": [
 
800
  },
801
  {
802
  "cell_type": "code",
803
+ "execution_count": 14,
804
  "id": "98cf393e",
805
  "metadata": {},
806
  "outputs": [
 
820
  "name": "stderr",
821
  "output_type": "stream",
822
  "text": [
823
+ "C:\\Users\\admin\\AppData\\Local\\Temp\\ipykernel_6580\\2479082378.py:2: DeprecationWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n",
824
  " split_stats = df.groupby(\"split\").apply(lambda g: pd.Series({\n"
825
  ]
826
  }
 
849
  },
850
  {
851
  "cell_type": "code",
852
+ "execution_count": 15,
853
  "id": "7d26ee8f",
854
  "metadata": {},
855
  "outputs": [
 
888
  },
889
  {
890
  "cell_type": "code",
891
+ "execution_count": 16,
892
  "id": "a647d42b",
893
  "metadata": {},
894
  "outputs": [
 
930
  },
931
  {
932
  "cell_type": "code",
933
+ "execution_count": 17,
934
  "id": "6f420beb",
935
  "metadata": {},
936
  "outputs": [
 
938
  "name": "stderr",
939
  "output_type": "stream",
940
  "text": [
941
+ "C:\\Users\\admin\\AppData\\Local\\Temp\\ipykernel_6580\\441297132.py:8: FutureWarning: \n",
942
  "\n",
943
  "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
944
  "\n",
 
974
  },
975
  {
976
  "cell_type": "code",
977
+ "execution_count": 18,
978
  "id": "2d4f87ba",
979
  "metadata": {},
980
  "outputs": [
 
1006
  },
1007
  {
1008
  "cell_type": "code",
1009
+ "execution_count": 19,
1010
  "id": "9e7ac126",
1011
  "metadata": {},
1012
  "outputs": [
 
1077
  },
1078
  {
1079
  "cell_type": "code",
1080
+ "execution_count": 20,
1081
  "id": "d3fa9af1",
1082
  "metadata": {},
1083
  "outputs": [
 
1151
  },
1152
  {
1153
  "cell_type": "code",
1154
+ "execution_count": 21,
1155
  "id": "6f5ba746",
1156
  "metadata": {},
1157
  "outputs": [
 
1218
  },
1219
  {
1220
  "cell_type": "code",
1221
+ "execution_count": 22,
1222
  "id": "47f751e4",
1223
  "metadata": {},
1224
  "outputs": [
 
1282
  " print(f\" {report_path(row)} sections: {row['section_headers']}\")"
1283
  ]
1284
  },
1285
+ {
1286
+ "cell_type": "markdown",
1287
+ "id": "609330c1",
1288
+ "metadata": {},
1289
+ "source": [
1290
+ "### 4. Copy các report bất thường ra thư mục riêng\n",
1291
+ "\n",
1292
+ "Copy file `.txt` của các report bất thường vào `OUT_DIR`, chia subfolder theo loại:\n",
1293
+ "`outlier_findings/`, `outlier_impression/`, `short_findings/`, `short_impression/`, `unusual_sections/`.\n",
1294
+ "\n",
1295
+ "- `COPY_ALL = False` → chỉ copy đúng `N` ví dụ đang hiển thị ở các cell trên.\n",
1296
+ "- `COPY_ALL = True` → copy **toàn bộ** report của mỗi nhóm."
1297
+ ]
1298
+ },
1299
+ {
1300
+ "cell_type": "code",
1301
+ "execution_count": 23,
1302
+ "id": "820581ef",
1303
+ "metadata": {},
1304
+ "outputs": [
1305
+ {
1306
+ "name": "stdout",
1307
+ "output_type": "stream",
1308
+ "text": [
1309
+ " outlier_findings : copied 3,905 (missing 0)\n",
1310
+ " outlier_impression : copied 4,257 (missing 0)\n",
1311
+ " short_findings : copied 50 (missing 0)\n",
1312
+ " short_impression : copied 74 (missing 0)\n",
1313
+ " unusual_sections : copied 9 (missing 0)\n",
1314
+ "\n",
1315
+ "Tong cong da copy 8,295 file -> D:\\USTH\\KLTN\\cxr-vlm-data\\eda_outlier_reports\n"
1316
+ ]
1317
+ }
1318
+ ],
1319
+ "source": [
1320
+ "import shutil\n",
1321
+ "\n",
1322
+ "OUT_DIR = Path(r\"D:\\USTH\\KLTN\\cxr-vlm-data\\eda_outlier_reports\")\n",
1323
+ "COPY_ALL = True # False: chi copy N vi du; True: copy toan bo moi nhom\n",
1324
+ "N = 10 # so vi du moi nhom khi COPY_ALL=False\n",
1325
+ "\n",
1326
+ "\n",
1327
+ "def _select(frame, sort_col=None, ascending=False, random_state=42):\n",
1328
+ " \"\"\"Lay toan bo (COPY_ALL) hoac N dong (sort theo sort_col neu co, else sample).\"\"\"\n",
1329
+ " if COPY_ALL:\n",
1330
+ " return frame\n",
1331
+ " if sort_col is not None:\n",
1332
+ " return frame.nlargest(N, sort_col) if not ascending else frame.nsmallest(N, sort_col)\n",
1333
+ " return frame.sample(min(N, len(frame)), random_state=random_state)\n",
1334
+ "\n",
1335
+ "\n",
1336
+ "# Dinh nghia cac nhom: ten subfolder -> dataframe da chon\n",
1337
+ "groups = {\n",
1338
+ " \"outlier_findings\": _select(df[df[\"findings_len\"] > thresh_f], \"findings_len\"),\n",
1339
+ " \"outlier_impression\": _select(df[df[\"impression_len\"] > thresh_i], \"impression_len\"),\n",
1340
+ " \"short_findings\": _select(short_f),\n",
1341
+ " \"short_impression\": _select(short_i),\n",
1342
+ " \"unusual_sections\": _select(no_fi[no_fi[\"section_headers\"].apply(\n",
1343
+ " lambda s: bool(set(h.strip().upper() for h in str(s).split(\"|\")) & other_set)\n",
1344
+ " )] if len(has_other) else no_fi),\n",
1345
+ "}\n",
1346
+ "\n",
1347
+ "total_copied = 0\n",
1348
+ "for name, frame in groups.items():\n",
1349
+ " dest = OUT_DIR / name\n",
1350
+ " dest.mkdir(parents=True, exist_ok=True)\n",
1351
+ " n_ok = n_miss = 0\n",
1352
+ " for _, row in frame.iterrows():\n",
1353
+ " src = CXR_ROOT / \"files\" / row[\"subset\"] / f\"p{row['subject_id']}\" / f\"s{row['study_id']}.txt\"\n",
1354
+ " if src.exists():\n",
1355
+ " shutil.copy2(src, dest / f\"s{row['study_id']}.txt\")\n",
1356
+ " n_ok += 1\n",
1357
+ " else:\n",
1358
+ " n_miss += 1\n",
1359
+ " total_copied += n_ok\n",
1360
+ " print(f\" {name:<20}: copied {n_ok:>5,} (missing {n_miss})\")\n",
1361
+ "\n",
1362
+ "print(f\"\\nTong cong da copy {total_copied:,} file -> {OUT_DIR}\")"
1363
+ ]
1364
+ },
1365
  {
1366
  "cell_type": "markdown",
1367
  "id": "40d7d2b0",
 
1372
  },
1373
  {
1374
  "cell_type": "code",
1375
+ "execution_count": 24,
1376
  "id": "6043d581",
1377
  "metadata": {},
1378
  "outputs": [
 
1430
  },
1431
  {
1432
  "cell_type": "code",
1433
+ "execution_count": 25,
1434
  "id": "941cd135",
1435
  "metadata": {},
1436
  "outputs": [
 
1493
  },
1494
  {
1495
  "cell_type": "code",
1496
+ "execution_count": 26,
1497
  "id": "158a6ce2",
1498
  "metadata": {},
1499
  "outputs": [
 
1560
  },
1561
  {
1562
  "cell_type": "code",
1563
+ "execution_count": 27,
1564
  "id": "7fd31f29",
1565
  "metadata": {},
1566
  "outputs": [
data/mimic_cxr_builder.py CHANGED
@@ -461,6 +461,12 @@ def build_mimic_cxr_instruct_json(
461
  "split": split_label,
462
  "study_id": study,
463
  "subject_id": subj,
 
 
 
 
 
 
464
  })
465
  n_vqa += 1
466
 
 
461
  "split": split_label,
462
  "study_id": study,
463
  "subject_id": subj,
464
+ # MIMIC-CXR-VQA question taxonomy (for per-category eval).
465
+ "content_type": row.get("content_type"),
466
+ "semantic_type": row.get("semantic_type"),
467
+ "answer_type": ("closed"
468
+ if str(answer).strip().lower() in ("yes", "no")
469
+ else "open"),
470
  })
471
  n_vqa += 1
472
 
data/mimic_cxr_resized_builder.py CHANGED
@@ -405,6 +405,14 @@ def build_mimic_cxr_resized_instruct_json(
405
  "split": split_label,
406
  "study_id": study_str,
407
  "subject_id": subj_str,
 
 
 
 
 
 
 
 
408
  })
409
  n_vqa += 1
410
 
 
405
  "split": split_label,
406
  "study_id": study_str,
407
  "subject_id": subj_str,
408
+ # MIMIC-CXR-VQA question taxonomy — carried through so
409
+ # evaluation can break metrics down per category. answer_type
410
+ # is derived: yes/no → closed, otherwise open (multi-label).
411
+ "content_type": row.get("content_type"),
412
+ "semantic_type": row.get("semantic_type"),
413
+ "answer_type": ("closed"
414
+ if str(answer).strip().lower() in ("yes", "no")
415
+ else "open"),
416
  })
417
  n_vqa += 1
418
 
docs/convert_md.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Convert file Markdown (.md) sang Word (.docx) / HTML / PDF bang pandoc.
3
+
4
+ CACH DUNG: chi can BAM CHAY (Run trong VSCode/PyCharm, hoac double-click).
5
+ Khong can go lenh / khong can nhap args. Chinh cau hinh o khoi CONFIG ben duoi.
6
+
7
+ CAI 1 LAN (neu chua co): python -m pip install pypandoc_binary
8
+ """
9
+
10
+ # =====================================================================
11
+ # CONFIG -- SUA TRUC TIEP O DAY ROI BAM CHAY
12
+ # =====================================================================
13
+
14
+ # File can convert. Co the la:
15
+ # - 1 ten file: "methodology_rewrite.md"
16
+ # - nhieu file: ["methodology_rewrite.md", "bai2.md"]
17
+ # - tat ca file .md: "ALL"
18
+ INPUT = "methodology_rewrite.md"
19
+
20
+ # Dinh dang dau ra: "docx" | "html" | "pdf"
21
+ OUTPUT_FORMAT = "docx"
22
+
23
+ # Ten file dau ra. De None thi tu dat trung ten file goc (chi dung khi INPUT la 1 file).
24
+ OUTPUT_NAME = None
25
+
26
+ # =====================================================================
27
+ # (Khong can sua tu day tro xuong)
28
+ # =====================================================================
29
+
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ HERE = Path(__file__).resolve().parent
34
+
35
+
36
+ def ensure_pypandoc():
37
+ try:
38
+ import pypandoc # noqa: F401
39
+ return
40
+ except ImportError:
41
+ print("[!] Chua co pypandoc. Dang cai 'pypandoc_binary' (kem san pandoc)...")
42
+ import subprocess
43
+ subprocess.check_call(
44
+ [sys.executable, "-m", "pip", "install", "--quiet", "pypandoc_binary"]
45
+ )
46
+ print("[+] Cai xong.")
47
+
48
+
49
+ def convert_one(md_path: Path, to_fmt: str, out_path: Path | None) -> Path:
50
+ import pypandoc
51
+
52
+ if not md_path.exists():
53
+ raise FileNotFoundError(f"Khong tim thay file: {md_path}")
54
+
55
+ if out_path is None:
56
+ out_path = md_path.with_suffix("." + to_fmt)
57
+
58
+ extra_args = ["--toc", "--toc-depth=3"]
59
+ if to_fmt == "html":
60
+ extra_args += ["--standalone", "--mathjax", "--embed-resources"]
61
+ elif to_fmt == "pdf":
62
+ extra_args += ["-V", "geometry:margin=2.5cm"]
63
+
64
+ pypandoc.convert_file(
65
+ str(md_path), to_fmt, outputfile=str(out_path), extra_args=extra_args
66
+ )
67
+ return out_path
68
+
69
+
70
+ def resolve_inputs(inp) -> list[Path]:
71
+ if isinstance(inp, str) and inp.upper() == "ALL":
72
+ return sorted(HERE.glob("*.md"))
73
+ if isinstance(inp, (list, tuple)):
74
+ return [(HERE / x) if not Path(x).is_absolute() else Path(x) for x in inp]
75
+ p = Path(inp)
76
+ return [p if p.is_absolute() else HERE / p]
77
+
78
+
79
+ def main():
80
+ ensure_pypandoc()
81
+
82
+ md_files = resolve_inputs(INPUT)
83
+ if not md_files:
84
+ print("[!] Khong tim thay file .md nao de convert.")
85
+ return
86
+
87
+ many = len(md_files) > 1
88
+ for md in md_files:
89
+ out = None
90
+ if OUTPUT_NAME and not many:
91
+ out = Path(OUTPUT_NAME)
92
+ if not out.is_absolute():
93
+ out = HERE / out
94
+ try:
95
+ result = convert_one(md, OUTPUT_FORMAT, out)
96
+ print(f"[+] {md.name} -> {result.name}")
97
+ except Exception as e: # noqa: BLE001
98
+ print(f"[x] Loi voi {md.name}: {e}")
99
+ if OUTPUT_FORMAT == "pdf":
100
+ print(" -> PDF can mot LaTeX engine (MiKTeX/TeX Live). "
101
+ "Thu doi OUTPUT_FORMAT='html' roi in PDF tu trinh duyet.")
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
106
+ # Giu cua so terminal mo khi double-click tren Windows
107
+ try:
108
+ if sys.stdin and sys.stdin.isatty():
109
+ input("\nXong. Nhan Enter de dong...")
110
+ except Exception:
111
+ pass
docs/methodology_figure_placements.md ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gợi ý vị trí chèn sơ đồ trong Methodology
2
+
3
+ > Danh sách các chỗ nên thêm hình (ngoài Figure pipeline tổng quan ở §Overview đã có). Mỗi mục ghi rõ: **anchor** (chèn ở đâu trong `methodology (1).tex`), **nội dung sơ đồ**, **caption gợi ý**, **độ ưu tiên**. Bạn xem rồi chọn cái nào làm.
4
+ >
5
+ > Ký hiệu ưu tiên: ⭐⭐⭐ rất nên có (paper/thesis nào cũng vẽ) · ⭐⭐ nên có · ⭐ tuỳ chọn.
6
+
7
+ ---
8
+
9
+ ## A. Phần Data Preparation (§sec:data)
10
+
11
+ ### A1. Sơ đồ data pipeline 3 phase ⭐⭐⭐
12
+ - **Anchor:** ngay sau đoạn mở đầu §Data Preparation (đoạn "...operates in three logical phases: selection / image transfer / resize and re-shard...", ~dòng 244–255), trước §Sources.
13
+ - **Nội dung:** flow ngang 3 phase → output: `[SSD: CSV metadata] → Selection → [Cloud notebook] → Image transfer (PhysioNet) → [GPU host] → Resize + tar shard → Unified JSON`. Đánh dấu nơi chạy (SSD/cloud/GPU) và artefact trung gian.
14
+ - **Caption:** "Three-phase offline data pipeline. Heavy linear-cost work (DICOM join, parsing, resize) runs once; the training loop only consumes the unified JSON + resized image tree."
15
+ - *Tương đương Figure 2.3 của report mẫu — gần như bắt buộc.*
16
+
17
+ ### A2. Funnel lọc 4 tầng (selection) ⭐⭐⭐
18
+ - **Anchor:** đầu §Selection Pipeline (sec:selection, ~dòng 283–288), hoặc ngay sau paragraph (d) Stratified sampling.
19
+ - **Nội dung:** sơ đồ phễu (funnel) thu hẹp dần: `~227k studies → (a) frontal-only → (b) report parse (cần cả Findings+Impression) → (c) length-outlier removal → (d) stratified patient-disjoint sampling → 50k (40k/5k/5k)`. Ghi số study còn lại sau mỗi tầng nếu có.
20
+ - **Caption:** "Four-stage filter chain reducing the full MIMIC-CXR corpus to the 50,000-study working subset."
21
+ - *Rất "ăn điểm" và dễ hiểu; nên có số liệu thật ở mỗi tầng.*
22
+
23
+ ### A3. Sơ đồ rẽ nhánh report_mode / image_mode → samples ⭐⭐
24
+ - **Anchor:** sau bảng `tab:reportmodes` (~dòng 418), trong §Unified Instruction JSON.
25
+ - **Nội dung:** 1 study → (image_mode: frontal_only_split → 1 ảnh) → (report_mode: split_cascade → 2 sample: `findings` từ ảnh; `impression` từ ảnh + GT findings). Minh hoạ cascade nối findings→impression.
26
+ - **Caption:** "How one study is serialised into training samples under `report_mode=split_cascade` and `image_mode=frontal_only_split`."
27
+
28
+ ---
29
+
30
+ ## B. Phần Model Architecture (§sec:arch)
31
+
32
+ ### B1. RAD-DINO patchification ⭐⭐
33
+ - **Anchor:** trong §Image Encoder (sec:encoder), sau câu "...emits 1369 patch tokens of dimension 768... class token is discarded..." (~dòng 453–456).
34
+ - **Nội dung:** `518×518 image → ViT-B/14 chia patch 14×14 → grid 37×37 = 1369 tokens (768-d) [+ CLS bị loại]`. Có thể thêm overlay heatmap minh hoạ patch.
35
+ - **Caption:** "RAD-DINO patchification: a 518×518 chest X-ray becomes 1369 patch tokens (768-d); the [CLS] token is dropped before projection."
36
+
37
+ ### B2. Chi tiết module MLP Projection ⭐⭐⭐
38
+ - **Anchor:** trong §MLP Projection (sec:projection), ngay sau khối align equations (eq:tap, ~dòng 501) hoặc cuối tiểu mục.
39
+ - **Nội dung:** `Patch features P (1369×768) + 32 learnable query Q0 → CrossAttn 8-head → H0 (32×768) → Linear W1 + GELU + Dropout → H1 (32×1024) [TAP cho ITC head] → Linear W2 → V (32×4096)`. Đánh dấu nhánh 1024-d rẽ sang ITC head.
40
+ - **Caption:** "MLP projection module. A 32-query cross-attention pool compresses 1369 patch tokens; the 1024-d intermediate is the tap point routed to the ITC head (Stage 1) or onward to the LLM (Stage 2)."
41
+ - *Module trung tâm của model — nên vẽ kỹ.*
42
+
43
+ ### B3. CheXpert classifier → PNU string ⭐⭐
44
+ - **Anchor:** trong §CheXpert Classifier (sec:chex), sau khối verbatim ví dụ PNU (~dòng 537) hoặc cuối tiểu mục.
45
+ - **Nội dung:** `RAD-DINO [CLS] embedding → MLP head → 14×3 logits (per-label argmax) → format_pnu() → PNU 3-section string → chèn vào prompt giữa visual tokens và instruction`. Phân biệt nhánh train (GT từ CSV) vs eval (classifier tự predict).
46
+ - **Caption:** "Auxiliary CheXpert head: the global RAD-DINO embedding is mapped to a 14×3 (Positive/Negative/Uncertain) tensor and serialised into the PNU prompt block."
47
+
48
+ ### B4. LoRA injection trên attention block ⭐⭐
49
+ - **Anchor:** trong §Language Model and PEFT (sec:llm), sau đoạn mô tả LoRA trên q/k/v/o (~dòng 584–593).
50
+ - **Nội dung:** 1 transformer block: base weight W (4-bit NF4, frozen) + adapter `ΔW = B·A` (rank 16, trainable) trên q/k/v/o; FFN để nguyên. Có thể thêm công thức `h = Wx + (α/r)·BAx`.
51
+ - **Caption:** "LoRA adaptation: rank-16 low-rank adapters on q/k/v/o of every Vicuna block; the 4-bit base weights stay frozen, the feed-forward sublayers are untouched."
52
+
53
+ ### B5. Anatomy của prompt + `<image>` expansion ⭐⭐⭐
54
+ - **Anchor:** trong §Prompt Assembly (sec:prompt), sau khối verbatim prompt skeleton (~dòng 606) và/hoặc sau đoạn mô tả expand 31 vị trí (~dòng 608–615).
55
+ - **Nội dung:** hai phần:
56
+ 1. **Prompt anatomy** — thanh ngang chia slot, tô màu theo task: `[SYSTEM] [<image>] [PNU block] [task context: GT findings / empty] [instruction] [ASSISTANT: target]`.
57
+ 2. **Image-token expansion** — `1 <image> placeholder → thay bằng 32 visual tokens → attention mask & label tensor nở thêm 31 vị trí; vùng visual + prompt + pad gán -100`.
58
+ - **Caption:** "Prompt assembly and image-token expansion. The single `<image>` placeholder is replaced by 32 visual tokens; attention mask, position ids, and labels are expanded by 31, with all non-target positions masked to −100."
59
+ - *Quan trọng cho phần dễ sai nhất của pipeline (loss masking).*
60
+
61
+ ---
62
+
63
+ ## C. Phần Training Strategy (§sec:training)
64
+
65
+ ### C1. Sơ đồ curriculum 3 stage ⭐⭐⭐
66
+ - **Anchor:** đầu §Training Strategy, sau đoạn intro (~dòng 665–674), trước §Stage 0.
67
+ - **Nội dung:** 3 khối nối tiếp, mỗi khối ghi module frozen/trainable + loss:
68
+ - `Stage 0`: RAD-DINO frozen → MLP head (BCE, U-MultiClass).
69
+ - `Stage 1 (ITC)`: encoder frozen, projection+ITC head trainable, **LLM không load**, loss InfoNCE.
70
+ - `Stage 2`: encoder+classifier+base-LLM frozen, projection+LoRA trainable, loss causal CE.
71
+ Mũi tên: projection weights Stage 1 → nạp vào Stage 2.
72
+ - **Caption:** "Two-stage training curriculum (preceded by the Stage 0 classifier). Stage 1 aligns the projection contrastively without loading Vicuna; Stage 2 loads the full model and instruction-tunes projection + LoRA."
73
+
74
+ ### C2. Sơ đồ contrastive Stage 1 (kiểu CLIP) ⭐⭐⭐
75
+ - **Anchor:** trong §Stage 1 (sec:stage1), sau equation InfoNCE (eq:itc, ~dòng 720).
76
+ - **Nội dung:** hai nhánh:
77
+ - Image: `image → RAD-DINO → projection → ITC head → mean-pool → 128-d L2-norm`.
78
+ - Text (offline): `findings (fallback impression) → CXR-BERT → 128-d L2-norm → cache {study_id → tensor}`.
79
+ - Ma trận tương đồng `B×B`, đường chéo = positive, InfoNCE đối xứng.
80
+ - **Caption:** "Stage 1 image–text contrastive alignment. Text embeddings are precomputed offline with CXR-BERT; only the projection + ITC head receive gradients."
81
+
82
+ ### C3. Sơ đồ loss masking (label tensor) ⭐⭐
83
+ - **Anchor:** trong §Loss Masking and Image-Token Accounting (~dòng 781–797).
84
+ - **Nội dung:** một dải token tuyến tính tô màu: `[system −100][32 visual −100][PNU −100][instruction −100][pad −100]` vs `[assistant response: loss được tính]`. Minh hoạ chỉ vùng response đóng góp cross-entropy.
85
+ - **Caption:** "Label masking. Cross-entropy is computed strictly on assistant-response tokens; system prompt, visual span, PNU block, instruction, and padding are all set to −100."
86
+
87
+ ---
88
+
89
+ ## D. Phần Evaluation (§sec:eval) — nếu giữ trong Methodology
90
+
91
+ ### D1. Sơ đồ luồng evaluation ⭐
92
+ - **Anchor:** đầu §Evaluation Protocol hoặc trong §Tasks and Held-Out Data (~dòng 877–896).
93
+ - **Nội dung:** `test image → [CheXpert PNU predicted] (+ GT findings nếu impression) → model greedy decode → hypothesis → {NLG metrics: BLEU/ROUGE/METEOR/BERTScore} + {Clinical: CheXbert F1} + {VQA: EM/token-F1/LLM-judge}`.
94
+ - **Caption:** "Evaluation flow per task, from test image to the metric families reported."
95
+
96
+ ### D2. Sơ đồ CheXbert F1 ⭐
97
+ - **Anchor:** trong §Clinical Correctness (sec:clinical-f1, ~dòng 938–961).
98
+ - **Nội dung:** `generated report + reference report → CheXbert labeler → 2 vectors 14 nhãn → binarise (−1,0→neg) → macro-F1/P/R`.
99
+ - **Caption:** "Clinical F1: both generated and reference reports are labeled by CheXbert; the 14-label vectors are compared after binarisation."
100
+
101
+ ---
102
+
103
+ ## Tổng hợp ưu tiên
104
+
105
+ | # | Sơ đồ | Mục | Ưu tiên |
106
+ |---|---|---|---|
107
+ | A1 | Data pipeline 3 phase | §Data Prep | ⭐⭐⭐ |
108
+ | A2 | Funnel lọc 4 tầng | §Selection | ⭐⭐⭐ |
109
+ | B2 | MLP Projection chi tiết | §Projection | ⭐⭐⭐ |
110
+ | B5 | Prompt anatomy + `<image>` expansion | §Prompt | ⭐⭐⭐ |
111
+ | C1 | Curriculum 3 stage | §Training | ⭐⭐⭐ |
112
+ | C2 | Contrastive Stage 1 (CLIP-style) | §Stage 1 | ⭐⭐⭐ |
113
+ | A3 | report_mode/image_mode → samples | §JSON | ⭐⭐ |
114
+ | B1 | RAD-DINO patchification | §Encoder | ⭐⭐ |
115
+ | B3 | CheXpert → PNU | §CheXpert | ⭐⭐ |
116
+ | B4 | LoRA injection | §LLM | ⭐⭐ |
117
+ | C3 | Loss masking | §Masking | ⭐⭐ |
118
+ | D1 | Luồng evaluation | §Eval | ⭐ |
119
+ | D2 | CheXbert F1 | §Clinical F1 | ⭐ |
120
+
121
+ > Nếu muốn, tôi có thể chèn sẵn các marker `% [INSERT FIGURE Xn: ...]` vào đúng vị trí trong `methodology (1).tex` (chỉ là comment, không ảnh hưởng biên dịch) để bạn thấy ngay chỗ nào cần hình khi mở file.
docs/methodology_revision_plan.md ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Kế hoạch chỉnh sửa Methodology cho khớp văn phong thesis USTH
2
+
3
+ > Tài liệu này so sánh `docs/methodology (1).tex` (bản hiện tại, viết theo văn phong paper RaDialog/META-CXR/BLIP-2) với report tham khảo của anh khoá trên (`main.pdf` — *Multi-Agent AI System for Industrial Machine Maintenance*, USTH Bachelor Thesis 2025), và liệt kê những gì cần chỉnh để bản methodology của bạn "na ná" report thesis đó. **Chưa sửa `.tex` — chỉ là plan để bạn duyệt.**
4
+
5
+ ---
6
+
7
+ ## 1. Report tham khảo có cấu trúc & văn phong gì?
8
+
9
+ **Cấu trúc toàn luận văn (4 chương):**
10
+ 1. **Introduction** — Problem Statement / Objectives / Expected Outcomes / Thesis structure
11
+ 2. **Methodology** — Key Concepts and Definitions → Dataset → System Architecture → Agents (mô tả từng module) → Reasoning Strategy
12
+ 3. **Experiments and Results** — Experimental Setup (Datasets, Environment) → Results and Discussion
13
+ 4. **Conclusion** — Summary / Limitations / Future Work
14
+ + Front matter: Acknowledgements, Abstract, **List of Abbreviations**, List of Figures, List of Tables
15
+
16
+ **Đặc điểm văn phong (cấp độ undergraduate, KHÔNG phải paper):**
17
+ - Mở đầu chương Methodology bằng tiểu mục **"Key Concepts and Definitions"** — định nghĩa từng khái niệm nền (Agent, LLM, ReAct/CoT, LangGraph) *trước khi* dùng. Đây là phần bản của bạn **đang thiếu hoàn toàn**.
18
+ - Mỗi module/thành phần được mô tả theo **template bullet cố định**: `Objective:` / `Input:` / `Output:` / `Core Logic:`. Rất dễ đọc, rất "thesis USTH".
19
+ - **Nhiều hình** (mỗi agent có 1 sơ đồ kiến trúc/flowchart riêng) và **nhiều bảng** (overview dataset, config, hyperparameters, sample output).
20
+ - Văn xuôi **đơn giản, mô tả** — câu ngắn, đôi khi xưng "I" ("I selected…", "I leverage…"). Ít biện luận kiểu nghiên cứu.
21
+ - Equation đánh số đơn giản (Eq 1, Eq 2…), giải thích ký hiệu bằng bullet `Where:`.
22
+ - **Evaluation/metrics nằm ở chương Experiments**, KHÔNG nằm trong Methodology.
23
+ - Có **bảng ví dụ output thật** (Sample Output from Alert Agent) để minh hoạ.
24
+ - Bibliography kiểu **IEEE đánh số `[1]`** kèm URL.
25
+
26
+ ---
27
+
28
+ ## 2. Methodology hiện tại của bạn đang như thế nào?
29
+
30
+ - 1 chương duy nhất, văn phong **paper top-tier**: Overview → Data Preparation → Model Architecture → Training Strategy → **Evaluation Protocol**.
31
+ - Văn xuôi **rất dày, học thuật cao** (đoạn dài, biện luận kiểu "ablations show…", "sweet spot in grid search over {1,2,4}", confidence interval bootstrap…).
32
+ - Chỉ có **1 hình** (TikZ pipeline). Có equation đẹp, vài bảng tốt.
33
+ - **Không có** phần định nghĩa khái niệm nền.
34
+ - **Có** cả Evaluation Protocol đầy đủ trong methodology (report tham khảo đẩy phần này sang Experiments).
35
+ - Bibliography kiểu `natbib`/`abbrvnat` (citep), khác kiểu IEEE số của report mẫu.
36
+
37
+ > Kết luận: nội dung của bạn **đầy đủ và chất lượng hơn** report mẫu, nhưng **văn phong & cách trình bày lệch hẳn** sang phong cách paper. Để "na ná thesis USTH" thì cần *hạ altitude*, *bullet-hoá*, *thêm hình/bảng*, và *thêm phần định nghĩa khái niệm*.
38
+
39
+ ---
40
+
41
+ ## 3. Bảng khác biệt chính & hướng xử lý
42
+
43
+ | Khía cạnh | Report mẫu (main.pdf) | Bản của bạn hiện tại | Cần chỉnh |
44
+ |---|---|---|---|
45
+ | Mở đầu chương | "Key Concepts and Definitions" định nghĩa nền | Vào thẳng Overview | **Thêm** tiểu mục Key Concepts |
46
+ | Mô tả module | Bullet `Objective/Input/Output/Core Logic` | Văn xuôi dày | **Bullet-hoá** từng module |
47
+ | Hình minh hoạ | Nhiều (mỗi module 1 sơ đồ) | 1 hình pipeline | **Thêm** 3–5 hình |
48
+ | Bảng | Overview, config, hyperparams, **sample output** | Có hyperparams, datasources, modes | **Thêm bảng sample output** |
49
+ | Văn phong | Đơn giản, undergrad, đôi khi "I" | Dày, paper, hedging nhiều | **Hạ altitude**, cắt biện luận thừa |
50
+ | Evaluation | Ở chương Experiments | Trong Methodology | **Cân nhắc tách** sang Experiments |
51
+ | Equation | Đơn giản + `Where:` bullet | Đẹp nhưng dày | Giữ, thêm `Where:` giải thích ký hiệu |
52
+ | Abbreviations | Có bảng riêng | Không | **Thêm** List of Abbreviations |
53
+ | Bibliography | IEEE số `[n]` + URL | natbib abbrvnat | (tuỳ template chung của luận văn) |
54
+
55
+ ---
56
+
57
+ ## 4. Khuyến nghị chỉnh sửa cụ thể
58
+
59
+ ### 4.1. Thêm tiểu mục "Key Concepts and Definitions" (ưu tiên cao)
60
+ Đặt ngay sau phần Overview, định nghĩa ngắn gọn (mỗi mục 2–4 câu, có thể kèm 1 hình) các khái niệm:
61
+ - **Vision–Language Model (VLM)** — mô hình kết hợp ảnh + ngôn ngữ.
62
+ - **Chest X-ray report**: phân biệt **Findings** vs **Impression**; **VQA**.
63
+ - **Large Language Model / Vicuna** — vai trò decoder.
64
+ - **Vision Transformer (ViT) & self-supervised (DINOv2) → RAD-DINO**.
65
+ - **LoRA / PEFT / QLoRA (4-bit NF4)** — định nghĩa "parameter-efficient".
66
+ - **Image–Text Contrastive (ITC) / InfoNCE** — học căn chỉnh ảnh–văn bản.
67
+ - **CheXpert 14 pathologies & U-MultiClass (Positive/Negative/Uncertain)**.
68
+
69
+ > Lý do: report mẫu dành hẳn §2.1 cho việc này; người chấm USTH quen đọc định nghĩa nền trước khi vào hệ thống.
70
+
71
+ ### 4.2. Bullet-hoá mô tả từng module (ưu tiên cao)
72
+ Áp dụng template của report mẫu cho 4 module trong §Model Architecture. Ví dụ cho **MLP Projection**:
73
+
74
+ ```
75
+ Objective: Pool patch features thành số token cố định và bridge không gian
76
+ vision (768-d) → LLM (4096-d).
77
+ Input: Patch features P ∈ R^{B×1369×768} từ RAD-DINO (frozen).
78
+ Output: Visual tokens V ∈ R^{B×32×4096}; nhánh phụ xuất 1024-d cho ITC head.
79
+ Core Logic: 32 learnable query → cross-attention 8-head → MLP (GELU, dropout)
80
+ → linear ra 4096-d. (kèm Eq. cross-attn)
81
+ ```
82
+ Làm tương tự cho: Image Encoder, CheXpert Classifier, Language Model + LoRA. Giữ lại các equation hiện có, chỉ tách phần biện luận dài thành câu mô tả ngắn.
83
+
84
+ ### 4.3. Thêm hình minh hoạ (ưu tiên cao — report mẫu rất "ăn điểm" nhờ hình)
85
+ Đề xuất tối thiểu 4 hình:
86
+ 1. **Data preparation pipeline** (giống Fig 2.3 của mẫu): selection → resize/shard → unified JSON. (Hiện chỉ mô tả bằng chữ ở §Data Preparation.)
87
+ 2. **Two-stage training schedule** — sơ đồ Stage 0 (CheXpert) → Stage 1 (ITC, không load LLM) → Stage 2 (instruction tuning), kèm ô "frozen/trainable".
88
+ 3. **Projection module detail** — query tokens → cross-attn → MLP → (ITC head / LLM).
89
+ 4. **Prompt assembly** — minh hoạ chèn `<image>` (1 token → 32 visual token) + PNU block + instruction.
90
+
91
+ > Hình pipeline TikZ hiện tại giữ nguyên làm Figure tổng quan ở Overview.
92
+
93
+ ### 4.4. Thêm bảng "Sample Output" (ưu tiên trung bình)
94
+ Report mẫu có Table 2.7/2.8 ví dụ output thật → rất thuyết phục. Bạn nên thêm:
95
+ - **Bảng ví dụ một training sample hoàn chỉnh**: prompt (system + `<image>` + PNU + instruction) → target findings.
96
+ - **Bảng ví dụ PNU string** sinh ra từ CheXpert classifier.
97
+ - (Nếu có) ví dụ findings/impression model sinh ra vs ground-truth.
98
+
99
+ ### 4.5. Hạ altitude văn phong (ưu tiên trung bình)
100
+ - Cắt/giảm các câu biện luận kiểu nghiên cứu sâu: "monotone improvement as batch grew from 8 to 64", "diverged within ~300 steps", "0.3% of best validation contrastive accuracy", "bootstrap 1000 resamples"… → giữ kết luận, bỏ chi tiết thí nghiệm (chuyển sang chương Experiments nếu muốn).
101
+ - Rút ngắn đoạn dài thành câu mô tả + bullet.
102
+ - Cho phép xưng "I"/"we" nhất quán như report mẫu (mẫu dùng "I").
103
+
104
+ ### 4.6. Cân nhắc tách Evaluation Protocol sang chương Experiments (ưu tiên trung bình)
105
+ Report mẫu để toàn bộ metrics ở **Chapter 3 — Experiments and Results** (Experimental Setup → Results). Bản của bạn đang để Evaluation Protocol trong Methodology.
106
+ - **Phương án A (giống mẫu nhất):** chuyển §Evaluation Protocol → chương Experiments, mục "Evaluation Metrics".
107
+ - **Phương án B (giữ trong Methodology):** giữ nhưng rút gọn, bỏ phần Reporting/bootstrap CI (đẩy sang Experiments).
108
+ → *Cần bạn quyết theo template chung của luận văn.*
109
+
110
+ ### 4.7. Front matter & tiểu tiết (ưu tiên thấp)
111
+ - Thêm **List of Abbreviations**: CXR, VLM, LLM, RRG, VQA, LoRA, QLoRA, PEFT, ITC, InfoNCE, PNU, ViT, MLP, BLEU, ROUGE, NLG, MIMIC-CXR, RAD-DINO, CheXbert…
112
+ - Đồng bộ **bibliography style** với template luận văn (nếu khoa yêu cầu IEEE số `[n]` thì đổi từ natbib `abbrvnat`).
113
+ - Đảm bảo mọi hình/bảng đều được **ref trong văn** (mẫu làm rất kỹ điều này).
114
+
115
+ ---
116
+
117
+ ## 5. Outline đề xuất cho chương Methodology (sau chỉnh)
118
+
119
+ ```
120
+ Chapter: Methodology
121
+ X.1 Overview (giữ, kèm Figure pipeline tổng quan)
122
+ X.2 Key Concepts and Definitions (MỚI — §4.1)
123
+ VLM · Findings/Impression/VQA · LLM/Vicuna · ViT/RAD-DINO ·
124
+ LoRA/QLoRA · ITC/InfoNCE · CheXpert U-MultiClass (PNU)
125
+ X.3 Dataset / Data Preparation (giữ; THÊM Figure pipeline dữ liệu)
126
+ Sources · Selection · Storage/Resize · Unified JSON
127
+ X.4 Model Architecture (bullet-hoá theo §4.2)
128
+ Image Encoder · MLP Projection · CheXpert Classifier · LLM + LoRA
129
+ · Prompt Assembly (THÊM Figure prompt + bảng sample)
130
+ X.5 Training Strategy (giữ; THÊM Figure 3-stage; hạ altitude §4.5)
131
+ Stage 0 · Stage 1 (ITC) · Stage 2 · Hyperparameters table
132
+ [X.6 Evaluation Protocol] (CÂN NHẮC chuyển sang Experiments — §4.6)
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 6. Checklist thực thi (khi bạn duyệt xong)
138
+
139
+ - [ ] Viết tiểu mục **Key Concepts and Definitions** (§4.1)
140
+ - [ ] Bullet-hoá 4 module + Prompt Assembly theo `Objective/Input/Output/Core Logic` (§4.2)
141
+ - [ ] Vẽ **4 hình** TikZ: data pipeline, 3-stage training, projection detail, prompt assembly (§4.3)
142
+ - [ ] Thêm **bảng sample**: training sample đầy đủ, PNU string (§4.4)
143
+ - [ ] **Hạ altitude** văn phong, cắt biện luận thí nghiệm thừa (§4.5)
144
+ - [ ] Quyết & xử lý **Evaluation Protocol** (giữ rút gọn / chuyển sang Experiments) (§4.6)
145
+ - [ ] Thêm **List of Abbreviations**; đồng bộ bibliography style (§4.7)
146
+ - [ ] Kiểm tra mọi figure/table đều được cross-ref trong văn
147
+
148
+ ---
149
+
150
+ ## 7. Lưu ý về nội dung (đối chiếu code/CLAUDE.md)
151
+
152
+ Nội dung methodology hiện tại **khớp tốt** với CLAUDE.md (RAD-DINO 518×518, 1369 patch token, projection 32 token 4096-d, ITC InfoNCE τ=0.07, LoRA r16/α32 trên q/k/v/o, QLoRA NF4, split_cascade + frontal_only_split, PNU 3-section…). Vài điểm cần bạn xác nhận lại với số liệu thật khi viết Experiments (không ảnh hưởng cấu trúc):
153
+ - Các con số sweep/grid-search (batch 16/32/64/96, lr 1e-4/2e-4/5e-4) — nếu là **minh hoạ/giả định** thì nên chuyển thành mô tả định tính, tránh khẳng định kết quả chưa chạy.
154
+ - Số study subset (40k/5k/5k), Δ prevalence <0.7pp — xác nhận đúng số liệu pipeline thật của bạn.
155
+ - Eval thresholds (max new tokens 300/200/64) — khớp config thật.
156
+
157
+ > Những điểm này thuộc về *độ chính xác số liệu*, sẽ chốt khi viết chương Experiments; không cản trở việc tái cấu trúc văn phong ở trên.
docs/methodology_rewrite.docx ADDED
Binary file (27.4 kB). View file
 
docs/methodology_rewrite.md ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chapter 2. Methodology
2
+
3
+ > **Ghi chú cho bạn:** đây là bản methodology viết lại theo văn phong thesis USTH (đơn giản hơn, bullet-hoá theo `Objective/Input/Output/Core Logic`, thêm phần *Key Concepts*, hạ altitude so với bản `.tex` paper-style). Các chú thích chèn biểu đồ được đặt ngay dưới đoạn liên quan dưới dạng `> 📊 **(Thêm biểu đồ: …)**`. Cấu hình giả định xuyên suốt: `report_mode = split_cascade`, `image_mode = frontal_only_split`.
4
+
5
+ This chapter describes how the proposed Vision–Language Model (**CXR-VLM**) is built and trained. It is organised in five parts. Section 2.1 gives an end-to-end overview. Section 2.2 defines the key concepts used throughout the chapter. Section 2.3 documents how the MIMIC-CXR corpus is filtered and serialised into the unified instruction format. Section 2.4 describes the four modules of the model and the prompt that ties them together. Section 2.5 specifies the two-stage training schedule. Section 2.6 defines the evaluation protocol.
6
+
7
+ ---
8
+
9
+ ## 2.1 Overview
10
+
11
+ 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:
12
+
13
+ - a frozen 14-pathology CheXpert-style classifier whose predictions are serialised into the prompt as a **Positive / Negative / Uncertain (PNU)** string, and
14
+ - an optional contrastive Stage 1 that pre-aligns the projection in a joint image–text space without ever loading the language model.
15
+
16
+ The full forward path can be summarised as:
17
+
18
+ $$
19
+ \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}}
20
+ $$
21
+
22
+ 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).
23
+
24
+ > 📊 **(Thêm biểu đồ — pipeline tổng quan):** sơ đồ end-to-end `CXR image → RAD-DINO (frozen) → MLP Projection (trainable) → Vicuna-7B + LoRA (trainable) → Findings/Impression/Answer`, với nhánh CheXpert classifier (frozen) bơm PNU string vào prompt. Tô màu xanh = frozen, cam = trainable. *(Đây là hình duy nhất bản cũ đã có — giữ lại làm Figure tổng quan.)*
25
+
26
+ ---
27
+
28
+ ## 2.2 Key Concepts and Definitions
29
+
30
+ This section defines the core concepts that the rest of the chapter relies on.
31
+
32
+ - **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.
33
+
34
+ - **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.
35
+
36
+ - **Visual Question Answering (VQA).** Given an image and a natural-language question, the model returns a short answer (often a single word or phrase).
37
+
38
+ - **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.
39
+
40
+ - **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.
41
+
42
+ - **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.
43
+
44
+ - **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.
45
+
46
+ - **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.
47
+
48
+ > 📊 **(Thêm biểu đồ — tuỳ chọn):** một hình nhỏ minh hoạ ViT patchification hoặc khái niệm contrastive (ảnh ↔ text kéo gần/đẩy xa) có thể đặt ở đây để minh hoạ định nghĩa, nếu muốn phần này trực quan hơn.
49
+
50
+ ---
51
+
52
+ ## 2.3 Data Preparation
53
+
54
+ The data pipeline runs in three phases, each tuned to a different bottleneck: **selection** runs on a local SSD where the MIMIC-CXR CSV metadata lives; **image transfer** runs on a cloud notebook so PhysioNet's authenticated transfer can saturate the network; and **resize / re-shard** runs once on a GPU host so 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.
55
+
56
+ > 📊 **(Thêm biểu đồ — A1, ưu tiên cao):** sơ đồ data pipeline 3 phase: `[SSD] Selection → [Cloud notebook] Image transfer (PhysioNet) → [GPU host] Resize + tar shard → Unified JSON`. Đánh dấu nơi chạy và artefact trung gian. *(Tương đương Figure 2.3 của report mẫu.)*
57
+
58
+ ### 2.3.1 Sources
59
+
60
+ All sources come from the public PhysioNet distribution under credentialed access; no manual annotation is performed.
61
+
62
+ | Source | Version | Used for |
63
+ |---|---|---|
64
+ | MIMIC-CXR | 2.1.0 | Radiology report `.txt` files |
65
+ | MIMIC-CXR-JPG | 2.1.0 | Pre-converted JPEG images |
66
+ | MIMIC-Ext-CXR-VQA | 1.0.0 | (image, question, answer) triples |
67
+ | `mimic-cxr-2.0.0-split` | — | Official patient-disjoint train/validate/test split |
68
+ | `mimic-cxr-2.0.0-metadata` | — | `ViewPosition` for frontal selection |
69
+ | `mimic-cxr-2.0.0-chexpert` | — | 14 pathology labels per study |
70
+
71
+ ### 2.3.2 Selection Pipeline
72
+
73
+ 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)**.
74
+
75
+ - **(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`.
76
+ - **(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.
77
+ - **(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.
78
+ - **(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**.
79
+
80
+ Distribution preservation is checked by comparing per-pathology prevalence at the raw, eligible-pool, and final-subset levels.
81
+
82
+ > 📊 **(Thêm biểu đồ — A2, ưu tiên cao):** sơ đồ phễu (funnel) thu hẹp dần qua 4 tầng `(a)→(b)→(c)→(d)`, ghi số study còn lại sau mỗi tầng: `~227k → … → 50k (40k/5k/5k)`.
83
+
84
+ ### 2.3.3 Storage and Consumption Layout
85
+
86
+ 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.
87
+
88
+ 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.
89
+
90
+ ### 2.3.4 Unified Instruction JSON
91
+
92
+ 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:
93
+
94
+ ```
95
+ { image_path, task, target, question, structured_findings, split, study_id, subject_id }
96
+ ```
97
+
98
+ Two knobs control sample creation:
99
+
100
+ - **`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.
101
+ - **`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.
102
+
103
+ | Mode | Behaviour |
104
+ |---|---|
105
+ | `split` | Two independent tasks; each sees only image + PNU string. |
106
+ | `split_cascade` | As above, but impression conditions on the GT findings string (findings → impression summarisation). |
107
+ | `merged` | One `report` task emitting both sections in a single pass. |
108
+
109
+ 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.
110
+
111
+ > 📊 **(Thêm biểu đồ — A3, ưu tiên trung bình):** sơ đồ rẽ nhánh `1 study → (frontal_only_split: 1 ảnh) → (split_cascade: sample findings [ảnh] + sample impression [ảnh + GT findings])`, minh hoạ quan hệ cascade findings→impression.
112
+
113
+ ---
114
+
115
+ ## 2.4 Model Architecture
116
+
117
+ 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.
118
+
119
+ ### 2.4.1 Image Encoder
120
+
121
+ - **Objective.** Turn a chest X-ray into a dense grid of patch features for the projection.
122
+ - **Input.** A 518×518 image (RAD-DINO's native resolution, matching the offline resize target).
123
+ - **Output.** Patch features `P ∈ R^{1369×768}`; the `[CLS]` token is discarded.
124
+ - **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.
125
+
126
+ > 📊 **(Thêm biểu đồ — B1, ưu tiên trung bình):** `518×518 image → ViT-B/14 patch 14×14 → grid 37×37 = 1369 tokens (768-d) [+ CLS bị loại]`.
127
+
128
+ ### 2.4.2 MLP Projection
129
+
130
+ - **Objective.** Pool a long, variable-length patch sequence into a fixed visual-token budget, and bridge the vision space (768-d) to the LLM space (4096-d).
131
+ - **Input.** Patch features `P ∈ R^{1369×768}`.
132
+ - **Output.** 32 visual tokens `V ∈ R^{32×4096}`; a 1024-d intermediate is also exposed as a tap point for the ITC head.
133
+ - **Core logic.** A perceiver-style block with 32 learnable query tokens. The forward pass is:
134
+
135
+ $$
136
+ \mathbf{H}^{(0)} = \operatorname{CrossAttn}(\mathbf{Q}_0, \mathbf{P}, \mathbf{P}) \in \mathbb{R}^{32\times 768}
137
+ $$
138
+ $$
139
+ \mathbf{H}^{(1)} = \operatorname{Dropout}(\operatorname{GELU}(\mathbf{W}_1 \mathbf{H}^{(0)})) \in \mathbb{R}^{32\times 1024}
140
+ $$
141
+ $$
142
+ \mathbf{V} = \mathbf{W}_2 \mathbf{H}^{(1)} \in \mathbb{R}^{32\times 4096}
143
+ $$
144
+
145
+ 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.
146
+
147
+ > 📊 **(Thêm biểu đồ — B2, ưu tiên cao):** chi tiết module: `P (1369×768) + 32 query Q₀ → CrossAttn 8-head → H⁰ (32×768) → Linear W₁+GELU+Dropout → H¹ (32×1024) [TAP → ITC head] → Linear W₂ → V (32×4096)`.
148
+
149
+ ### 2.4.3 CheXpert Abnormality Classifier
150
+
151
+ - **Objective.** Provide explicit abnormality cues to the prompt as a readable 3-class string instead of a logit vector.
152
+ - **Input.** The global `[CLS]` embedding of RAD-DINO.
153
+ - **Output.** A 14×3 logit tensor → one of {Positive, Negative, Uncertain} per pathology → the PNU string.
154
+ - **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:
155
+
156
+ ```
157
+ Positive Abnormalities: Cardiomegaly, Pleural Effusion
158
+ Negative Abnormalities: No Finding, Edema, Pneumothorax, ...
159
+ Uncertain Abnormalities: Atelectasis
160
+ ```
161
+
162
+ 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.
163
+
164
+ > 📊 **(Thêm biểu đồ — B3, ưu tiên trung bình):** `RAD-DINO [CLS] → MLP head → 14×3 logits → format_pnu() → PNU 3-section string → chèn vào prompt`. Phân biệt nhánh train (GT từ CSV) vs eval (classifier predict).
165
+
166
+ ### 2.4.4 Language Model and Parameter-Efficient Adaptation
167
+
168
+ - **Objective.** Generate the findings / impression / answer text conditioned on visual tokens and the prompt.
169
+ - **Input.** The assembled token sequence with the `<image>` placeholder replaced by 32 visual tokens.
170
+ - **Output.** The autoregressive text response.
171
+ - **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`.
172
+
173
+ > 📊 **(Thêm biểu đồ — B4, ưu tiên trung bình):** một transformer block: base weight W (4-bit NF4, frozen) + adapter `ΔW = B·A` (rank 16, trainable) trên q/k/v/o; FFN frozen. Kèm `h = Wx + (α/r)·BAx`.
174
+
175
+ ### 2.4.5 Prompt Assembly
176
+
177
+ All three tasks share one prompt skeleton, following Vicuna's v1.1 chat template:
178
+
179
+ ```
180
+ {SYSTEM_PROMPT} USER: <image>
181
+ {PNU structured findings}
182
+ {task-specific context block}
183
+ {instruction} ASSISTANT: {target}
184
+ ```
185
+
186
+ 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.
187
+
188
+ The task-specific context block is the main difference between `split` and `split_cascade`:
189
+
190
+ - **Findings** — block empty; produce the findings from image + PNU labels.
191
+ - **Impression** — block is the literal `Findings: <GT findings>`; the model conditions on the ground-truth findings and summarises it.
192
+ - **VQA** — block empty; the question becomes the instruction.
193
+
194
+ 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.
195
+
196
+ > 📊 **(Thêm biểu đồ — B5, ưu tiên cao):** hai phần — (1) *prompt anatomy*: thanh ngang chia slot tô màu `[SYSTEM][<image>][PNU][context: GT findings/empty][instruction][ASSISTANT: target]`; (2) *image-token expansion*: `1 <image> → 32 visual tokens`, mask & label nở thêm 31, vùng non-target gán −100.
197
+
198
+ ---
199
+
200
+ ## 2.5 Training Strategy
201
+
202
+ 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.
203
+
204
+ > 📊 **(Thêm biểu đồ — C1, ưu tiên cao):** curriculum 3 stage nối tiếp, mỗi khối ghi frozen/trainable + loss: `Stage 0 (CheXpert head, BCE)` → `Stage 1 (projection + ITC head, InfoNCE, LLM không load)` → `Stage 2 (projection + LoRA, causal CE)`. Mũi tên: projection weights Stage 1 → nạp vào Stage 2.
205
+
206
+ ### 2.5.1 Stage 0 — CheXpert Classifier Head
207
+
208
+ The PNU classifier is fitted before Stages 1 and 2. It is a small MLP on the frozen RAD-DINO `[CLS]` embedding, optimised under 14 independent binary cross-entropies on the `chex_*` manifest columns with U-MultiClass targets. Training takes minutes once the patch features are cached; the checkpoint is then loaded read-only by both later stages.
209
+
210
+ ### 2.5.2 Stage 1 — Contrastive Alignment
211
+
212
+ 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.
213
+
214
+ - **Image side.** The 32 intermediate 1024-d tokens are mean-pooled, projected to 128-d, and L2-normalised by the ITC head.
215
+ - **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.
216
+
217
+ Stage 1 minimises the symmetric InfoNCE loss:
218
+
219
+ $$
220
+ \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]
221
+ $$
222
+
223
+ **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).
224
+
225
+ 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).
226
+
227
+ > 📊 **(Thêm biểu đồ — C2, ưu tiên cao):** sơ đồ contrastive kiểu CLIP — nhánh image `image→RAD-DINO→projection→ITC head→mean-pool→128-d L2`, nhánh text offline `findings→CXR-BERT→128-d L2 cache`, ma trận tương đồng B×B (đường chéo = positive), InfoNCE đối xứng.
228
+
229
+ ### 2.5.3 Stage 2 — Instruction Tuning
230
+
231
+ Stage 2 rebuilds the full model with `load_llm = True`, loads the Stage 1 projection weights, and switches to the autoregressive objective. The dataset returns mixed batches by task weight (findings 30% / impression 20% / VQA 50% for MIMIC-CXR; weights renormalise when VQA is absent). The loss is the standard causal cross-entropy:
232
+
233
+ $$
234
+ \mathcal{L}_{\text{LM}} = -\frac{1}{|\mathcal{T}|}\sum_{(t,y)\in\mathcal{T}} \log p_\theta(y_t \mid y_{<t}, \mathbf{V}, \mathbf{c})
235
+ $$
236
+
237
+ **Where:** `T` is the set of positions where the label is not −100 (assistant response only), `V` the 32 visual tokens, and `c` the textual prompt context.
238
+
239
+ Trainable parameters are the projection's MLP and the LoRA adapters; the encoder and classifier stay frozen. Stage 2 runs for 10 epochs at peak LR `2e-4` with a 5% cosine warm-up; the effective batch size is fixed at 16 across hardware by trading per-device batch against gradient accumulation. The optimiser is AdamW with FP32 master weights for the projection and LoRA, the 4-bit base Vicuna acting as a quantised constant.
240
+
241
+ ### 2.5.4 Loss Masking and Image-Token Accounting
242
+
243
+ 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.
244
+
245
+ > 📊 **(Thêm biểu đồ — C3, ưu tiên trung bình):** dải token tuyến tính tô màu `[system −100][32 visual −100][PNU −100][instruction −100][pad −100]` vs `[assistant response: loss tính]`, minh hoạ chỉ vùng response đóng góp cross-entropy.
246
+
247
+ ### 2.5.5 Hyperparameters
248
+
249
+ | Hyperparameter | Stage 1 (ITC) | Stage 2 |
250
+ |---|---|---|
251
+ | Trainable modules | Projection + ITC head | Projection + LoRA |
252
+ | Frozen modules | Encoder, classifier; LLM not loaded | Encoder, classifier, base LLM |
253
+ | Loss | Symmetric InfoNCE | Causal CE |
254
+ | Temperature τ | 0.07 | — |
255
+ | Epochs | 2 | 10 |
256
+ | Peak learning rate | 1e-3 | 2e-4 |
257
+ | LR schedule | cosine, 5% warm-up | cosine, 5% warm-up |
258
+ | Weight decay | 0.01 | 0.01 |
259
+ | Effective batch size | 64–96 | 16 |
260
+ | Mixed precision | BF16 (Ampere+) / FP16 (T4) | BF16 / FP16 |
261
+ | LLM quantisation | — | 4-bit NF4, double-quant |
262
+ | LoRA (r, α, dropout) | — | (16, 32, 0.05) |
263
+ | LoRA modules | — | {q, k, v, o}_proj |
264
+ | Cutoff length | — | 512 tokens |
265
+ | Optimiser | AdamW | AdamW |
266
+
267
+ ---
268
+
269
+ ## 2.6 Evaluation Protocol
270
+
271
+ > **Ghi chú cho bạn:** report mẫu để toàn bộ metrics ở chương Experiments. Nếu theo đúng cấu trúc đó thì có thể chuyển mục 2.6 này sang Chapter 3. Tôi giữ ở đây để file method tự đủ; bạn quyết sau.
272
+
273
+ 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.
274
+
275
+ | Family | Metric | Tasks |
276
+ |---|---|---|
277
+ | Lexical n-gram | BLEU-1, BLEU-4 | findings, impression, VQA |
278
+ | | ROUGE-1/2/L | findings, impression |
279
+ | Fluency / synonym | METEOR | findings, impression, VQA |
280
+ | Semantic embedding | BERTScore F1 | findings, impression, VQA |
281
+ | Clinical accuracy | CheXbert macro-F1, P, R | findings, impression |
282
+ | Exact answer | Exact match, token F1 | VQA |
283
+ | Judge model | GPT-4o-mini score (0–5) | VQA (optional) |
284
+
285
+ ### 2.6.1 Tasks and Held-Out Data
286
+
287
+ 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.
288
+
289
+ > 📊 **(Thêm biểu đồ — D1, tuỳ chọn):** luồng evaluation `test image → [CheXpert PNU] (+GT findings nếu impression) → greedy decode → hypothesis → {NLG} + {CheXbert F1} + {VQA: EM/token-F1/judge}`.
290
+
291
+ ### 2.6.2 Natural Language Generation Metrics
292
+
293
+ - **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.
294
+ - **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).
295
+ - **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.
296
+ - **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.
297
+
298
+ ### 2.6.3 Clinical Correctness: CheXbert F1
299
+
300
+ 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.
301
+
302
+ > 📊 **(Thêm biểu đồ — D2, tuỳ chọn):** `generated report + reference report → CheXbert labeler → 2 vectors 14 nhãn → binarise → macro-F1/P/R`.
303
+
304
+ ### 2.6.4 Visual Question Answering Metrics
305
+
306
+ VQA targets are short, so the suite differs from generation.
307
+
308
+ - **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.
309
+ - **Token F1.** F1 between the bags of normalised tokens — the most diagnostic single number for short-answer correctness.
310
+ - **BLEU-1, METEOR, BERTScore.** Reported for symmetry with the generation tasks. BLEU-4 and ROUGE-L are omitted (rarely meaningful / subsumed by token F1).
311
+ - **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.
312
+
313
+ ### 2.6.5 Model Selection and Reporting
314
+
315
+ 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/stratified_sampling.docx ADDED
Binary file (29 kB). View file
 
docs/stratified_sampling.md ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stratified Sampling for the CXR-VLM Subset
2
+
3
+ > A complete, worked explanation of how the 50 k MIMIC-CXR subset is drawn from the ~140 k eligible pool so that every CheXpert pathology and every VQA question type ends up with the same relative frequency in the subset as in the pool.
4
+ >
5
+ > **English version → §1**
6
+ > **Vietnamese version → §2**
7
+
8
+ ---
9
+
10
+ ## §1. English version
11
+
12
+ ### 1.1. The problem this solves
13
+
14
+ After the filter chain (frontal only → both findings + impression → length outlier removal), we are left with an *eligible pool* of roughly 140 000 studies. We want to keep only 50 000 of them — 40 k for train, 5 k for val, 5 k for test — for cost and storage reasons.
15
+
16
+ The naive approach is to draw 50 000 studies **uniformly at random** from the pool. Mathematically this gives the correct *expected* distribution for every label, but it has a fatal flaw for rare pathologies: high variance.
17
+
18
+ Concretely: "Pleural Other" is positive in about 0.5 % of the pool, i.e. ~700 studies. If we draw 50 000 of 140 000, the count of Pleural Other in the subset follows a hypergeometric distribution. Some draws will give us 240 such studies, others will give us 180 — a relative variance of ±10 %. For VQA, the rare semantic types (e.g. "choose") suffer the same problem.
19
+
20
+ The training loss never sees individual labels, so this doesn't break training. But the **evaluation** suffers: with only 200 examples of a rare pathology in the test set, the per-pathology F1 score has wide confidence intervals — the noise drowns out the signal we want to measure.
21
+
22
+ **Stratified sampling** fixes this by giving rare-label studies a *guaranteed* quota instead of leaving them to chance.
23
+
24
+ ### 1.2. The algorithm — five steps
25
+
26
+ #### Step 1 — Compute pool-level pathology prevalence
27
+
28
+ Count, for each of the 14 CheXpert labels, how many studies in the eligible pool have that label positive (`= 1`).
29
+
30
+ ```
31
+ Pleural Other 700 / 140 000 = 0.50 %
32
+ Lung Lesion 2 940 / 140 000 = 2.10 %
33
+ Fracture 2 520 / 140 000 = 1.80 %
34
+ Pneumothorax 4 200 / 140 000 = 3.00 %
35
+ Pneumonia 12 600 / 140 000 = 9.00 %
36
+ Consolidation 11 200 / 140 000 = 8.00 %
37
+ Lung Opacity 30 800 / 140 000 = 22.00 %
38
+ Atelectasis 33 740 / 140 000 = 24.10 %
39
+ Pleural Effusion 29 400 / 140 000 = 21.00 %
40
+ Cardiomegaly 25 900 / 140 000 = 18.50 %
41
+ Edema 14 000 / 140 000 = 10.00 %
42
+ Enlarged Card. 11 200 / 140 000 = 8.00 %
43
+ Support Devices 51 800 / 140 000 = 37.00 %
44
+ No Finding 33 740 / 140 000 = 24.10 %
45
+ ```
46
+
47
+ (Numbers are illustrative but close to real MIMIC-CXR statistics. The point is the *ordering*.)
48
+
49
+ #### Step 2 — Sort labels by rarity
50
+
51
+ Lowest prevalence first:
52
+
53
+ ```
54
+ rare_order = [ Pleural Other, Fracture, Lung Lesion, Pneumothorax,
55
+ Consolidation, Enlarged Cardiomediastinum, Pneumonia,
56
+ Edema, Cardiomegaly, Pleural Effusion, Lung Opacity,
57
+ Atelectasis, No Finding, Support Devices ]
58
+ ```
59
+
60
+ #### Step 3 — Assign each study a single *stratum*
61
+
62
+ A study often has *multiple* positive labels. We need to put it in exactly one bucket. Rule: **walk `rare_order` top to bottom; the first label that is positive becomes the study's stratum**.
63
+
64
+ ```
65
+ Study with Cardiomegaly=1, Pleural_Effusion=1, others=0
66
+ → walk rare_order: Pleural_Other? no. Fracture? no. Lung_Lesion? no.
67
+ Pneumothorax? no. ... Cardiomegaly? YES.
68
+ → stratum = Cardiomegaly
69
+
70
+ Study with Pleural_Other=1, Cardiomegaly=1, others=0
71
+ → walk rare_order: Pleural_Other? YES.
72
+ → stratum = Pleural_Other (the rare label wins)
73
+
74
+ Study with No_Finding=1 only
75
+ → stratum = No_Finding
76
+
77
+ Study with all labels 0 or blank
78
+ → stratum = "None"
79
+ ```
80
+
81
+ This rule has a critical consequence: every study positive for a rare label is *captured* into that label's stratum, where it cannot be drowned out by more common companions.
82
+
83
+ After this pass, every study has exactly one stratum. Count studies per stratum:
84
+
85
+ ```
86
+ Pleural_Other ~ 600 (some of the 700 PO studies have an even rarer label)
87
+ Fracture ~ 2 200
88
+ Lung_Lesion ~ 2 500
89
+ Pneumothorax ~ 3 500
90
+ Consolidation ~ 9 000
91
+ Enlarged Cardiomediast. ~ 9 000
92
+ Pneumonia ~10 000
93
+ Edema ~11 000
94
+ Cardiomegaly ~18 000
95
+ Pleural_Effusion ~12 000
96
+ Lung_Opacity ~ 8 000
97
+ Atelectasis ~ 6 200
98
+ No_Finding ~33 000
99
+ None ~15 000
100
+ -------
101
+ TOTAL 140 000
102
+ ```
103
+
104
+ Note that the stratum size for Cardiomegaly (~18 000) is *less than* the number of studies positive for Cardiomegaly (~25 900). The ~7 900 difference are studies where Cardiomegaly is positive but a rarer label is also positive, so they ended up in that rarer stratum. This is the whole point.
105
+
106
+ #### Step 4 — Allocate a sampling quota per stratum, proportional to stratum size
107
+
108
+ ```python
109
+ quota[j] = round( |stratum_j| / N × n )
110
+ ```
111
+
112
+ with N = 140 000, n = 50 000. So the sampling rate is n/N = 35.7 %.
113
+
114
+ ```
115
+ Pleural_Other 600 → quota = round(600 × 0.357) = 214
116
+ Fracture 2 200 → quota = 786
117
+ Lung_Lesion 2 500 → quota = 893
118
+ Pneumothorax 3 500 → quota = 1 250
119
+ Consolidation 9 000 → quota = 3 214
120
+ Enlarged Card. 9 000 → quota = 3 214
121
+ Pneumonia 10 000 → quota = 3 571
122
+ Edema 11 000 → quota = 3 929
123
+ Cardiomegaly 18 000 → quota = 6 429
124
+ Pleural_Effusion 12 000 → quota = 4 286
125
+ Lung_Opacity 8 000 → quota = 2 857
126
+ Atelectasis 6 200 → quota = 2 214
127
+ No_Finding 33 000 → quota = 11 786
128
+ None 15 000 → quota = 5 357
129
+ -------
130
+ TOTAL 50 000 ✓
131
+ ```
132
+
133
+ Rounding can push the total slightly off 50 000. The code adjusts:
134
+
135
+ ```python
136
+ while alloc.sum() != n:
137
+ diff = n - alloc.sum()
138
+ k = alloc.idxmax() if diff < 0 else alloc.idxmin()
139
+ alloc[k] += np.sign(diff)
140
+ ```
141
+
142
+ (Take a unit from / add a unit to the largest / smallest stratum until the total matches.)
143
+
144
+ #### Step 5 — Draw uniformly within each stratum, preferring `has_vqa`
145
+
146
+ For each stratum, pick `quota[j]` studies. Within the stratum we have two pools: those whose `dicom_id` has at least one VQA question (`has_vqa = True`) and those without. Take the VQA-bearing ones first, then top up with non-VQA studies if the quota is not yet filled.
147
+
148
+ ```python
149
+ g_vqa = stratum[stratum.has_vqa]
150
+ g_rest = stratum[~stratum.has_vqa]
151
+
152
+ take_vqa = min(quota[j], len(g_vqa))
153
+ sel = g_vqa.sample(take_vqa, random_state=seed)
154
+ if quota[j] - take_vqa > 0:
155
+ sel = pd.concat([sel, g_rest.sample(quota[j] - take_vqa, random_state=seed)])
156
+ ```
157
+
158
+ This is how VQA distribution is preserved as a side effect: across all strata combined, we soak up most of the available VQA pairs without disturbing the pathology quotas.
159
+
160
+ The resulting 50 000 studies form the *subset*.
161
+
162
+ ### 1.3. Why this preserves prevalence — the math, honestly
163
+
164
+ The cleanest way to see it: under the proportional allocation above, every study in the pool has an approximately equal probability of being sampled.
165
+
166
+ ```
167
+ P(study s sampled) = quota[stratum(s)] / |stratum(s)|
168
+ ≈ ( |stratum(s)| / N × n ) / |stratum(s)|
169
+ = n / N
170
+ = 35.7 %
171
+ ```
172
+
173
+ So:
174
+
175
+ ```
176
+ E[ count of label k in subset ]
177
+ = Σ over s in pool : P(s sampled) × label_k(s)
178
+ ≈ (n/N) × ( Σ over s : label_k(s) )
179
+ = (n/N) × (# studies positive for k in pool)
180
+ = (n/N) × pos_k
181
+
182
+ E[ prevalence of k in subset ]
183
+ = E[count_k] / n
184
+ = pos_k / N
185
+ = pool prevalence of k ✓
186
+ ```
187
+
188
+ The expected prevalence in the subset equals the pool prevalence — for **every** label, not just the strata-defining ones. This is because rare labels propagate through their own strata, and common labels propagate through every stratum proportionally.
189
+
190
+ ### 1.4. Honest caveat — rounding causes a small drift
191
+
192
+ The argument "P(s sampled) = n/N" used `quota[j] ≈ (|stratum_j| / N) × n`. The "≈" hides rounding.
193
+
194
+ Toy example to expose it. Pool of 1 000 studies, target 100. Strata:
195
+
196
+ | Stratum | size | exact quota | rounded quota | actual rate |
197
+ |---|---|---|---|---|
198
+ | Pleural Other | 5 | 0.5 | **1** | 1/5 = 20 % |
199
+ | Lung Lesion | 18 | 1.8 | **2** | 2/18 = 11.1 % |
200
+ | Cardiomegaly | 170 | 17.0 | **17** | 17/170 = 10.0 % |
201
+ | No Finding | 290 | 29.0 | **29** | 29/290 = 10.0 % |
202
+ | None | 517 | 51.7 | **51** | 51/517 = 9.86 % |
203
+ | **Total** | 1 000 | 100 | 100 | mean ≈ 10 % |
204
+
205
+ The Pleural Other stratum is oversampled (20 % instead of 10 %) because rounding pushed 0.5 up to 1. This *bleeds into* Cardiomegaly prevalence depending on how many Pleural-Other studies also have Cardiomegaly:
206
+
207
+ ```
208
+ E[ Cardiomegaly count in subset ]
209
+ = 0.20 × x_PO + 0.111 × x_LL + 0.10 × 170 + 0 + 0
210
+ ```
211
+
212
+ where `x_PO` is the number of Pleural-Other studies that *also* have Cardiomegaly = 1, and `x_LL` similarly for Lung Lesion. We know `x_PO + x_LL + 170 = 180` (the pool total for Cardiomegaly), so `x_PO + x_LL = 10`. Three scenarios:
213
+
214
+ | co-occurrence | x_PO | x_LL | E[count] | subset prev | pool prev | \|Δ\| |
215
+ |---|---|---|---|---|---|---|
216
+ | all PO have Cardio | 5 | 5 | 18.56 | 18.56 % | 18.0 % | 0.56 % |
217
+ | mixed | 2 | 8 | 18.29 | 18.29 % | 18.0 % | 0.29 % |
218
+ | no PO has Cardio | 0 | 10 | 18.11 | 18.11 % | 18.0 % | 0.11 % |
219
+
220
+ So:
221
+
222
+ - The expected prevalence in the subset is *not exactly* the pool prevalence; it drifts by an amount that depends on the joint distribution of labels.
223
+ - The drift is bounded by `max_j | quota_j/|stratum_j| − n/N |` × `max co-occurrence intensity`.
224
+ - For our real data (pool 140 k, target 50 k, smallest stratum ≈ 600), the per-stratum rate deviation is at most ±0.001, so the prevalence drift is at most a few tenths of a percentage point. This is what the verification cell reports.
225
+
226
+ ### 1.5. What if the verification fails? (|Δ| > 1 %)
227
+
228
+ The notebook does **not** retry. Verification is diagnostic. If it fails, debug. Four root causes:
229
+
230
+ #### A. A stratum is too small for its computed quota
231
+
232
+ If `|stratum_j|` is less than `quota_j`, the code clips with `k = min(k, len(g))` and fills the deficit with random rows from the whole pool. Those random rows are not stratum-aware, so they skew the distribution.
233
+
234
+ *Symptom*: |Δ| is positive for the common labels (No Finding, Support Devices) — they got the random-fill spillover.
235
+
236
+ *Fix*: enlarge the pool (relax filter b or c) so the rare stratum has more candidates, *or* merge tiny strata into a single "Other rare" bucket, *or* reduce the target `n` until quotas fit.
237
+
238
+ #### B. The stratum definition is wrong
239
+
240
+ E.g. `rare_order` was computed from the raw full distribution instead of the eligible-pool distribution; or the stratum assignment treats the CheXpert "uncertain" (-1) values as positive.
241
+
242
+ *Symptom*: |Δ| is huge on one or two specific labels, the rest are fine.
243
+
244
+ *Fix*: print `rare_order` and the stratum count table; reproduce by hand for a few sample studies; correct the comparison operator (`== 1` vs `!= 0`).
245
+
246
+ #### C. The eligible-pool distribution is biased relative to raw full MIMIC
247
+
248
+ Filter (b) "must have findings and impression" disproportionately removes short reports (often `Normal.` impressions with no findings), which are the bulk of `No Finding` studies. Filter (c) IQR removes the long-tail complex cases (often multi-positive studies).
249
+
250
+ *Symptom*: `|Δ|` between the *eligible* column and the *raw_full* column is the big one; subset matches eligible just fine.
251
+
252
+ *Fix*: this is not a bug, it is a documented trade-off. State explicitly in the thesis that the model is trained and evaluated on the "well-reported subset" of MIMIC-CXR, and that the prevalence shift from raw full is the cost of the report-quality filter.
253
+
254
+ #### D. CSV merge bug — silent NaN
255
+
256
+ If the chexpert CSV is missing rows for some studies, the merge produces NaN values that get coerced to 0 (negative). Distribution warps.
257
+
258
+ *Symptom*: many labels drift simultaneously in the same direction; the "None" stratum is unusually large.
259
+
260
+ *Fix*: `elig[label_cols].isna().sum()` should be zero; verify pre- and post-merge row counts.
261
+
262
+ ### 1.6. Why iterative methods are unnecessary here
263
+
264
+ A reader familiar with survey sampling may ask: "Why not raking / iterative proportional fitting?" Raking iteratively rescales row weights to match multiple marginals simultaneously (e.g. pathology *and* VQA type *and* answer format), and converges to a joint distribution that respects all of them.
265
+
266
+ For this project, one-shot stratified sampling is sufficient because:
267
+
268
+ 1. We only directly stratify on **one axis** (rarest-positive pathology). VQA type is preserved indirectly via the `has_vqa` preference inside each stratum (§1.2 step 5) and via the correlation between pathology and VQA-question topic. Empirically the verification confirms VQA type drift is small.
269
+ 2. The eligible pool is large (140 k); strata sizes are in the hundreds-to-tens-of-thousands range, so rounding error is tiny.
270
+ 3. The seed is fixed; the entire selection is reproducible bit-for-bit. An iterative method would still be reproducible but introduces dependence on convergence tolerance and iteration order, which makes paper reviewers less comfortable.
271
+
272
+ Raking only earns its complexity when you need to balance ≥ 2 axes that don't reduce to one. If a future revision wants to stratify on pathology × VQA semantic type × answer format, then raking is the appropriate tool. Until then, stratified one-shot is the right choice.
273
+
274
+ ### 1.7. Patient-disjoint splits
275
+
276
+ Stratification gives us a 50 k subset. Splitting it into 40 k / 5 k / 5 k still needs care: a single patient (subject_id) may have several studies, and if any patient appears in two splits, the test metrics are inflated because the model has memorised that patient's anatomy from train.
277
+
278
+ MIMIC ships an official patient-disjoint split CSV. The "carve" logic uses it as the primary source:
279
+
280
+ 1. Take the val and test pools from MIMIC's official validate / test splits, stratify-sample each to the target size.
281
+ 2. Remove every subject_id chosen for val or test from the train candidate pool.
282
+ 3. Stratify-sample 40 k from what remains.
283
+ 4. If MIMIC's official val or test pool is too small after the filter chain, top up from the train pool — but every subject borrowed this way is removed from train, so disjointness is preserved.
284
+
285
+ The final three sets share no subject_id. A patient's entire study history lives in exactly one split.
286
+
287
+ ### 1.8. Putting it together — what the verifier actually shows
288
+
289
+ After `carve_split` returns, the notebook computes a 14 × 3 table of prevalences:
290
+
291
+ ```
292
+ raw_full(%) eligible(%) subset(%) |Δ| subset-eligible
293
+ No Finding 22.4 24.1 24.0 0.10
294
+ Cardiomegaly 17.8 18.5 18.6 0.10
295
+ Pleural Effusion 21.5 21.0 20.9 0.10
296
+ Atelectasis 21.3 24.1 23.8 0.30
297
+ Pneumonia 7.9 9.0 9.0 0.00
298
+ Pneumothorax 4.6 3.0 3.0 0.00
299
+ Consolidation 4.0 8.0 7.9 0.10
300
+ Edema 9.6 10.0 10.0 0.00
301
+ Enl. Cardiomed. 7.5 8.0 8.0 0.00
302
+ Lung Opacity 21.2 22.0 22.0 0.00
303
+ Lung Lesion 2.7 2.1 2.1 0.00
304
+ Fracture 1.9 1.8 1.8 0.00
305
+ Pleural Other 0.42 0.51 0.58 0.07
306
+ Support Devices 22.1 37.0 36.7 0.30
307
+ ```
308
+
309
+ Two `|Δ|` columns matter:
310
+
311
+ - `|Δ| subset − eligible` < 0.7 % everywhere → the stratification worked.
312
+ - `|Δ| eligible − raw_full` is bigger on some labels (e.g. Support Devices 22 % → 37 %) → the report-quality filter biased the pool. This is a known trade-off, reported in the thesis as a limitation.
313
+
314
+ A second pass (cell 17) checks VQA distribution across `semantic_type`, `content_type`, and `answer_type`, plotted as side-by-side bars of full-VQA vs subset-VQA percentages. In our run all three axes show drift below 1 percentage point per category — sufficient for an unbiased evaluation.
315
+
316
+ ---
317
+
318
+ ## §2. Phiên bản tiếng Việt
319
+
320
+ ### 2.1. Vấn đề cần giải quyết
321
+
322
+ Sau chuỗi filter (frontal only → có cả findings + impression → IQR outlier), eligible pool còn khoảng 140 000 study. Cần chọn 50 000 — 40 k train, 5 k val, 5 k test — vì lý do chi phí và lưu trữ.
323
+
324
+ Cách naive là **random uniform** 50 000 trong 140 000. Toán học đảm bảo expected distribution đúng cho mọi label, nhưng có một nhược điểm chết người cho các nhãn hiếm: **variance lớn**.
325
+
326
+ Cụ thể: "Pleural Other" chiếm khoảng 0.5 % pool, tức ~700 study. Khi lấy 50 000 từ 140 000, số Pleural Other trong subset theo phân phối hypergeometric. Lần thì 240, lần thì 180 — biến thiên tương đối ±10 %. Với VQA, các semantic_type hiếm (vd "choose") cũng có vấn đề tương tự.
327
+
328
+ Loss function không nhìn từng nhãn, nên việc này không phá training. Nhưng **evaluation** gặp khó: test set có 200 sample của 1 pathology hiếm thì F1 trên pathology đó có confidence interval rộng — nhiễu lấn át tín hiệu cần đo.
329
+
330
+ **Stratified sampling** sửa bằng cách cấp quota *cố định* cho các study có nhãn hiếm thay vì để may rủi quyết định.
331
+
332
+ ### 2.2. Thuật toán — 5 bước
333
+
334
+ #### Bước 1 — Tính prevalence từng pathology trong pool
335
+
336
+ Đếm số study trong eligible pool có mỗi nhãn dương tính (`= 1`).
337
+
338
+ ```
339
+ Pleural Other 700 / 140 000 = 0.50 %
340
+ Lung Lesion 2 940 / 140 000 = 2.10 %
341
+ Fracture 2 520 / 140 000 = 1.80 %
342
+ Pneumothorax 4 200 / 140 000 = 3.00 %
343
+ Pneumonia 12 600 / 140 000 = 9.00 %
344
+ Consolidation 11 200 / 140 000 = 8.00 %
345
+ Lung Opacity 30 800 / 140 000 = 22.00 %
346
+ Atelectasis 33 740 / 140 000 = 24.10 %
347
+ Pleural Effusion 29 400 / 140 000 = 21.00 %
348
+ Cardiomegaly 25 900 / 140 000 = 18.50 %
349
+ Edema 14 000 / 140 000 = 10.00 %
350
+ Enlarged Card. 11 200 / 140 000 = 8.00 %
351
+ Support Devices 51 800 / 140 000 = 37.00 %
352
+ No Finding 33 740 / 140 000 = 24.10 %
353
+ ```
354
+
355
+ (Số minh hoạ, gần đúng với thống kê thực của MIMIC-CXR. Quan trọng là *thứ tự*.)
356
+
357
+ #### Bước 2 — Sắp nhãn theo độ hiếm
358
+
359
+ Hiếm trước, phổ biến sau:
360
+
361
+ ```
362
+ rare_order = [ Pleural Other, Fracture, Lung Lesion, Pneumothorax,
363
+ Consolidation, Enlarged Cardiomediastinum, Pneumonia,
364
+ Edema, Cardiomegaly, Pleural Effusion, Lung Opacity,
365
+ Atelectasis, No Finding, Support Devices ]
366
+ ```
367
+
368
+ #### Bước 3 — Gán mỗi study đúng một *stratum*
369
+
370
+ Mỗi study thường có *nhiều* nhãn dương. Cần đưa vào đúng một nhóm. Quy tắc: **duyệt `rare_order` từ trên xuống; nhãn đầu tiên dương sẽ là stratum**.
371
+
372
+ ```
373
+ Study có Cardiomegaly=1, Pleural_Effusion=1, các nhãn khác=0
374
+ → duyệt rare_order: Pleural_Other? không. Fracture? không. Lung_Lesion? không.
375
+ Pneumothorax? không. ... Cardiomegaly? CÓ.
376
+ → stratum = Cardiomegaly
377
+
378
+ Study có Pleural_Other=1, Cardiomegaly=1, khác=0
379
+ → duyệt rare_order: Pleural_Other? CÓ.
380
+ → stratum = Pleural_Other (nhãn hiếm thắng)
381
+
382
+ Study chỉ có No_Finding=1
383
+ → stratum = No_Finding
384
+
385
+ Study tất cả nhãn 0 hoặc blank
386
+ → stratum = "None"
387
+ ```
388
+
389
+ Quy tắc này có hệ quả quan trọng: mọi study dương tính với một nhãn hiếm đều bị *bắt* vào stratum của nhãn đó, không bị các nhãn phổ biến đi kèm "che lấp".
390
+
391
+ Sau bước này, mỗi study có đúng 1 stratum. Đếm số study mỗi stratum:
392
+
393
+ ```
394
+ Pleural_Other ~ 600 (một số trong 700 PO study có nhãn hiếm hơn nữa)
395
+ Fracture ~ 2 200
396
+ Lung_Lesion ~ 2 500
397
+ Pneumothorax ~ 3 500
398
+ Consolidation ~ 9 000
399
+ Enlarged Cardiomediast. ~ 9 000
400
+ Pneumonia ~10 000
401
+ Edema ~11 000
402
+ Cardiomegaly ~18 000
403
+ Pleural_Effusion ~12 000
404
+ Lung_Opacity ~ 8 000
405
+ Atelectasis ~ 6 200
406
+ No_Finding ~33 000
407
+ None ~15 000
408
+ -------
409
+ TỔNG 140 000
410
+ ```
411
+
412
+ Lưu ý: stratum Cardiomegaly (~18 000) *nhỏ hơn* số study dương tính Cardiomegaly (~25 900). Chênh lệch ~7 900 là các study có Cardiomegaly nhưng cũng có một nhãn hiếm hơn, nên đã vào stratum hiếm hơn. **Đây chính là ý đồ thiết kế**.
413
+
414
+ #### Bước 4 — Tính quota cho từng stratum, tỉ lệ với kích thước stratum
415
+
416
+ ```python
417
+ quota[j] = round( |stratum_j| / N × n )
418
+ ```
419
+
420
+ với N = 140 000, n = 50 000. Sampling rate là n/N = 35.7 %.
421
+
422
+ ```
423
+ Pleural_Other 600 → quota = round(600 × 0.357) = 214
424
+ Fracture 2 200 → quota = 786
425
+ Lung_Lesion 2 500 → quota = 893
426
+ Pneumothorax 3 500 → quota = 1 250
427
+ Consolidation 9 000 → quota = 3 214
428
+ Enlarged Card. 9 000 → quota = 3 214
429
+ Pneumonia 10 000 → quota = 3 571
430
+ Edema 11 000 → quota = 3 929
431
+ Cardiomegaly 18 000 → quota = 6 429
432
+ Pleural_Effusion 12 000 → quota = 4 286
433
+ Lung_Opacity 8 000 → quota = 2 857
434
+ Atelectasis 6 200 → quota = 2 214
435
+ No_Finding 33 000 → quota = 11 786
436
+ None 15 000 → quota = 5 357
437
+ -------
438
+ TỔNG 50 000 ✓
439
+ ```
440
+
441
+ Rounding có thể làm tổng lệch khỏi 50 000. Code điều chỉnh:
442
+
443
+ ```python
444
+ while alloc.sum() != n:
445
+ diff = n - alloc.sum()
446
+ k = alloc.idxmax() if diff < 0 else alloc.idxmin()
447
+ alloc[k] += np.sign(diff)
448
+ ```
449
+
450
+ (Trừ/cộng 1 đơn vị từ stratum lớn nhất/nhỏ nhất cho đến khi tổng khớp.)
451
+
452
+ #### Bước 5 — Random pick trong từng stratum, ưu tiên `has_vqa`
453
+
454
+ Cho mỗi stratum, lấy `quota[j]` study. Trong stratum có 2 pool: những study mà `dicom_id` có ít nhất 1 câu hỏi VQA (`has_vqa = True`) và những study không có. Lấy có-VQA trước, nếu chưa đủ quota thì bù bằng study không-VQA.
455
+
456
+ ```python
457
+ g_vqa = stratum[stratum.has_vqa]
458
+ g_rest = stratum[~stratum.has_vqa]
459
+
460
+ take_vqa = min(quota[j], len(g_vqa))
461
+ sel = g_vqa.sample(take_vqa, random_state=seed)
462
+ if quota[j] - take_vqa > 0:
463
+ sel = pd.concat([sel, g_rest.sample(quota[j] - take_vqa, random_state=seed)])
464
+ ```
465
+
466
+ Đây là cách phân phối VQA được giữ ngầm: cộng dồn mọi stratum, ta vớt được hầu hết VQA pairs khả dụng mà không phá vỡ quota pathology.
467
+
468
+ 50 000 study thu được là *subset* cuối cùng.
469
+
470
+ ### 2.3. Tại sao prevalence được giữ — toán học, nói thẳng
471
+
472
+ Cách dễ thấy nhất: dưới proportional allocation, **mọi study trong pool có xác suất sampling gần như bằng nhau**.
473
+
474
+ ```
475
+ P(study s được chọn) = quota[stratum(s)] / |stratum(s)|
476
+ ≈ ( |stratum(s)| / N × n ) / |stratum(s)|
477
+ = n / N
478
+ = 35.7 %
479
+ ```
480
+
481
+ Do đó:
482
+
483
+ ```
484
+ E[ số study có nhãn k trong subset ]
485
+ = Σ over s in pool : P(s sampled) × label_k(s)
486
+ ≈ (n/N) × ( Σ over s : label_k(s) )
487
+ = (n/N) × (số study pool dương với k)
488
+ = (n/N) × pos_k
489
+
490
+ E[ prevalence k trong subset ]
491
+ = E[count_k] / n
492
+ = pos_k / N
493
+ = prevalence k trong pool ✓
494
+ ```
495
+
496
+ Expected prevalence trong subset bằng prevalence pool — cho **mọi** nhãn, không chỉ các nhãn dùng làm stratum. Vì nhãn hiếm lan qua chính stratum của nó, nhãn phổ biến lan qua mọi stratum theo tỉ lệ.
497
+
498
+ ### 2.4. Caveat trung thực — rounding gây drift nhỏ
499
+
500
+ Lập luận "P(s sampled) = n/N" dùng `quota[j] ≈ (|stratum_j| / N) × n`. Dấu "≈" che giấu rounding.
501
+
502
+ Ví dụ nhỏ phơi ra vấn đề. Pool 1 000 study, target 100. Các stratum:
503
+
504
+ | Stratum | size | quota chuẩn | quota làm tròn | sampling rate thực |
505
+ |---|---|---|---|---|
506
+ | Pleural Other | 5 | 0.5 | **1** | 1/5 = 20 % |
507
+ | Lung Lesion | 18 | 1.8 | **2** | 2/18 = 11.1 % |
508
+ | Cardiomegaly | 170 | 17.0 | **17** | 17/170 = 10.0 % |
509
+ | No Finding | 290 | 29.0 | **29** | 29/290 = 10.0 % |
510
+ | None | 517 | 51.7 | **51** | 51/517 = 9.86 % |
511
+ | **Tổng** | 1 000 | 100 | 100 | trung bình ≈ 10 % |
512
+
513
+ Stratum Pleural Other bị oversample (20 % thay vì 10 %) vì rounding 0.5 → 1. Drift này *lan sang* prevalence Cardiomegaly tuỳ theo bao nhiêu study Pleural Other cũng có Cardiomegaly:
514
+
515
+ ```
516
+ E[ Cardiomegaly count trong subset ]
517
+ = 0.20 × x_PO + 0.111 × x_LL + 0.10 × 170 + 0 + 0
518
+ ```
519
+
520
+ `x_PO` = số study Pleural Other *cũng* có Cardiomegaly = 1, `x_LL` tương tự cho Lung Lesion. Ta biết `x_PO + x_LL + 170 = 180` (tổng pool Cardiomegaly), nên `x_PO + x_LL = 10`. 3 kịch bản:
521
+
522
+ | co-occurrence | x_PO | x_LL | E[count] | subset prev | pool prev | \|Δ\| |
523
+ |---|---|---|---|---|---|---|
524
+ | mọi PO có Cardio | 5 | 5 | 18.56 | 18.56 % | 18.0 % | 0.56 % |
525
+ | trộn | 2 | 8 | 18.29 | 18.29 % | 18.0 % | 0.29 % |
526
+ | không PO nào có Cardio | 0 | 10 | 18.11 | 18.11 % | 18.0 % | 0.11 % |
527
+
528
+ Vậy:
529
+
530
+ - Expected prevalence trong subset *không bằng chính xác* pool prevalence; nó drift một lượng phụ thuộc co-occurrence của các nhãn.
531
+ - Drift bị bound bởi `max_j | quota_j/|stratum_j| − n/N |` × `max co-occurrence intensity`.
532
+ - Với data thật (pool 140 k, target 50 k, stratum nhỏ nhất ≈ 600), độ lệch rate per-stratum tối đa ±0.001, nên drift prevalence tối đa vài phần mười %. Đây là con số verification cell báo cáo.
533
+
534
+ ### 2.5. Nếu verification fail (|Δ| > 1 %) thì sao?
535
+
536
+ Notebook **không** retry. Verification là chẩn đoán. Nếu fail, debug. 4 nguyên nhân gốc:
537
+
538
+ #### A. Stratum quá nhỏ so với quota tính được
539
+
540
+ Nếu `|stratum_j|` nhỏ hơn `quota_j`, code clip `k = min(k, len(g))` rồi lấp phần thiếu bằng random từ toàn pool. Random rows này không stratum-aware → skew distribution.
541
+
542
+ *Triệu chứng*: |Δ| dương cho các nhãn phổ biến (No Finding, Support Devices) — chúng nhận phần lấp random.
543
+
544
+ *Fix*: nới pool eligible (giảm filter b/c), hoặc gộp các stratum nhỏ thành một bucket "Other rare", hoặc giảm target `n` để quota vừa.
545
+
546
+ #### B. Định nghĩa stratum sai
547
+
548
+ Vd: `rare_order` tính từ raw full thay vì eligible pool; hoặc gán stratum coi CheXpert "uncertain" (-1) là positive.
549
+
550
+ *Triệu chứng*: |Δ| cực lớn trên 1-2 nhãn cụ thể, các nhãn còn lại bình thường.
551
+
552
+ *Fix*: print `rare_order` và bảng đếm stratum; tái hiện bằng tay vài study mẫu; sửa toán tử so sánh (`== 1` vs `!= 0`).
553
+
554
+ #### C. Eligible pool đã lệch so với raw full MIMIC
555
+
556
+ Filter (b) "có cả F + I" cắt nhiều report ngắn (kiểu `Normal.` impression không có findings), vốn chủ yếu là `No Finding` study. Filter (c) IQR cắt long-tail case phức tạp (study đa nhãn).
557
+
558
+ *Triệu chứng*: `|Δ|` giữa cột *eligible* và cột *raw_full* là cái lớn; subset vẫn khớp eligible tốt.
559
+
560
+ *Fix*: đây không phải bug, là trade-off đã chọn. Ghi rõ trong thesis: model train + eval trên "well-reported subset" của MIMIC-CXR, prevalence shift so với raw full là cái giá phải trả cho report-quality filter.
561
+
562
+ #### D. CSV merge bug — NaN âm thầm
563
+
564
+ Nếu chexpert CSV thiếu rows cho 1 số study, merge sinh NaN, bị ép về 0 (negative). Distribution bị warp.
565
+
566
+ *Triệu chứng*: nhiều nhãn cùng drift một hướng; stratum "None" lớn bất thường.
567
+
568
+ *Fix*: `elig[label_cols].isna().sum()` phải bằng 0; verify số row trước và sau merge.
569
+
570
+ ### 2.6. Vì sao không cần iterative methods ở đây
571
+
572
+ Người đọc quen survey sampling có thể hỏi: "Sao không dùng raking / iterative proportional fitting?" Raking lặp lại rescale weight để khớp nhiều marginal cùng lúc (vd pathology *và* VQA type *và* answer format), hội tụ về phân phối joint tôn trọng tất cả các trục đó.
573
+
574
+ Với project này, one-shot stratified đủ vì:
575
+
576
+ 1. Chỉ trực tiếp stratify trên **một trục** (rare-positive pathology). VQA type được giữ gián tiếp qua `has_vqa` preference trong từng stratum (§2.2 bước 5) và qua tương quan giữa pathology và topic câu hỏi VQA. Empirically verification xác nhận VQA type drift nhỏ.
577
+ 2. Eligible pool lớn (140 k); kích thước stratum từ hàng trăm đến hàng chục nghìn, rounding error nhỏ.
578
+ 3. Seed cố định; toàn bộ selection reproducible từng bit. Iterative cũng reproducible nhưng phụ thuộc convergence tolerance và thứ tự iteration → reviewer kém thoải mái hơn.
579
+
580
+ Raking chỉ đáng phức tạp khi cần balance ≥ 2 trục không quy về 1 được. Nếu sau này muốn stratify trên pathology × VQA semantic type × answer format, lúc đó raking đúng tool. Hiện tại, stratified one-shot là lựa chọn đúng.
581
+
582
+ ### 2.7. Patient-disjoint splits
583
+
584
+ Stratification cho ta subset 50 k. Chia 40 k / 5 k / 5 k vẫn phải cẩn thận: một bệnh nhân (subject_id) có thể có nhiều study, và nếu bệnh nhân xuất hiện ở 2 split, test metric bị inflate vì model đã "nhớ" giải phẫu bệnh nhân đó từ train.
585
+
586
+ MIMIC ship sẵn split CSV patient-disjoint chính thức. Logic "carve" dùng nó làm nguồn chính:
587
+
588
+ 1. Lấy val và test pool từ MIMIC validate / test chính thức, stratify-sample mỗi cái đến target size.
589
+ 2. Loại mọi subject_id đã chọn cho val hoặc test khỏi train candidate pool.
590
+ 3. Stratify-sample 40 k từ phần còn lại.
591
+ 4. Nếu pool val/test chính thức của MIMIC quá nhỏ sau filter chain, bù từ train pool — nhưng mọi subject mượn theo cách này bị loại khỏi train, nên disjointness được bảo toàn.
592
+
593
+ 3 set cuối không chia sẻ subject_id nào. Toàn bộ study history của 1 bệnh nhân nằm chính xác trong 1 split.
594
+
595
+ ### 2.8. Tóm lại — verifier thực sự cho thấy gì
596
+
597
+ Sau khi `carve_split` trả về, notebook tính bảng 14 × 3 prevalence:
598
+
599
+ ```
600
+ raw_full(%) eligible(%) subset(%) |Δ| subset-eligible
601
+ No Finding 22.4 24.1 24.0 0.10
602
+ Cardiomegaly 17.8 18.5 18.6 0.10
603
+ Pleural Effusion 21.5 21.0 20.9 0.10
604
+ Atelectasis 21.3 24.1 23.8 0.30
605
+ Pneumonia 7.9 9.0 9.0 0.00
606
+ Pneumothorax 4.6 3.0 3.0 0.00
607
+ Consolidation 4.0 8.0 7.9 0.10
608
+ Edema 9.6 10.0 10.0 0.00
609
+ Enl. Cardiomed. 7.5 8.0 8.0 0.00
610
+ Lung Opacity 21.2 22.0 22.0 0.00
611
+ Lung Lesion 2.7 2.1 2.1 0.00
612
+ Fracture 1.9 1.8 1.8 0.00
613
+ Pleural Other 0.42 0.51 0.58 0.07
614
+ Support Devices 22.1 37.0 36.7 0.30
615
+ ```
616
+
617
+ Hai cột `|Δ|` đáng xem:
618
+
619
+ - `|Δ| subset − eligible` < 0.7 % khắp nơi → stratification work.
620
+ - `|Δ| eligible − raw_full` lớn hơn ở một số nhãn (vd Support Devices 22 % → 37 %) → report-quality filter đã bias pool. Đây là trade-off đã biết, được báo cáo trong thesis như một giới hạn.
621
+
622
+ Pass thứ hai (cell 17) check VQA distribution trên `semantic_type`, `content_type`, `answer_type`, plot dạng bar so sánh full-VQA vs subset-VQA percentage. Trong lần chạy thực, cả 3 trục đều drift dưới 1 percentage point per category — đủ cho evaluation không thiên lệch.
623
+
624
+ ---
625
+
626
+ ## TL;DR — sơ đồ logic
627
+
628
+ ```
629
+ eligible pool (N = 140 000)
630
+ │
631
+ │ rare-first stratum:
632
+ │ mỗi study được gắn 1 nhãn = nhãn hiếm nhất mà nó dương
633
+ ▼
634
+ strata (Pleural_Other, Fracture, ..., No_Finding, None)
635
+ │
636
+ │ quota[j] = round(|stratum_j| / N × n)
637
+ ▼
638
+ quotas (214, 786, ..., 11 786, 5 357) tổng = n = 50 000
639
+ │
640
+ │ trong từng stratum: ưu tiên has_vqa, random pick
641
+ ▼
642
+ subset (50 000 study)
643
+ │
644
+ │ carve_split:
645
+ │ val ← MIMIC validate (stratified)
646
+ │ test ← MIMIC test (stratified)
647
+ │ remove val/test subject_ids from train candidates
648
+ │ train ← rest (stratified)
649
+ ▼
650
+ train (40 k) + val (5 k) + test (5 k)
651
+ patient-disjoint
652
+ │
653
+ │ verify (cell 16, 17):
654
+ │ pathology prevalence raw vs eligible vs subset
655
+ │ VQA distribution full vs subset on 3 axes
656
+ ▼
657
+ |Δ| < 1 % on every label/type → PASS
658
+
659
+ Toán bảo đảm: mọi study có P(sampled) ≈ n/N → E[prevalence subset] = pool prevalence,
660
+ với rounding error bound bởi 1/min(|stratum|) × max co-occurrence — nhỏ ở scale 140 k.
661
+ ```
docs/tempCodeRunnerFile.py ADDED
@@ -0,0 +1 @@
 
 
1
+ python -m pip install pypandoc_binary
evaluation/evaluate.py CHANGED
@@ -61,7 +61,7 @@ from data.prompt_templates import (
61
  build_vqa_prompt,
62
  )
63
  from data.dataset import parse_generated_report
64
- from evaluation.metrics import evaluate_all, print_results
65
  from utils.logger import setup_logger
66
  from utils.checkpoint import load_checkpoint
67
  from utils.hf_uploader import build_tracker_from_cfg
@@ -75,7 +75,9 @@ def parse_args():
75
  parser.add_argument("--checkpoint", type=str, required=True,
76
  help="Path to model checkpoint")
77
  parser.add_argument("--task", type=str, default="all",
78
- choices=["findings", "impression", "report", "vqa", "all"])
 
 
79
  parser.add_argument("--split", type=str, default="test")
80
  parser.add_argument("--output_dir", type=str, default="results/",
81
  help="Root dir; results land under {output_dir}/{run_id}/")
@@ -135,9 +137,11 @@ def run_inference(
135
  """
136
  task_samples = [s for s in dataset.samples if s["task"] == task]
137
  if not task_samples:
138
- return {"hypotheses": [], "references": [], "questions": []}
 
139
 
140
  hypotheses, references, questions = [], [], []
 
141
 
142
  # mininterval=1.0 keeps the bar visibly updating in Colab subprocess
143
  # output (default 10s feels like the kernel froze on T4). dynamic_ncols
@@ -190,8 +194,15 @@ def run_inference(
190
  references.extend([s["target"] for s in batch_samples])
191
  if task == "vqa":
192
  questions.extend([s.get("question", "") for s in batch_samples])
 
 
 
 
 
193
 
194
- return {"hypotheses": hypotheses, "references": references, "questions": questions}
 
 
195
 
196
 
197
  def save_predictions(predictions: Dict, task: str, output_dir: str):
@@ -313,17 +324,22 @@ def main():
313
  f"({len(dataset.samples):,} total samples in split={args.split!r})", flush=True)
314
 
315
  # Build task list, intersected with what's available for this dataset.
 
 
 
316
  if args.task == "all":
317
  tasks_to_eval = list(spec.tasks)
318
  else:
319
- if args.task not in spec.tasks:
320
- logger.warning(
321
- f"Task '{args.task}' not available for {spec.dataset_name} "
322
- f"(has: {spec.tasks}). Skipping."
323
- )
324
- tasks_to_eval = []
325
- else:
326
- tasks_to_eval = [args.task]
 
 
327
 
328
  # Print per-task sample counts up-front so the user can estimate runtime
329
  # before tqdm starts.
@@ -371,6 +387,30 @@ def main():
371
  print_results(metrics, task)
372
  all_results[task] = metrics
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  # ── If task is "report" (merged mode), also report per-section
375
  # metrics by parsing the generated and reference reports back into
376
  # findings / impression. This gives an apples-to-apples comparison
@@ -403,13 +443,42 @@ def main():
403
  print_results(m_i, "report→impression")
404
  all_results["report__impression_only"] = m_i
405
 
406
- # Save all metrics summary
 
 
 
407
  summary_path = results_dir / "metrics_summary.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  with open(summary_path, "w") as f:
409
  json.dump(
410
  {"dataset_name": spec.dataset_name, "run_id": run_id,
411
  "split": args.split, "pnu_source": args.pnu_source,
412
- "metrics": all_results},
413
  f, indent=2,
414
  )
415
  logger.info(f"\nMetrics summary saved to {summary_path}")
 
61
  build_vqa_prompt,
62
  )
63
  from data.dataset import parse_generated_report
64
+ from evaluation.metrics import evaluate_all, print_results, compute_vqa_breakdown
65
  from utils.logger import setup_logger
66
  from utils.checkpoint import load_checkpoint
67
  from utils.hf_uploader import build_tracker_from_cfg
 
75
  parser.add_argument("--checkpoint", type=str, required=True,
76
  help="Path to model checkpoint")
77
  parser.add_argument("--task", type=str, default="all",
78
+ help="'all', a single task, or a comma-separated list "
79
+ "(e.g. 'findings,vqa'). Tasks not available for the "
80
+ "dataset are skipped with a warning.")
81
  parser.add_argument("--split", type=str, default="test")
82
  parser.add_argument("--output_dir", type=str, default="results/",
83
  help="Root dir; results land under {output_dir}/{run_id}/")
 
137
  """
138
  task_samples = [s for s in dataset.samples if s["task"] == task]
139
  if not task_samples:
140
+ return {"hypotheses": [], "references": [], "questions": [],
141
+ "semantic_types": [], "content_types": [], "answer_types": []}
142
 
143
  hypotheses, references, questions = [], [], []
144
+ semantic_types, content_types, answer_types = [], [], []
145
 
146
  # mininterval=1.0 keeps the bar visibly updating in Colab subprocess
147
  # output (default 10s feels like the kernel froze on T4). dynamic_ncols
 
194
  references.extend([s["target"] for s in batch_samples])
195
  if task == "vqa":
196
  questions.extend([s.get("question", "") for s in batch_samples])
197
+ # Per-question taxonomy for the per-category breakdown (None when
198
+ # the JSON predates the type fields — falls into "unknown").
199
+ semantic_types.extend([s.get("semantic_type") for s in batch_samples])
200
+ content_types.extend([s.get("content_type") for s in batch_samples])
201
+ answer_types.extend([s.get("answer_type") for s in batch_samples])
202
 
203
+ return {"hypotheses": hypotheses, "references": references,
204
+ "questions": questions, "semantic_types": semantic_types,
205
+ "content_types": content_types, "answer_types": answer_types}
206
 
207
 
208
  def save_predictions(predictions: Dict, task: str, output_dir: str):
 
324
  f"({len(dataset.samples):,} total samples in split={args.split!r})", flush=True)
325
 
326
  # Build task list, intersected with what's available for this dataset.
327
+ # --task accepts "all", a single task, or a comma-separated list
328
+ # ("findings,vqa") so the notebook's per-task booleans evaluate exactly
329
+ # the requested subset in ONE model load.
330
  if args.task == "all":
331
  tasks_to_eval = list(spec.tasks)
332
  else:
333
+ requested = [t.strip() for t in args.task.split(",") if t.strip()]
334
+ tasks_to_eval = []
335
+ for t in requested:
336
+ if t in spec.tasks:
337
+ tasks_to_eval.append(t)
338
+ else:
339
+ logger.warning(
340
+ f"Task '{t}' not available for {spec.dataset_name} "
341
+ f"(has: {spec.tasks}). Skipping."
342
+ )
343
 
344
  # Print per-task sample counts up-front so the user can estimate runtime
345
  # before tqdm starts.
 
387
  print_results(metrics, task)
388
  all_results[task] = metrics
389
 
390
+ # ── VQA: per-category breakdown (semantic / content / answer type) ──
391
+ # A single aggregate mixes easy yes/no with hard open-set answers
392
+ # and isn't comparable to the MIMIC-CXR-VQA paper, which reports
393
+ # accuracy + micro-F1 per question category. Skipped gracefully when
394
+ # the JSON has no type fields (all fall into "unknown").
395
+ if task == "vqa":
396
+ hyps, refs = predictions["hypotheses"], predictions["references"]
397
+ for axis, key in (("semantic_type", "semantic_types"),
398
+ ("content_type", "content_types"),
399
+ ("answer_type", "answer_types")):
400
+ labels = predictions.get(key) or []
401
+ if not any(l not in (None, "") for l in labels):
402
+ continue # JSON predates type fields → nothing to break down
403
+ breakdown = compute_vqa_breakdown(hyps, refs, labels)
404
+ all_results[f"vqa__by_{axis}"] = breakdown
405
+ print(f"\n[vqa] breakdown by {axis}:", flush=True)
406
+ print(f" {'group':<14s} {'n':>6s} {'set_f1':>8s} "
407
+ f"{'set_acc':>8s} {'tok_f1':>8s}", flush=True)
408
+ for grp, m in breakdown.items():
409
+ print(f" {grp:<14s} {m['n']:>6d} "
410
+ f"{m['vqa_set_micro_f1']:>8.4f} "
411
+ f"{m['vqa_set_exact_match']:>8.4f} "
412
+ f"{m['vqa_token_f1']:>8.4f}", flush=True)
413
+
414
  # ── If task is "report" (merged mode), also report per-section
415
  # metrics by parsing the generated and reference reports back into
416
  # findings / impression. This gives an apples-to-apples comparison
 
443
  print_results(m_i, "report→impression")
444
  all_results["report__impression_only"] = m_i
445
 
446
+ # Save all metrics summary. MERGE into any existing summary so that
447
+ # re-evaluating only a subset of tasks (e.g. just VQA via the notebook's
448
+ # per-task booleans) does NOT wipe the previously-scored tasks. Only the
449
+ # tasks evaluated in THIS run are overwritten; the rest are preserved.
450
  summary_path = results_dir / "metrics_summary.json"
451
+ merged_metrics: Dict = {}
452
+ if summary_path.is_file():
453
+ try:
454
+ prev = json.loads(summary_path.read_text())
455
+ if prev.get("split") == args.split:
456
+ merged_metrics = dict(prev.get("metrics") or {})
457
+ if merged_metrics:
458
+ print(f"[evaluate] merging into existing summary "
459
+ f"(kept tasks: {sorted(merged_metrics)})", flush=True)
460
+ else:
461
+ print(f"[evaluate] existing summary is for split "
462
+ f"{prev.get('split')!r} ≠ {args.split!r} — not merging.",
463
+ flush=True)
464
+ except Exception as e:
465
+ print(f"[evaluate] could not read existing summary ({e}); "
466
+ f"writing fresh.", flush=True)
467
+ # Drop ALL stale keys belonging to the tasks evaluated this run before
468
+ # merging — so re-running e.g. VQA fully replaces every "vqa"/"vqa__*"
469
+ # entry (including any custom/orphan keys from an older eval scheme),
470
+ # instead of leaving stale ones behind. Other tasks are untouched.
471
+ for _t in tasks_to_eval:
472
+ for k in [k for k in merged_metrics
473
+ if k == _t or k.startswith(f"{_t}_")]:
474
+ merged_metrics.pop(k, None)
475
+ merged_metrics.update(all_results) # this run's tasks take precedence
476
+
477
  with open(summary_path, "w") as f:
478
  json.dump(
479
  {"dataset_name": spec.dataset_name, "run_id": run_id,
480
  "split": args.split, "pnu_source": args.pnu_source,
481
+ "metrics": merged_metrics},
482
  f, indent=2,
483
  )
484
  logger.info(f"\nMetrics summary saved to {summary_path}")
evaluation/metrics.py CHANGED
@@ -257,6 +257,88 @@ def compute_vqa_accuracy(
257
  }
258
 
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  def _normalize_answer(text: str) -> str:
261
  """Lowercase, remove punctuation, strip whitespace."""
262
  text = text.lower().strip()
@@ -434,8 +516,13 @@ def evaluate_all(
434
  ))
435
 
436
  elif task == "vqa":
437
- # Lexical
 
 
438
  results.update(compute_vqa_accuracy(hypotheses, references))
 
 
 
439
  results.update(compute_bleu(hypotheses, references))
440
  results.update(compute_meteor(hypotheses, references))
441
  # Semantic
 
257
  }
258
 
259
 
260
+ def _answer_to_set(text: str) -> set:
261
+ """
262
+ Split a VQA answer into a SET of normalized labels.
263
+
264
+ MIMIC-CXR-VQA answers are multi-label: open answers are comma-separated
265
+ lists of items ("atelectasis, edema"); closed answers are singletons
266
+ ("yes" / "no"). The official metric treats answers as sets, so we split on
267
+ commas and normalize each element.
268
+ """
269
+ return {a for a in (_normalize_answer(p) for p in str(text).split(",")) if a}
270
+
271
+
272
+ def compute_vqa_set_metrics(
273
+ hypotheses: List[str],
274
+ references: List[str],
275
+ ) -> Dict[str, float]:
276
+ """
277
+ Set-based VQA metrics matching the official MIMIC-CXR-VQA protocol
278
+ (EHRXQA, Bae et al. NeurIPS 2023): answers are sets of labels, scored with
279
+
280
+ - Accuracy : strict set equality (predicted set == ground-truth set)
281
+ - Micro-F1 : over total TP/FP/FN of label elements across ALL instances
282
+
283
+ This is the apples-to-apples comparison against published baselines
284
+ (e.g. M3AE ≈ 0.69 acc / 0.72 F1). `vqa_exact_match`/`vqa_token_f1` from
285
+ compute_vqa_accuracy are kept for backward-compatibility but are string-
286
+ rather than set-based.
287
+
288
+ Returns:
289
+ {"vqa_set_micro_f1": float, "vqa_set_exact_match": float}
290
+ """
291
+ tp = fp = fn = 0
292
+ exact = []
293
+ for hyp, ref in zip(hypotheses, references):
294
+ h, r = _answer_to_set(hyp), _answer_to_set(ref)
295
+ tp += len(h & r)
296
+ fp += len(h - r)
297
+ fn += len(r - h)
298
+ exact.append(int(h == r))
299
+
300
+ precision = tp / (tp + fp) if (tp + fp) else 0.0
301
+ recall = tp / (tp + fn) if (tp + fn) else 0.0
302
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
303
+ return {
304
+ "vqa_set_micro_f1": round(f1, 4),
305
+ "vqa_set_exact_match": round(float(np.mean(exact)) if exact else 0.0, 4),
306
+ }
307
+
308
+
309
+ def compute_vqa_breakdown(
310
+ hypotheses: List[str],
311
+ references: List[str],
312
+ group_labels: List[Optional[str]],
313
+ ) -> Dict[str, Dict[str, float]]:
314
+ """
315
+ Group (hyp, ref) pairs by `group_labels` (e.g. semantic_type or
316
+ content_type) and compute per-group VQA metrics. Lets us report accuracy /
317
+ F1 per question category the way the MIMIC-CXR-VQA paper does, instead of a
318
+ single muddy aggregate that mixes easy yes/no with hard open-set answers.
319
+
320
+ Returns:
321
+ {group_value: {set_micro_f1, set_exact_match, vqa_exact_match,
322
+ vqa_token_f1, n}}
323
+ """
324
+ from collections import defaultdict
325
+ groups: Dict[str, Dict[str, List[str]]] = defaultdict(
326
+ lambda: {"hyps": [], "refs": []}
327
+ )
328
+ for hyp, ref, g in zip(hypotheses, references, group_labels):
329
+ key = g if g not in (None, "") else "unknown"
330
+ groups[key]["hyps"].append(hyp)
331
+ groups[key]["refs"].append(ref)
332
+
333
+ out: Dict[str, Dict[str, float]] = {}
334
+ for key, d in sorted(groups.items()):
335
+ metrics = compute_vqa_set_metrics(d["hyps"], d["refs"])
336
+ metrics.update(compute_vqa_accuracy(d["hyps"], d["refs"]))
337
+ metrics["n"] = len(d["hyps"])
338
+ out[key] = metrics
339
+ return out
340
+
341
+
342
  def _normalize_answer(text: str) -> str:
343
  """Lowercase, remove punctuation, strip whitespace."""
344
  text = text.lower().strip()
 
516
  ))
517
 
518
  elif task == "vqa":
519
+ # Set-based (official MIMIC-CXR-VQA protocol) — the headline numbers.
520
+ results.update(compute_vqa_set_metrics(hypotheses, references))
521
+ # String-based exact-match + token-F1 (kept for backward-compat).
522
  results.update(compute_vqa_accuracy(hypotheses, references))
523
+ # Lexical generation metrics — only meaningful for long open answers;
524
+ # near-useless for 1-3 word answers (BLEU-4 in particular). Reported
525
+ # but should NOT be the headline for short-answer VQA.
526
  results.update(compute_bleu(hypotheses, references))
527
  results.update(compute_meteor(hypotheses, references))
528
  # Semantic
scripts/cxrvlm_colab_eval.ipynb CHANGED
@@ -65,8 +65,17 @@
65
  "REPORT_MODE = None # None → read from run's saved train_config.yaml\n",
66
  "IMAGE_MODE = None # None → read from run's saved train_config.yaml\n",
67
  "\n",
68
- "# ── What to evaluate ─────────────────────────────────────────────\n",
69
- "TASK = 'all' # 'all' | 'findings' | 'impression' | 'report' | 'vqa'\n",
 
 
 
 
 
 
 
 
 
70
  "SPLIT = 'test' # always 'test' for this notebook; here for visibility\n",
71
  "BATCH_SIZE = None # None → auto from GPU profile (T4:1, L4:4, A100:8, H100:16)\n",
72
  "MAX_NEW_TOKENS = 300\n",
@@ -112,13 +121,20 @@
112
  "assert PLATFORM in ('kaggle', 'colab', 'lightning', 'gcp', 'local')\n",
113
  "assert DATASET_NAME in ('MIMIC-CXR', 'MIMIC-CXR_resized', 'IU-Xray')\n",
114
  "assert CKPT_PICK in ('best', 'last')\n",
115
- "assert TASK in ('all', 'findings', 'impression', 'report', 'vqa')\n",
 
 
 
 
 
 
 
116
  "assert PNU_SOURCE in ('oracle', 'predicted')\n",
117
  "\n",
118
  "print(f'PLATFORM = {PLATFORM}')\n",
119
  "print(f'RUN_ID = {RUN_ID} (ckpt: stage2/{CKPT_PICK})')\n",
120
  "print(f'DATASET_NAME = {DATASET_NAME}')\n",
121
- "print(f'TASK = {TASK} SPLIT = {SPLIT}')\n",
122
  "print(f'PNU_SOURCE = {PNU_SOURCE}')\n",
123
  "print(f'LOCAL_RESULTS_DIR = {LOCAL_RESULTS_DIR} (upload→HF: {UPLOAD_RESULTS_TO_HF})')\n"
124
  ]
@@ -865,7 +881,20 @@
865
  " print(' [mimic_cxr_resized_builder] vqa added/dropped : N / M')\n",
866
  " print(' If N=0 the model was likely NOT trained on VQA either — same JSON cache.')\n",
867
  "elif 'vqa' in spec.tasks:\n",
868
- " print(f'\\nVQA in test split: {test_vqa:,} samples — OK')\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
869
  ]
870
  },
871
  {
@@ -917,7 +946,7 @@
917
  "\n",
918
  "print(f'Evaluating run_id : {RUN_ID}')\n",
919
  "print(f'Checkpoint : {CKPT_ARG}')\n",
920
- "print(f'Task : {TASK} (split={SPLIT})')\n",
921
  "print(f'PNU source : {PNU_SOURCE}')\n",
922
  "print(f'Local results dir : {RESULTS_DIR_LOCAL}/{RUN_ID}/')\n",
923
  "print(f'Upload to HF : {UPLOAD_RESULTS_TO_HF}')\n",
@@ -934,7 +963,7 @@
934
  " --train_config configs/train_config.yaml \\\n",
935
  " --checkpoint \"{CKPT_ARG}\" \\\n",
936
  " --run_id \"{RUN_ID}\" \\\n",
937
- " --task {TASK} \\\n",
938
  " --split {SPLIT} \\\n",
939
  " --batch_size {BATCH_SIZE} \\\n",
940
  " --max_new_tokens {MAX_NEW_TOKENS} \\\n",
@@ -970,7 +999,22 @@
970
  " print(f'Split : {summary.get(\"split\")}')\n",
971
  " print(f'PNU source : {summary.get(\"pnu_source\")}')\n",
972
  " print()\n",
 
 
 
 
 
973
  " for task, metrics in (summary.get('metrics') or {}).items():\n",
 
 
 
 
 
 
 
 
 
 
974
  " print(f'─── {task.upper()} ───')\n",
975
  " for k, v in metrics.items():\n",
976
  " if isinstance(v, float):\n",
 
65
  "REPORT_MODE = None # None → read from run's saved train_config.yaml\n",
66
  "IMAGE_MODE = None # None → read from run's saved train_config.yaml\n",
67
  "\n",
68
+ "# ── What to evaluate (toggle each task independently) ────────────\n",
69
+ "# Set True only for the parts you want to (re-)evaluate. e.g. to re-run ONLY\n",
70
+ "# VQA: EVAL_VQA=True and the other two False. Tasks not present in your\n",
71
+ "# dataset / report_mode are skipped automatically. Re-running a subset MERGES\n",
72
+ "# into the existing metrics_summary.json — other tasks' numbers are preserved,\n",
73
+ "# so you won't lose your findings/impression scores by re-running VQA alone.\n",
74
+ "EVAL_FINDINGS = True\n",
75
+ "EVAL_IMPRESSION = True\n",
76
+ "EVAL_VQA = True\n",
77
+ "EVAL_REPORT = False # only for runs trained in merged report_mode (task='report')\n",
78
+ "\n",
79
  "SPLIT = 'test' # always 'test' for this notebook; here for visibility\n",
80
  "BATCH_SIZE = None # None → auto from GPU profile (T4:1, L4:4, A100:8, H100:16)\n",
81
  "MAX_NEW_TOKENS = 300\n",
 
121
  "assert PLATFORM in ('kaggle', 'colab', 'lightning', 'gcp', 'local')\n",
122
  "assert DATASET_NAME in ('MIMIC-CXR', 'MIMIC-CXR_resized', 'IU-Xray')\n",
123
  "assert CKPT_PICK in ('best', 'last')\n",
124
+ "# Build the comma-separated task list passed to evaluate.py (--task).\n",
125
+ "_SELECTED_TASKS = []\n",
126
+ "if EVAL_FINDINGS: _SELECTED_TASKS.append('findings')\n",
127
+ "if EVAL_IMPRESSION: _SELECTED_TASKS.append('impression')\n",
128
+ "if EVAL_REPORT: _SELECTED_TASKS.append('report')\n",
129
+ "if EVAL_VQA: _SELECTED_TASKS.append('vqa')\n",
130
+ "assert _SELECTED_TASKS, 'Select at least one task (set one of EVAL_* = True).'\n",
131
+ "TASK_ARG = ','.join(_SELECTED_TASKS)\n",
132
  "assert PNU_SOURCE in ('oracle', 'predicted')\n",
133
  "\n",
134
  "print(f'PLATFORM = {PLATFORM}')\n",
135
  "print(f'RUN_ID = {RUN_ID} (ckpt: stage2/{CKPT_PICK})')\n",
136
  "print(f'DATASET_NAME = {DATASET_NAME}')\n",
137
+ "print(f'TASKS = {TASK_ARG} SPLIT = {SPLIT}')\n",
138
  "print(f'PNU_SOURCE = {PNU_SOURCE}')\n",
139
  "print(f'LOCAL_RESULTS_DIR = {LOCAL_RESULTS_DIR} (upload→HF: {UPLOAD_RESULTS_TO_HF})')\n"
140
  ]
 
881
  " print(' [mimic_cxr_resized_builder] vqa added/dropped : N / M')\n",
882
  " print(' If N=0 the model was likely NOT trained on VQA either — same JSON cache.')\n",
883
  "elif 'vqa' in spec.tasks:\n",
884
+ " print(f'\\nVQA in test split: {test_vqa:,} samples — OK')\n",
885
+ "\n",
886
+ "# VQA question-type distribution (test split) — confirms the per-category\n",
887
+ "# breakdown will have data. If everything shows \"unknown\", the JSON predates\n",
888
+ "# the content_type/semantic_type fields → rebuild it (delete the cached\n",
889
+ "# instruct JSON so the builder regenerates with the type fields).\n",
890
+ "if test_vqa:\n",
891
+ " print()\n",
892
+ " print('VQA test-split type distribution:')\n",
893
+ " for axis in ('semantic_type', 'content_type', 'answer_type'):\n",
894
+ " dist = Counter(s.get(axis) or 'unknown'\n",
895
+ " for s in samples if s['split'] == 'test' and s['task'] == 'vqa')\n",
896
+ " pretty = ', '.join(f'{k}={v:,}' for k, v in sorted(dist.items()))\n",
897
+ " print(f' {axis:<14s}: {pretty}')\n"
898
  ]
899
  },
900
  {
 
946
  "\n",
947
  "print(f'Evaluating run_id : {RUN_ID}')\n",
948
  "print(f'Checkpoint : {CKPT_ARG}')\n",
949
+ "print(f'Tasks : {TASK_ARG} (split={SPLIT})')\n",
950
  "print(f'PNU source : {PNU_SOURCE}')\n",
951
  "print(f'Local results dir : {RESULTS_DIR_LOCAL}/{RUN_ID}/')\n",
952
  "print(f'Upload to HF : {UPLOAD_RESULTS_TO_HF}')\n",
 
963
  " --train_config configs/train_config.yaml \\\n",
964
  " --checkpoint \"{CKPT_ARG}\" \\\n",
965
  " --run_id \"{RUN_ID}\" \\\n",
966
+ " --task {TASK_ARG} \\\n",
967
  " --split {SPLIT} \\\n",
968
  " --batch_size {BATCH_SIZE} \\\n",
969
  " --max_new_tokens {MAX_NEW_TOKENS} \\\n",
 
999
  " print(f'Split : {summary.get(\"split\")}')\n",
1000
  " print(f'PNU source : {summary.get(\"pnu_source\")}')\n",
1001
  " print()\n",
1002
+ "\n",
1003
+ " def _is_breakdown(v):\n",
1004
+ " # nested {group: {metric: val}} dict (the vqa__by_* entries)\n",
1005
+ " return isinstance(v, dict) and v and all(isinstance(x, dict) for x in v.values())\n",
1006
+ "\n",
1007
  " for task, metrics in (summary.get('metrics') or {}).items():\n",
1008
+ " if _is_breakdown(metrics):\n",
1009
+ " # Per-category VQA table (semantic / content / answer type).\n",
1010
+ " print(f'─── {task.upper()} ───')\n",
1011
+ " cols = ['vqa_set_micro_f1', 'vqa_set_exact_match', 'vqa_token_f1']\n",
1012
+ " print(f' {\"group\":<14s} {\"n\":>7s} ' + ' '.join(f'{c.replace(\"vqa_\",\"\"):>12s}' for c in cols))\n",
1013
+ " for grp, m in metrics.items():\n",
1014
+ " print(f' {grp:<14s} {m.get(\"n\",0):>7d} '\n",
1015
+ " + ' '.join(f'{m.get(c,0):>12.4f}' for c in cols))\n",
1016
+ " print()\n",
1017
+ " continue\n",
1018
  " print(f'─── {task.upper()} ───')\n",
1019
  " for k, v in metrics.items():\n",
1020
  " if isinstance(v, float):\n",