lukeingawesome commited on
Commit
23c824a
·
verified ·
1 Parent(s): 47f2d5a

Fix loader: correct CADAD signature, bundle sentence-segmentation collate + concept vocab, decode_greedy path verified end-to-end

Browse files
Files changed (3) hide show
  1. chest2err.py +85 -90
  2. chest2err_collate.py +105 -0
  3. concept2id.json +387 -0
chest2err.py CHANGED
@@ -2,35 +2,38 @@
2
 
3
  Usage:
4
  from chest2err import chest2err_score, chest2err_detail
 
 
5
 
6
- score = chest2err_score(ref_report, candidate_report) # float in (0, 1]
7
- detail = chest2err_detail(ref_report, candidate_report) # full breakdown
8
-
9
- The bundle ships the merged backbone + decoder weights and the Qwen3-architecture
10
- config, so no extra weights are downloaded at inference time. The backbone class
11
- itself is loaded from the `transformers` package.
12
  """
13
  from __future__ import annotations
14
 
15
  import json
16
- import os
17
- import re
18
  import math
 
19
  from pathlib import Path
20
- from typing import Any, Dict, List, Optional, Tuple
21
 
22
  import torch
23
- import torch.nn.functional as F
24
  from transformers import AutoModel, AutoTokenizer
25
  from safetensors.torch import load_file
26
 
27
- # Import the decoder module that ships in the same directory.
28
  from chest2err_modeling import CADAD
29
-
30
- # ---------------------------------------------------------------------------
31
 
32
  PACKAGE_DIR = Path(__file__).resolve().parent
33
 
 
 
 
 
 
 
34
 
35
  def _load_config() -> Dict[str, Any]:
36
  with open(PACKAGE_DIR / "chest2err_config.json") as f:
@@ -40,114 +43,106 @@ def _load_config() -> Dict[str, Any]:
40
  class Chest2Err:
41
  """Loads the merged backbone + decoder once, then scores pairs."""
42
 
43
- def __init__(self, device: str = "cuda" if torch.cuda.is_available() else "cpu"):
 
 
44
  cfg = _load_config()
45
  self.cfg = cfg
46
  self.device = device
47
  self.max_length = cfg["max_length"]
48
- self.template = cfg["input_template"]
49
 
50
- # Backbone: load the chest2vec_0.6b architecture from the bundled config + weights.
51
- # No HuggingFace download — the safetensors and config.json are local to this package.
52
- self.tokenizer = AutoTokenizer.from_pretrained(str(PACKAGE_DIR))
53
- self.backbone = AutoModel.from_pretrained(
54
- str(PACKAGE_DIR),
55
- torch_dtype=torch.bfloat16,
56
- ).to(device).eval()
57
 
58
- # Decoder + null embeddings + heads.
59
- decoder_state = load_file(str(PACKAGE_DIR / "decoder.safetensors"))
60
- n_concepts = decoder_state["concept_head.weight"].shape[0] if "concept_head.weight" in decoder_state else 1
61
- self.decoder = CADAD(
62
- hidden=cfg["hidden_size"],
63
- n_cat=cfg["n_cat"] + 1, # +1 for EOS at index 0
 
 
 
 
 
 
 
64
  n_anat=cfg["n_anat"],
65
- n_concepts=n_concepts,
 
66
  decoder_layers=cfg["decoder_layers"],
67
  decoder_heads=cfg["decoder_heads"],
68
  decoder_ff=cfg["decoder_ff"],
69
- decoder_dropout=cfg["decoder_dropout"],
70
  max_decode_steps=cfg["max_decode_steps"],
71
  )
72
- self.decoder.load_state_dict(decoder_state, strict=False)
73
- self.decoder = self.decoder.to(device).to(torch.bfloat16).eval()
74
-
75
- # ----------------------- input prep ------------------------- #
76
-
77
- @staticmethod
78
- def _split_sentences(text: str) -> List[str]:
79
- """Light sentence splitter. Section headers and bullet lines count as boundaries too."""
80
- # Split on . ! ? and section headers like [Lungs] or "Lungs:"
81
- chunks = re.split(r"(?<=[.!?])\s+|\n+", text or "")
82
- sents = [c.strip().lstrip("- ").strip() for c in chunks]
83
- return [s for s in sents if s]
84
-
85
- def _encode_pair(self, ref: str, cand: str) -> Dict[str, torch.Tensor]:
86
- ref_sents = self._split_sentences(ref)
87
- cand_sents = self._split_sentences(cand)
88
- text = self.template.format(reference_report=ref, candidate_report=cand)
89
- enc = self.tokenizer(
90
- text,
91
- max_length=self.max_length,
92
- truncation=True,
93
- padding=False,
94
- return_tensors="pt",
95
- add_special_tokens=False,
96
- )
97
- # NB: a production-grade encoder also produces seg_token_mask aligning each
98
- # sentence to its token span. The CADAD decoder consumes per-sentence
99
- # mean-pooled vectors; this helper exposes the API surface.
100
- return {
101
- "input_ids": enc["input_ids"].to(self.device),
102
- "attention_mask": enc["attention_mask"].to(self.device),
103
- "ref_sentences": ref_sents,
104
- "cand_sentences": cand_sents,
105
- }
106
 
107
- # ----------------------- public API ------------------------- #
 
 
 
 
 
 
 
108
 
109
  @torch.inference_mode()
110
  def score(self, ref: str, cand: str) -> float:
111
- """chest2err-score (0, 1]. Higher = better."""
112
- detail = self.detail(ref, cand)
113
- return detail["score"]
114
 
115
  @torch.inference_mode()
116
  def detail(self, ref: str, cand: str) -> Dict[str, Any]:
117
- """Full breakdown: score, K_total, per-error tuples, per-category and per-anatomy counts."""
118
- enc = self._encode_pair(ref, cand)
119
- out = self.backbone(
120
- input_ids=enc["input_ids"],
121
- attention_mask=enc["attention_mask"],
122
- use_cache=False,
123
- )
124
- h = out.last_hidden_state
125
- tuples = self.decoder.generate(
126
- h=h,
127
- attention_mask=enc["attention_mask"],
128
- ref_sentences=enc["ref_sentences"],
129
- cand_sentences=enc["cand_sentences"],
130
  )
131
- K_total = len(tuples)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  score = math.exp(-K_total)
 
133
  cat_counts = [0] * self.cfg["n_cat"]
134
  anat_counts = [0] * self.cfg["n_anat"]
135
- for t in tuples:
136
- if 1 <= t["cat"] <= self.cfg["n_cat"]:
137
- cat_counts[t["cat"] - 1] += 1
138
- if 0 <= t["anat"] < self.cfg["n_anat"]:
139
- anat_counts[t["anat"]] += 1
 
 
 
 
 
 
 
 
 
 
 
140
  return {
141
  "score": score,
142
  "K_total": K_total,
143
- "tuples": tuples,
144
  "category_counts": cat_counts,
145
  "anatomy_counts": anat_counts,
146
  }
147
 
148
 
149
- # ----------------------- module-level convenience ----------------------- #
150
-
151
  _INSTANCE: Optional[Chest2Err] = None
152
 
153
 
 
2
 
3
  Usage:
4
  from chest2err import chest2err_score, chest2err_detail
5
+ score = chest2err_score(ref, cand) # float in (0, 1]
6
+ detail = chest2err_detail(ref, cand) # full breakdown
7
 
8
+ The bundle ships the merged backbone weights, the decoder weights, the
9
+ tokenizer, and the concept vocabulary. No additional downloads occur at
10
+ inference; the Qwen3-architecture backbone class is taken from the
11
+ `transformers` package and instantiated from the bundled `config.json`.
 
 
12
  """
