""" MAUDE NLP Classifier v2 — Bio_ClinicalBERT Inference Demo Hugging Face Space: mukundisb/maude-classifier-bert Tabs: 1. Single Inference — classify a free-text MAUDE narrative 2. Model Card — metrics comparison Phase 1 (TF-IDF) vs Phase 2 (ClinicalBERT) """ import json import os import numpy as np import streamlit as st import torch import torch.nn as nn import matplotlib import matplotlib.pyplot as plt from transformers import AutoModel, AutoTokenizer from huggingface_hub import hf_hub_download matplotlib.use("Agg") HUB_REPO = "mukundisb/maude-clinicalbert" LABEL_ORDER = ["D", "I", "M", "O"] LABEL_NAMES = {"D": "Death", "I": "Injury", "M": "Malfunction", "O": "Other"} SEVERITY_COLORS = { "D": "#d62728", # red "I": "#ff7f0e", # orange "M": "#1f77b4", # blue "O": "#7f7f7f", # grey } POOLING = "cls_mean_concat" MAX_LENGTH = 512 # ── Model class (must match Kaggle training architecture exactly) ───────────── class BertSeverityClassifier(nn.Module): def __init__(self, bert, num_labels: int, dropout: float, pooling: str): super().__init__() self.bert = bert H = bert.config.hidden_size self.dropout = nn.Dropout(dropout) self.pooling = pooling head_in = H * 2 if pooling == "cls_mean_concat" else H self.classifier = nn.Linear(head_in, num_labels) def forward(self, input_ids, attention_mask, token_type_ids=None): out = self.bert(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids) last = out.last_hidden_state cls = last[:, 0, :] if self.pooling == "cls": pooled = cls elif self.pooling == "mean": mask = attention_mask.unsqueeze(-1).float() pooled = (last * mask).sum(1) / mask.sum(1).clamp(min=1) else: # cls_mean_concat mask = attention_mask.unsqueeze(-1).float() mean = (last * mask).sum(1) / mask.sum(1).clamp(min=1) pooled = torch.cat([cls, mean], dim=-1) return self.classifier(self.dropout(pooled)) # ── Model loader ────────────────────────────────────────────────────────────── @st.cache_resource(show_spinner="Loading Bio_ClinicalBERT from Hub (first run ~30 s)…") def load_model_and_tokenizer(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") tokenizer = AutoTokenizer.from_pretrained(HUB_REPO, do_lower_case=True) bert = AutoModel.from_pretrained(HUB_REPO) head_path = hf_hub_download(repo_id=HUB_REPO, filename="classifier_head.pt") head_data = torch.load(head_path, map_location="cpu", weights_only=True) model = BertSeverityClassifier( bert=bert, num_labels=head_data["num_labels"], dropout=head_data["dropout_p"], pooling=head_data["pooling"], ) model.classifier.load_state_dict(head_data["classifier_state_dict"]) model = model.to(device) model.eval() return model, tokenizer, device def predict(model, tokenizer, device, text: str) -> dict: enc = tokenizer( text, truncation=True, padding="max_length", max_length=MAX_LENGTH, return_tensors="pt", ) input_ids = enc["input_ids"].to(device) attention_mask = enc["attention_mask"].to(device) token_type_ids = enc.get("token_type_ids") if token_type_ids is not None: token_type_ids = token_type_ids.to(device) with torch.no_grad(): logits = model(input_ids, attention_mask, token_type_ids) probs = torch.softmax(logits, dim=-1).squeeze().cpu().numpy() label_order = LABEL_ORDER predicted = label_order[int(np.argmax(probs))] return { "predicted_label": predicted, "probabilities": {lbl: float(probs[i]) for i, lbl in enumerate(label_order)}, } # ── Page config ─────────────────────────────────────────────────────────────── st.set_page_config( page_title="MAUDE ClinicalBERT Classifier", page_icon="🏥", layout="wide", initial_sidebar_state="collapsed", ) st.title("🏥 MAUDE Adverse Event Classifier — Bio_ClinicalBERT") st.markdown( "Classify FDA MAUDE adverse event narratives by severity using a fine-tuned " "[Bio_ClinicalBERT](https://huggingface.co/emilyalsentzer/Bio_ClinicalBERT) model. " "Classes: **D**eath · **I**njury · **M**alfunction · **O**ther" ) # Load model once — cached across all users try: model, tokenizer, device = load_model_and_tokenizer() model_ready = True hw = "GPU" if device.type == "cuda" else "CPU" st.caption(f"Model loaded · {hw} · fold 3 (Death F1 = 0.842, Weighted F1 = 0.869)") except Exception as exc: model_ready = False st.error(f"Could not load model from {HUB_REPO}: {exc}") # ── Tabs ────────────────────────────────────────────────────────────────────── tab_infer, tab_card = st.tabs(["🔍 Single Inference", "📋 Model Card"]) # ══════════════════════════════════════════════════════════════════════════════ # TAB 1 — Single Inference # ══════════════════════════════════════════════════════════════════════════════ with tab_infer: st.subheader("Classify an Adverse Event Narrative") SAMPLES = { "Select a sample…": "", "Pump malfunction (no injury)": ( "The infusion pump alarmed and stopped delivering medication. " "No patient injury was reported. The device was returned for analysis." ), "Patient death — pacemaker": ( "The patient experienced cardiac arrest following implantation of the pacemaker. " "The patient passed away 48 hours post-procedure. The event was considered device-related." ), "Serious injury — burn": ( "The patient sustained third-degree burns on the left forearm during an " "electrosurgical procedure due to a grounding pad malfunction." ), "Catheter — minor injury": ( "During catheter insertion the patient reported significant pain and minor bleeding. " "The procedure was discontinued and the patient was monitored overnight." ), } sample_choice = st.selectbox("Load a sample narrative", list(SAMPLES.keys())) narrative_input = st.text_area( "Narrative text", value=SAMPLES[sample_choice], height=160, placeholder="Paste an MDR adverse event narrative here…", ) classify_btn = st.button( "🔎 Classify", use_container_width=True, disabled=not model_ready, ) if classify_btn: text = narrative_input.strip() if not text: st.warning("Please enter or select a narrative text.") else: with st.spinner("Running inference… (~3–8 s on CPU)"): result = predict(model, tokenizer, device, text) predicted = result["predicted_label"] label_name = LABEL_NAMES.get(predicted, predicted) color = SEVERITY_COLORS.get(predicted, "#333333") top_prob = result["probabilities"][predicted] st.markdown( f"