Text Classification
Transformers
Safetensors
English
chest2vec_labeler
feature-extraction
radiology
chest-ct
report-labeling
multi-label
ct-rate
chexbert-style-f1
custom_code
Instructions to use chest2vec/chest2vec_labeler with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use chest2vec/chest2vec_labeler with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="chest2vec/chest2vec_labeler", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("chest2vec/chest2vec_labeler", trust_remote_code=True, dtype="auto") - Notebooks
- Google Colab
- Kaggle
Upload modeling_chest2vec_labeler.py with huggingface_hub
Browse files- modeling_chest2vec_labeler.py +69 -20
modeling_chest2vec_labeler.py
CHANGED
|
@@ -50,6 +50,7 @@ class Chest2VecLabelerConfig(PretrainedConfig):
|
|
| 50 |
instruction: str = "Given the following chest CT report, extract the presence/absence of entities",
|
| 51 |
max_len: int = 512,
|
| 52 |
default_threshold: float = 0.5,
|
|
|
|
| 53 |
**kwargs,
|
| 54 |
):
|
| 55 |
super().__init__(**kwargs)
|
|
@@ -62,6 +63,7 @@ class Chest2VecLabelerConfig(PretrainedConfig):
|
|
| 62 |
self.instruction = instruction
|
| 63 |
self.max_len = max_len
|
| 64 |
self.default_threshold = default_threshold
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
@dataclass
|
|
@@ -201,35 +203,82 @@ class Chest2VecLabelerModel(PreTrainedModel):
|
|
| 201 |
names = out["labels"]
|
| 202 |
return [{names[j]: "positive" for j in range(len(names)) if row[j]} for row in out["positive"]]
|
| 203 |
|
| 204 |
-
# ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
@torch.no_grad()
|
| 206 |
def score_reports(self, gt_reports: List[str], pred_reports: List[str], tokenizer=None,
|
| 207 |
threshold: Optional[float] = None, batch_size: int = 16,
|
| 208 |
-
max_len: Optional[int] = None, device=None
|
|
|
|
| 209 |
"""
|
| 210 |
-
Label both GT and predicted reports, then compute label-
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
`
|
| 214 |
-
|
| 215 |
"""
|
| 216 |
from sklearn.metrics import precision_recall_fscore_support
|
|
|
|
| 217 |
if len(gt_reports) != len(pred_reports):
|
| 218 |
raise ValueError("gt_reports and pred_reports must have the same length")
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
res[
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
return res
|
| 234 |
|
| 235 |
|
|
|
|
| 50 |
instruction: str = "Given the following chest CT report, extract the presence/absence of entities",
|
| 51 |
max_len: int = 512,
|
| 52 |
default_threshold: float = 0.5,
|
| 53 |
+
label_hierarchy: Optional[dict] = None,
|
| 54 |
**kwargs,
|
| 55 |
):
|
| 56 |
super().__init__(**kwargs)
|
|
|
|
| 63 |
self.instruction = instruction
|
| 64 |
self.max_len = max_len
|
| 65 |
self.default_threshold = default_threshold
|
| 66 |
+
self.label_hierarchy = label_hierarchy or {}
|
| 67 |
|
| 68 |
|
| 69 |
@dataclass
|
|
|
|
| 203 |
names = out["labels"]
|
| 204 |
return [{names[j]: "positive" for j in range(len(names)) if row[j]} for row in out["positive"]]
|
| 205 |
|
| 206 |
+
# ---- hierarchy roll-up (leaf -> upper -> anatomy), max over children ----
|
| 207 |
+
def aggregate_hierarchy(self, leaf_prob):
|
| 208 |
+
"""Roll leaf positive-probabilities up to upper and anatomy levels (max over children).
|
| 209 |
+
|
| 210 |
+
Mirrors the training-time evaluation: each upper group's score is the max over its
|
| 211 |
+
child-leaf probabilities; each anatomy score is the max over its upper groups plus the
|
| 212 |
+
section's `*_others` leaf. Returns (upper_prob, upper_names, anatomy_prob, anatomy_names).
|
| 213 |
+
"""
|
| 214 |
+
import numpy as np
|
| 215 |
+
leaf_prob = np.asarray(leaf_prob, dtype=np.float32)
|
| 216 |
+
H = self.config.label_hierarchy or {}
|
| 217 |
+
idx = {n: i for i, n in enumerate(self.config.labels)}
|
| 218 |
+
N = leaf_prob.shape[0]
|
| 219 |
+
u_names, u_cols, a_names, a_cols = [], [], [], []
|
| 220 |
+
for anat, groups in H.items():
|
| 221 |
+
a_names.append(anat)
|
| 222 |
+
ac = np.full(N, -1.0, dtype=np.float32)
|
| 223 |
+
for up, leaves in groups.items():
|
| 224 |
+
u_names.append(up)
|
| 225 |
+
cols = [idx[l] for l in leaves if l in idx]
|
| 226 |
+
uc = leaf_prob[:, cols].max(axis=1) if cols else np.zeros(N, dtype=np.float32)
|
| 227 |
+
u_cols.append(uc)
|
| 228 |
+
ac = np.maximum(ac, uc)
|
| 229 |
+
okey = f"{anat}_others"
|
| 230 |
+
if okey in idx:
|
| 231 |
+
ac = np.maximum(ac, leaf_prob[:, idx[okey]])
|
| 232 |
+
a_cols.append(np.maximum(ac, 0.0))
|
| 233 |
+
import numpy as _np
|
| 234 |
+
up = _np.column_stack(u_cols) if u_cols else _np.zeros((N, 0), dtype=_np.float32)
|
| 235 |
+
an = _np.column_stack(a_cols) if a_cols else _np.zeros((N, 0), dtype=_np.float32)
|
| 236 |
+
return up, u_names, an, a_names
|
| 237 |
+
|
| 238 |
+
# ---- CheXbert / SRR-BERT-style report-comparison F1 (leaf / upper / anatomy) ----
|
| 239 |
@torch.no_grad()
|
| 240 |
def score_reports(self, gt_reports: List[str], pred_reports: List[str], tokenizer=None,
|
| 241 |
threshold: Optional[float] = None, batch_size: int = 16,
|
| 242 |
+
max_len: Optional[int] = None, device=None,
|
| 243 |
+
levels=("leaf", "upper", "anatomy")) -> Dict[str, Any]:
|
| 244 |
"""
|
| 245 |
+
Label both GT and predicted reports, then compute label-agreement F1 (CheXbert-style)
|
| 246 |
+
at the requested hierarchy levels.
|
| 247 |
|
| 248 |
+
`gt_reports` labels are treated as truth, `pred_reports` as prediction. For each level
|
| 249 |
+
in `levels` ("leaf" = 137 labels, "upper" = container groups, "anatomy" = sections),
|
| 250 |
+
returns micro / macro / weighted precision-recall-F1 plus per-label scores.
|
| 251 |
"""
|
| 252 |
from sklearn.metrics import precision_recall_fscore_support
|
| 253 |
+
import numpy as np
|
| 254 |
if len(gt_reports) != len(pred_reports):
|
| 255 |
raise ValueError("gt_reports and pred_reports must have the same length")
|
| 256 |
+
thr = self.config.default_threshold if threshold is None else threshold
|
| 257 |
+
kw = dict(tokenizer=tokenizer, batch_size=batch_size, max_len=max_len, device=device)
|
| 258 |
+
gt_leaf = self.predict_proba(gt_reports, **kw).numpy()
|
| 259 |
+
pr_leaf = self.predict_proba(pred_reports, **kw).numpy()
|
| 260 |
+
|
| 261 |
+
level_inputs = {"leaf": (gt_leaf, pr_leaf, list(self.config.labels))}
|
| 262 |
+
if "upper" in levels or "anatomy" in levels:
|
| 263 |
+
gu, un, ga, an = self.aggregate_hierarchy(gt_leaf)
|
| 264 |
+
pu, _, pa, _ = self.aggregate_hierarchy(pr_leaf)
|
| 265 |
+
level_inputs["upper"] = (gu, pu, un)
|
| 266 |
+
level_inputs["anatomy"] = (ga, pa, an)
|
| 267 |
+
|
| 268 |
+
res: Dict[str, Any] = {"n_reports": len(gt_reports), "threshold": thr}
|
| 269 |
+
for lvl in levels:
|
| 270 |
+
gp, pp, names = level_inputs[lvl]
|
| 271 |
+
y_true = (gp >= thr).astype(int)
|
| 272 |
+
y_pred = (pp >= thr).astype(int)
|
| 273 |
+
block: Dict[str, Any] = {"n_labels": len(names)}
|
| 274 |
+
for avg in ("micro", "macro", "weighted"):
|
| 275 |
+
p, r, f, _ = precision_recall_fscore_support(y_true, y_pred, average=avg, zero_division=0)
|
| 276 |
+
block[avg] = {"precision": float(p), "recall": float(r), "f1": float(f)}
|
| 277 |
+
p, r, f, s = precision_recall_fscore_support(y_true, y_pred, average=None,
|
| 278 |
+
labels=list(range(len(names))), zero_division=0)
|
| 279 |
+
block["per_label"] = {names[j]: {"precision": float(p[j]), "recall": float(r[j]),
|
| 280 |
+
"f1": float(f[j]), "support_gt": int(s[j])} for j in range(len(names))}
|
| 281 |
+
res[lvl] = block
|
| 282 |
return res
|
| 283 |
|
| 284 |
|