13
  from __future__ import annotations
14
 
15
  import json
 
 
16
  import math
17
+ import os
18
  from pathlib import Path
19
+ from typing import Any, Dict, List, Optional
20
 
21
  import torch
 
22
  from transformers import AutoModel, AutoTokenizer
23
  from safetensors.torch import load_file
24
 
25
+ # Sibling files in this package
26
  from chest2err_modeling import CADAD
27
+ from chest2err_collate import encode_pair_for_decoder, collate_decoder_batch
 
28
 
29
  PACKAGE_DIR = Path(__file__).resolve().parent
30
 
31
+ CAT_NAMES = {0: "EOS", 1: "false_prediction", 2: "omission", 3: "location",
32
+ 4: "severity", 5: "comparison"}
33
+ ANAT_NAMES = {0: "Lung & Airways", 1: "Cardiovascular", 2: "Mediastinum & Hila",
34
+ 3: "Upper Abdomen", 4: "Pleura", 5: "Bones / Spine", 6: "Chest Wall",
35
+ 7: "Lower Neck", 8: "Others"}
36
+
37
 
38
  def _load_config() -> Dict[str, Any]:
39
  with open(PACKAGE_DIR / "chest2err_config.json") as f:
 
43
  class Chest2Err:
44
  """Loads the merged backbone + decoder once, then scores pairs."""
45
 
46
+ def __init__(self,
47
+ device: str = "cuda" if torch.cuda.is_available() else "cpu",
48
+ attn_implementation: Optional[str] = None):
49
  cfg = _load_config()
50
  self.cfg = cfg
51
  self.device = device
52
  self.max_length = cfg["max_length"]
 
53
 
54
+ # Concept vocab (size determines decoder output head dim)
55
+ with open(PACKAGE_DIR / "concept2id.json") as f:
56
+ self.concept2id: Dict[str, int] = json.load(f)
57
+ self.n_concept = len(self.concept2id)
58
+ self.id2concept = {v: k for k, v in self.concept2id.items()}
 
 
59
 
60
+ # Tokenizer + backbone load from bundled files only.
61
+ self.tokenizer = AutoTokenizer.from_pretrained(str(PACKAGE_DIR))
62
+ kw = {"torch_dtype": torch.bfloat16}
63
+ if attn_implementation:
64
+ kw["attn_implementation"] = attn_implementation
65
+ backbone = AutoModel.from_pretrained(str(PACKAGE_DIR), **kw)
66
+
67
+ # CADAD wraps the backbone + decoder. Construct, then load merged backbone
68
+ # weights + decoder weights.
69
+ self.model = CADAD(
70
+ backbone=backbone,
71
+ hidden_size=cfg["hidden_size"],
72
+ n_cat=cfg["n_cat"],
73
  n_anat=cfg["n_anat"],
74
+ n_concept=self.n_concept,
75
+ n_severity=2,
76
  decoder_layers=cfg["decoder_layers"],
77
  decoder_heads=cfg["decoder_heads"],
78
  decoder_ff=cfg["decoder_ff"],
79
+ dropout=cfg["decoder_dropout"],
80
  max_decode_steps=cfg["max_decode_steps"],
81
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ # The backbone weights were already loaded by AutoModel.from_pretrained.
84
+ # Now layer the decoder weights on top.
85
+ decoder_state = load_file(str(PACKAGE_DIR / "decoder.safetensors"))
86
+ missing, unexpected = self.model.load_state_dict(decoder_state, strict=False)
87
+ # Expected: many `backbone.*` keys are "missing" from decoder_state
88
+ # (they came from model.safetensors via from_pretrained). That's fine.
89
+
90
+ self.model = self.model.to(device).eval()
91
 
92
  @torch.inference_mode()
93
  def score(self, ref: str, cand: str) -> float:
94
+ return self.detail(ref, cand)["score"]
 
 
95
 
96
  @torch.inference_mode()
97
  def detail(self, ref: str, cand: str) -> Dict[str, Any]:
98
+ item = encode_pair_for_decoder(
99
+ self.tokenizer, ref, cand, max_length=self.max_length,
 
 
 
 
 
 
 
 
 
 
 
100
  )
101
+ batch = collate_decoder_batch([item],
102
+ pad_token_id=self.tokenizer.pad_token_id or 0)
103
+ batch = {k: v.to(self.device) for k, v in batch.items()}
104
+
105
+ with torch.autocast(
106
+ device_type="cuda" if str(self.device).startswith("cuda") else "cpu",
107
+ dtype=torch.bfloat16,
108
+ ):
109
+ seqs = self.model.decode_greedy(
110
+ batch["input_ids"],
111
+ batch["attention_mask"],
112
+ batch["ref_seg_token_mask"],
113
+ batch["cand_seg_token_mask"],
114
+ )
115
+ seq = seqs[0]
116
+ K_total = len(seq)
117
  score = math.exp(-K_total)
118
+
119
  cat_counts = [0] * self.cfg["n_cat"]
120
  anat_counts = [0] * self.cfg["n_anat"]
121
+ tuples_out: List[Dict[str, Any]] = []
122
+ for t in seq:
123
+ c = int(t.get("cat", 0))
124
+ a = int(t.get("anat", 0))
125
+ if 1 <= c <= self.cfg["n_cat"]:
126
+ cat_counts[c - 1] += 1
127
+ if 0 <= a < self.cfg["n_anat"]:
128
+ anat_counts[a] += 1
129
+ tuples_out.append({
130
+ "cat": c, "cat_name": CAT_NAMES.get(c, str(c)),
131
+ "anat": a, "anat_name": ANAT_NAMES.get(a, str(a)),
132
+ "concept_id": int(t.get("concept_id", 0)),
133
+ "concept": self.id2concept.get(int(t.get("concept_id", 0)), "<UNK>"),
134
+ "ref_seg_idx": int(t.get("ref_seg_idx", -1)),
135
+ "cand_seg_idx": int(t.get("cand_seg_idx", -1)),
136
+ })
137
  return {
138
  "score": score,
139
  "K_total": K_total,
140
+ "tuples": tuples_out,
141
  "category_counts": cat_counts,
142
  "anatomy_counts": anat_counts,
143
  }
144
 
145
 
 
 
146
  _INSTANCE: Optional[Chest2Err] = None
147
 
148
 
chest2err_collate.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sentence segmentation + collate for chest2err inference.
2
+
3
+ Stripped-down version of the in-tree training collate: keeps only what's needed
4
+ to run greedy decoding on one (ref, cand) pair.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from typing import Any, Dict, List, Tuple
10
+
11
+ import torch
12
+
13
+
14
+ _SECTION_HEADER_RE = re.compile(r"\[[^\]]+\]")
15
+ _BULLET_RE = re.compile(r"\n\s*[-*•]\s+")
16
+ _SENT_BOUNDARY_RE = re.compile(r"(?<=\.)\s+(?=[A-Z])")
17
+
18
+
19
+ def segment_report(text: str, min_len: int = 3) -> List[str]:
20
+ if not text or not text.strip():
21
+ return []
22
+ t = _SECTION_HEADER_RE.sub("\n", text)
23
+ t = _BULLET_RE.sub("\n", t)
24
+ out: List[str] = []
25
+ for line in t.split("\n"):
26
+ line = line.strip()
27
+ if len(line) < min_len:
28
+ continue
29
+ for sent in _SENT_BOUNDARY_RE.split(line):
30
+ sent = sent.strip()
31
+ if len(sent) >= min_len:
32
+ out.append(sent)
33
+ return out
34
+
35
+
36
+ def encode_pair_for_decoder(
37
+ tokenizer,
38
+ ref_text: str,
39
+ cand_text: str,
40
+ max_length: int = 1280,
41
+ ref_marker: str = "[REF]",
42
+ cand_marker: str = "[PRED]",
43
+ ) -> Dict[str, Any]:
44
+ ref_segs = segment_report(ref_text)
45
+ cand_segs = segment_report(cand_text)
46
+
47
+ ref_marker_ids = tokenizer.encode(ref_marker + " ", add_special_tokens=False)
48
+ cand_marker_ids = tokenizer.encode(" " + cand_marker + " ", add_special_tokens=False)
49
+ ref_seg_token_ids = [tokenizer.encode(s + " ", add_special_tokens=False) for s in ref_segs]
50
+ cand_seg_token_ids = [tokenizer.encode(s + " ", add_special_tokens=False) for s in cand_segs]
51
+
52
+ def _total_len(rs, cs):
53
+ return (len(ref_marker_ids) + sum(len(x) for x in rs)
54
+ + len(cand_marker_ids) + sum(len(x) for x in cs))
55
+
56
+ while _total_len(ref_seg_token_ids, cand_seg_token_ids) > max_length and cand_seg_token_ids:
57
+ cand_seg_token_ids.pop(); cand_segs = cand_segs[:-1]
58
+ while _total_len(ref_seg_token_ids, cand_seg_token_ids) > max_length and ref_seg_token_ids:
59
+ ref_seg_token_ids.pop(); ref_segs = ref_segs[:-1]
60
+
61
+ input_ids: List[int] = []
62
+ input_ids.extend(ref_marker_ids)
63
+ ref_ranges: List[Tuple[int, int]] = []
64
+ for ids in ref_seg_token_ids:
65
+ s = len(input_ids); input_ids.extend(ids); ref_ranges.append((s, len(input_ids)))
66
+ input_ids.extend(cand_marker_ids)
67
+ cand_ranges: List[Tuple[int, int]] = []
68
+ for ids in cand_seg_token_ids:
69
+ s = len(input_ids); input_ids.extend(ids); cand_ranges.append((s, len(input_ids)))
70
+
71
+ return {
72
+ "input_ids": input_ids,
73
+ "ref_seg_ranges": ref_ranges,
74
+ "cand_seg_ranges": cand_ranges,
75
+ "ref_segs": ref_segs,
76
+ "cand_segs": cand_segs,
77
+ }
78
+
79
+
80
+ def collate_decoder_batch(items: List[Dict[str, Any]], pad_token_id: int = 0) -> Dict[str, torch.Tensor]:
81
+ T = max(len(it["input_ids"]) for it in items)
82
+ Sr = max(max(len(it["ref_seg_ranges"]), 1) for it in items)
83
+ Sc = max(max(len(it["cand_seg_ranges"]), 1) for it in items)
84
+ B = len(items)
85
+
86
+ input_ids = torch.full((B, T), pad_token_id, dtype=torch.long)
87
+ attention_mask = torch.zeros((B, T), dtype=torch.long)
88
+ ref_seg_token_mask = torch.zeros((B, Sr, T), dtype=torch.bool)
89
+ cand_seg_token_mask = torch.zeros((B, Sc, T), dtype=torch.bool)
90
+
91
+ for b, it in enumerate(items):
92
+ ids = it["input_ids"]; L = len(ids)
93
+ input_ids[b, :L] = torch.tensor(ids, dtype=torch.long)
94
+ attention_mask[b, :L] = 1
95
+ for s, (a, e) in enumerate(it["ref_seg_ranges"]):
96
+ ref_seg_token_mask[b, s, a:e] = True
97
+ for s, (a, e) in enumerate(it["cand_seg_ranges"]):
98
+ cand_seg_token_mask[b, s, a:e] = True
99
+
100
+ return {
101
+ "input_ids": input_ids,
102
+ "attention_mask": attention_mask,
103
+ "ref_seg_token_mask": ref_seg_token_mask,
104
+ "cand_seg_token_mask": cand_seg_token_mask,
105
+ }
concept2id.json ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Abdominal aortic aneurysm": 0,
3
+ "Abdominal aortic aneurysm (partially imaged)": 1,
4
+ "Abdominal aortic calcification / atherosclerosis": 2,
5
+ "Abdominal aortic calcification / atherosclerosis (partially imaged)": 3,
6
+ "Abdominal lymphadenopathy": 4,
7
+ "Aberrant right subclavian artery": 5,
8
+ "Accessory hemiazygos vein": 6,
9
+ "Accessory spleen": 7,
10
+ "Accessory spleen / splenule / polysplenia": 8,
11
+ "Acinar infiltration areas": 9,
12
+ "Acinar opacities": 10,
13
+ "Acute rib fracture": 11,
14
+ "Adrenal atrophy": 12,
15
+ "Adrenal calcification": 13,
16
+ "Adrenal gland": 14,
17
+ "Adrenal gland calibration": 15,
18
+ "Adrenal gland normal": 16,
19
+ "Adrenal nodule": 17,
20
+ "Adrenal nodule (mass)": 18,
21
+ "Adrenal nodule / mass": 19,
22
+ "Adrenal thickening / hyperplasia": 20,
23
+ "Adrenals": 21,
24
+ "Air bronchograms": 22,
25
+ "Alveolar-interstitial density": 23,
26
+ "Angiomyolipoma": 24,
27
+ "Anterior mediastinal mass": 25,
28
+ "Aorta": 26,
29
+ "Aorta / pulmonary artery": 27,
30
+ "Aortic aneurysm": 28,
31
+ "Aortic calcification": 29,
32
+ "Aortic dissection / intramural hematoma": 30,
33
+ "Aortic stent": 31,
34
+ "Aortic valve calcification": 32,
35
+ "Aortic valve replacement": 33,
36
+ "Ascending aorta": 34,
37
+ "Ascites": 35,
38
+ "Atelectasis": 36,
39
+ "Atheroma plaques": 37,
40
+ "Atheroma plaques are observed in the aorta": 38,
41
+ "Atheromatous plaques": 39,
42
+ "Atherosclerotic wall calcifications": 40,
43
+ "Axillary lymphadenopathy": 41,
44
+ "Azygos fissure / lobe": 42,
45
+ "Azygos fissure lobe": 43,
46
+ "Azygos fissure variation": 44,
47
+ "Azygos lobe": 45,
48
+ "Azygos lobe / fissure": 46,
49
+ "Azygos lobe variation": 47,
50
+ "Bilateral adrenal glands": 48,
51
+ "Biliary drainage catheter": 49,
52
+ "Biliary duct dilation": 50,
53
+ "Biliary sludge": 51,
54
+ "Biliary stent / catheter / drain": 52,
55
+ "Bones / Spine": 53,
56
+ "Bones / Spine_others": 54,
57
+ "Bovine arch aorta": 55,
58
+ "Bowel wall thickening / inflammation": 56,
59
+ "Breast": 57,
60
+ "Breast & Axilla": 58,
61
+ "Breast implant": 59,
62
+ "Breast implant (intact or present)": 60,
63
+ "Breast mass / focal asymmetry": 61,
64
+ "Bronchial wall thickening": 62,
65
+ "Bronchiectasis": 63,
66
+ "Bronchopleural fistula": 64,
67
+ "Bronchopneumonia": 65,
68
+ "Bronchopneumonic infiltration": 66,
69
+ "Bulla": 67,
70
+ "Bulla / giant bulla": 68,
71
+ "Bullae / giant bulla": 69,
72
+ "CT involvement score": 70,
73
+ "Calcific mediastinal / hilar lymph nodes": 71,
74
+ "Calcified mediastinal / hilar lymph nodes": 72,
75
+ "Cardiac size and morphology": 73,
76
+ "Cardiomegaly": 74,
77
+ "Cardiovascular": 75,
78
+ "Cardiovascular_others": 76,
79
+ "Cavitary nodule / mass": 77,
80
+ "Central venous catheter": 78,
81
+ "Central venous catheter / PICC": 79,
82
+ "Centrilobular nodules / bronchiolitis pattern": 80,
83
+ "Cervical / supraclavicular lymphadenopathy": 81,
84
+ "Chest Wall": 82,
85
+ "Chest Wall_others": 83,
86
+ "Chest tube": 84,
87
+ "Chest tube / pleural drain": 85,
88
+ "Chest wall mass": 86,
89
+ "Chest wall soft tissue edema": 87,
90
+ "Chest wall soft tissue edema / hematoma": 88,
91
+ "Chest wall tumor invasion": 89,
92
+ "Cholecystectomy": 90,
93
+ "Cholehithiasis / gallstones": 91,
94
+ "Cholelithiasis": 92,
95
+ "Cholelithiasis / gallstones": 93,
96
+ "Complex renal cyst / solid renal mass": 94,
97
+ "Consolidation": 95,
98
+ "Coronary artery calcification": 96,
99
+ "Coronary stent or bypass graft": 97,
100
+ "Crazy-paving pattern": 98,
101
+ "Cylindrical and cystic bronchiectasis": 99,
102
+ "Cylindrical bronchiectasis": 100,
103
+ "DISH": 101,
104
+ "Degenerative / osseous lesions": 102,
105
+ "Degenerative spine changes": 103,
106
+ "Dextrocardia": 104,
107
+ "Diaphragmatic elevation": 105,
108
+ "Diverticulosis": 106,
109
+ "Effusion": 107,
110
+ "Emphysema": 108,
111
+ "Endotracheal tube": 109,
112
+ "Esophageal dilation": 110,
113
+ "Esophageal stent": 111,
114
+ "Esophageal wall thickening / mass": 112,
115
+ "Esophagus dilation": 113,
116
+ "Fibrotic band": 114,
117
+ "Focal liver lesion": 115,
118
+ "Focal liver lesion (nodule / mass)": 116,
119
+ "Focal splenic lesion": 117,
120
+ "Focal splenic lesion (nodule / mass)": 118,
121
+ "Fractures": 119,
122
+ "Free fluid": 120,
123
+ "GGO": 121,
124
+ "Gallbladder": 122,
125
+ "Gallbladder & biliary": 123,
126
+ "Gallbladder wall thickening": 124,
127
+ "Gallstones": 125,
128
+ "Gallstones / cholelithiasis": 126,
129
+ "Goiter": 127,
130
+ "Ground-glass opacity": 128,
131
+ "Ground-glass opacity (GGO)": 129,
132
+ "Gynecomastia": 130,
133
+ "Healed rib fracture": 131,
134
+ "Heart": 132,
135
+ "Heart contour": 133,
136
+ "Heart contour and size": 134,
137
+ "Heart contour and size are natural": 135,
138
+ "Heart contour and size are normal": 136,
139
+ "Heart contour and size are normal.": 137,
140
+ "Heart contour size": 138,
141
+ "Heart contour size is natural": 139,
142
+ "Heart contour size is natural.": 140,
143
+ "Heart contour size is normal": 141,
144
+ "Heart contour, size": 142,
145
+ "Heart contour, size are normal": 143,
146
+ "Heart contour, size are normal.": 144,
147
+ "Heart contour, size is normal": 145,
148
+ "Heart dimensions": 146,
149
+ "Heart dimensions and compartments": 147,
150
+ "Heart has a natural appearance": 148,
151
+ "Heart size": 149,
152
+ "Heart size and morphology": 150,
153
+ "Hemangioma": 151,
154
+ "Hemothorax": 152,
155
+ "Hepatic calcification": 153,
156
+ "Hepatic steatosis": 154,
157
+ "Hepatomegaly": 155,
158
+ "Hepatosteatosis": 156,
159
+ "Hiatal hernia": 157,
160
+ "Hilar lymphadenopathy": 158,
161
+ "Honeycomb appearance": 159,
162
+ "Honeycomb lung": 160,
163
+ "Honeycombing": 161,
164
+ "Horseshoe kidney": 162,
165
+ "Horseshoe kidney variation": 163,
166
+ "Hydatid cyst": 164,
167
+ "Hydronephrosis": 165,
168
+ "Hydropic gallbladder / distension": 166,
169
+ "IVC filter": 167,
170
+ "Infectious process": 168,
171
+ "Interlobular septal thickening": 169,
172
+ "Interstitial / fibrotic lung disease": 170,
173
+ "Interstitial lung disease-fibrosis": 171,
174
+ "Inverted halo sign": 172,
175
+ "Kidneys / urinary tract": 173,
176
+ "LVAD": 174,
177
+ "LVAD / other cardiac assist device": 175,
178
+ "Left atrium": 176,
179
+ "Linear atelectasis": 177,
180
+ "Liver": 178,
181
+ "Liver contour irregularity / cirrhosis features": 179,
182
+ "Liver is in normal appearance.": 180,
183
+ "Liver right lobe transplantation": 181,
184
+ "Liver transplant": 182,
185
+ "Lobar / segmental atelectasis": 183,
186
+ "Lobular kidney contours": 184,
187
+ "Loculated pleural effusion": 185,
188
+ "Lower Neck_others": 186,
189
+ "Lung parenchymal attenuation patterns": 187,
190
+ "Lungs": 188,
191
+ "Lungs & Airways_others": 189,
192
+ "Lymph nodes": 190,
193
+ "Lymphadenopathy": 191,
194
+ "Lytic bone lesion": 192,
195
+ "Lytic-destructive lesions": 193,
196
+ "Main pulmonary artery enlargement": 194,
197
+ "Mediastinal hematoma / fluid collection": 195,
198
+ "Mediastinal lymph nodes": 196,
199
+ "Mediastinal lymphadenopathy": 197,
200
+ "Mediastinal mass": 198,
201
+ "Mediastinal masses / cysts": 199,
202
+ "Mediastinal structures": 200,
203
+ "Mediastinum & Hila": 201,
204
+ "Mediastinum & Hila_others": 202,
205
+ "Middle / posterior mediastinal mass or cyst": 203,
206
+ "Mitral annular calcification": 204,
207
+ "Mixed osteolytic-osteosclerotic lesion": 205,
208
+ "Mosaic attenuation / air-trapping": 206,
209
+ "Motion artifact": 207,
210
+ "Motion artifact / suboptimal study": 208,
211
+ "Mucoid impaction / plugging": 209,
212
+ "Nasogastric / orogastric tube": 210,
213
+ "Nasogastric tube": 211,
214
+ "Neck soft tissue mass": 212,
215
+ "Nephrectomy": 213,
216
+ "Nephrectomy (kidney absent / operated)": 214,
217
+ "Nephrostomy catheter": 215,
218
+ "Neural foramina": 216,
219
+ "No lytic-destructive lesion": 217,
220
+ "No significant intrathoracic abnormality": 218,
221
+ "No upper abdominal free fluid-collection": 219,
222
+ "No upper abdominal free fluid-collection was detected in the sections.": 220,
223
+ "Nodular infiltrates": 221,
224
+ "Nodules and masses": 222,
225
+ "Non-acute / healed rib fracture": 223,
226
+ "Omental caking / peritoneal carcinomatosa": 224,
227
+ "Omental caking / peritoneal carcinomatosis": 225,
228
+ "Osteolytic bone lesion": 226,
229
+ "Osteopenia": 227,
230
+ "Osteophyte": 228,
231
+ "Osteophytes": 229,
232
+ "Osteoporosis": 230,
233
+ "Osteosclerotic bone lesion": 231,
234
+ "Others": 232,
235
+ "Others (devices / post-surgical / global)": 233,
236
+ "Others_others": 234,
237
+ "Pacemaker / ICD leads": 235,
238
+ "Pancreas": 236,
239
+ "Pancreatic calcification": 237,
240
+ "Pancreatic lipomatosis": 238,
241
+ "Pancreatic mass (>3 cm)": 239,
242
+ "Pancreatic mass / focal lesion": 240,
243
+ "Paraseptal emphysema": 241,
244
+ "Parenchymal scarring": 242,
245
+ "Parenchymal scarring / fibrotic band": 243,
246
+ "Pectus excavatum": 244,
247
+ "Pectus excavatum deformity": 245,
248
+ "Pectus excavatus": 246,
249
+ "Pectus excavatus anomaly": 247,
250
+ "Peribronchial sheath thickening": 248,
251
+ "Peribronchial thickening": 249,
252
+ "Peribronchial wall thickening": 250,
253
+ "Pericardial effusion": 251,
254
+ "Pericardial thickening": 252,
255
+ "Pericardial thickening / calcification": 253,
256
+ "Peripheral patchy ground glass densities": 254,
257
+ "Peritoneal carcinomatosis": 255,
258
+ "Pleura_others": 256,
259
+ "Pleural effusion": 257,
260
+ "Pleural nodule": 258,
261
+ "Pleural nodule / mass": 259,
262
+ "Pleural plaques": 260,
263
+ "Pleural thickening": 261,
264
+ "Pneumobilia": 262,
265
+ "Pneumomediastinum": 263,
266
+ "Pneumonia": 264,
267
+ "Pneumonic infiltration": 265,
268
+ "Pneumopericardium": 266,
269
+ "Pneumoperitoneum": 267,
270
+ "Pneumothorax": 268,
271
+ "Post-cholecystectomy": 269,
272
+ "Post-cholecystectomy (gallbladder operated / absent)": 270,
273
+ "Post-lobectomy / segmentectomy": 271,
274
+ "Post-lumpectomy / post-mastectomy change": 272,
275
+ "Post-mastectomy change": 273,
276
+ "Post-pneumonectomy": 274,
277
+ "Post-surgical change": 275,
278
+ "Post-thoracotomy change": 276,
279
+ "Post-thyroidectomy change": 277,
280
+ "Post-transplant change": 278,
281
+ "Postoperative spine change / hardware": 279,
282
+ "Postoperative stomach change": 280,
283
+ "Pulmonary cyst / cystic lung disease": 281,
284
+ "Pulmonary cysts / cystic lung disease": 282,
285
+ "Pulmonary embolism": 283,
286
+ "Pulmonary mass": 284,
287
+ "Pulmonary mass (>3 cm)": 285,
288
+ "Pulmonary nodule": 286,
289
+ "Pulmonary nodule (solid / PSN / GGN)": 287,
290
+ "Pulmonary trunk": 288,
291
+ "Pulmonary trunk caliber": 289,
292
+ "Pulmonary trunk calibration": 290,
293
+ "Pulmonary trunk diameter": 291,
294
+ "Renal artery stent": 292,
295
+ "Renal atrophy": 293,
296
+ "Renal atrophy / decreased renal size": 294,
297
+ "Renal calcification": 295,
298
+ "Renal calculi": 296,
299
+ "Renal calculi / nephrolithiasis": 297,
300
+ "Renal cyst": 298,
301
+ "Reticulation / intralobular thickening": 299,
302
+ "Schmorl nodules": 300,
303
+ "Schmorl's nodules": 301,
304
+ "Sclerotic bone lesion": 302,
305
+ "Scoliosis / kyphosis": 303,
306
+ "Septal thickening": 304,
307
+ "Sequela parenchymal changes": 305,
308
+ "Simple renal cyst": 306,
309
+ "Soft tissue density": 307,
310
+ "Soft tissue masses": 308,
311
+ "Spleen": 309,
312
+ "Spleen size": 310,
313
+ "Spleen sizes": 311,
314
+ "Splenectomy": 312,
315
+ "Splenomegaly": 313,
316
+ "Splenosis": 314,
317
+ "Sternal fracture": 315,
318
+ "Sternal hardware": 316,
319
+ "Study limitation / limited evaluation (non-motion)": 317,
320
+ "Study quality / global": 318,
321
+ "Subcarinal lymph nodes": 319,
322
+ "Subcutaneous emphysema": 320,
323
+ "Subsegmental / linear atelectasis": 321,
324
+ "Surgical hardware": 322,
325
+ "Surgical material": 323,
326
+ "Surgical suture materials": 324,
327
+ "Surgical sutures": 325,
328
+ "Suture materials": 326,
329
+ "Suture materials secondary to bypass surgery": 327,
330
+ "Syndesmophytes": 328,
331
+ "The left hemidiaphragm is elevated": 329,
332
+ "Thoracic aorta diameter": 330,
333
+ "Thoracic aortic aneurysm": 331,
334
+ "Thoracic aortic calcification": 332,
335
+ "Thoracic aortic diameter": 333,
336
+ "Thoracic aortic dilation": 334,
337
+ "Thoracic aortic ectasia / dilation": 335,
338
+ "Thoracic aortic ectasia / dilation (non-aneurysmal)": 336,
339
+ "Thoracic esophageal calibration": 337,
340
+ "Thoracic esophageal dilation": 338,
341
+ "Thoracic esophageal wall thickening": 339,
342
+ "Thoracic esophageal wall thickening / mass": 340,
343
+ "Thoracic esophagus": 341,
344
+ "Thoracic esophagus calibration": 342,
345
+ "Thoracic kyphosis": 343,
346
+ "Thoracic vertebral corpus heights": 344,
347
+ "Thoracic vertebral corpus heights, alignments and densities are normal": 345,
348
+ "Thoracic vertebral corpus heights, alignments and densities are normal.": 346,
349
+ "Thymic remnant / hyperplasia": 347,
350
+ "Thyroid enlargement (goiter)": 348,
351
+ "Thyroid nodule": 349,
352
+ "Trachea": 350,
353
+ "Trachea & Airways": 351,
354
+ "Trachea and main bronchi are open": 352,
355
+ "Tracheal / bronchial wall thickening": 353,
356
+ "Tracheal diverticulum": 354,
357
+ "Tracheal stenosis": 355,
358
+ "Tracheal stenosis / malacia": 356,
359
+ "Tracheal wall thickening": 357,
360
+ "Tracheobronchopathy osteochondroplastica": 358,
361
+ "Tracheomegaly": 359,
362
+ "Tracheostomy tube": 360,
363
+ "Traction bronchiectasis": 361,
364
+ "Traction bronchiectasis / bronchiolectasis": 362,
365
+ "Tree-in-bud": 363,
366
+ "Tubes, catheters, and support devices": 364,
367
+ "Upper Abdomen": 365,
368
+ "Upper Abdomen_others": 366,
369
+ "Upper abdominal free fluid-collection": 367,
370
+ "Upper abdominal vessels": 368,
371
+ "Valves and cardiac devices": 369,
372
+ "Vertebral compression fracture": 370,
373
+ "Vertebral corpus height": 371,
374
+ "Vertebral corpus heights": 372,
375
+ "Vertebral corpus heights are preserved": 373,
376
+ "Vertebral corpus heights are preserved.": 374,
377
+ "Vertebral corpus heights preserved": 375,
378
+ "Vertebral corpus heights, alignments and densities": 376,
379
+ "Vertebral corpus heights, alignments and densities within the sections are normal": 377,
380
+ "Vertebral fracture": 378,
381
+ "Vertebral hemangioma": 379,
382
+ "Viral pneumonia": 380,
383
+ "Volume loss / hyperinflation": 381,
384
+ "azygos fissure variation": 382,
385
+ "tree-in-bud": 383,
386
+ "<UNK>": 384
387
+ }