Spaces:
Paused
Paused
| """ | |
| PHASE 4: Medical Named Entity Recognition (NER). | |
| Extracts medical entities (diseases, treatments, symptoms, medications) from | |
| a health claim using a BERT-based biomedical NER model. | |
| NOTE ON MODEL CHOICE: | |
| `dmis-lab/biobert-v1.1` (the base BioBERT checkpoint your teacher mentioned) is a | |
| BERT encoder pretrained on biomedical text, but it has no classification head -- | |
| it can't do NER out of the box without fine-tuning it yourself on a labeled dataset | |
| (e.g. NCBI-disease or BC5CDR), which takes real training time. | |
| For a working hackathon pipeline, we use `d4data/biomedical-ner-all` -- a model | |
| that IS BioBERT, already fine-tuned for token classification (NER) on biomedical | |
| text. Same underlying BERT/biomedical-transformer technology, just already trained | |
| for exactly this task so it works out of the box. This satisfies the "use BERT" | |
| requirement while being immediately usable. | |
| First run will download the model (~400MB) -- this is normal, only happens once. | |
| """ | |
| from transformers import pipeline | |
| _ner_pipeline = None | |
| def _get_pipeline(): | |
| global _ner_pipeline | |
| if _ner_pipeline is None: | |
| _ner_pipeline = pipeline( | |
| "token-classification", | |
| model="d4data/biomedical-ner-all", | |
| aggregation_strategy="simple", # merges sub-word tokens into full entity words | |
| ) | |
| return _ner_pipeline | |
| CONFIDENCE_THRESHOLD = 0.50 # lowered slightly -- was excluding useful entities like food/drug terms | |
| def extract_entities(claim_text: str) -> list[dict]: | |
| """ | |
| Extract medical entities from a claim. | |
| Returns a list of dicts like: | |
| [ | |
| {"text": "COVID-19", "label": "Disease_disorder", "confidence": 0.97}, | |
| {"text": "garlic", "label": "Food", "confidence": 0.81}, | |
| ... | |
| ] | |
| """ | |
| pipe = _get_pipeline() | |
| raw_entities = pipe(claim_text) | |
| entities = [] | |
| for ent in raw_entities: | |
| if ent["score"] >= CONFIDENCE_THRESHOLD: | |
| entities.append({ | |
| "text": ent["word"].strip(), | |
| "label": ent["entity_group"], | |
| "confidence": round(float(ent["score"]), 2), | |
| }) | |
| return entities | |
| def entities_to_search_query(claim_text: str, entities: list[dict]) -> str: | |
| """ | |
| Build a search query that COMBINES the original claim with extracted | |
| entities (not replace it). This keeps full sentence context for the | |
| embedding model while giving extra weight to the key medical terms. | |
| Replacing the claim entirely with just entity words was losing context | |
| and hurting retrieval accuracy -- combining is more robust. | |
| """ | |
| if not entities: | |
| return claim_text | |
| entity_terms = " ".join(e["text"] for e in entities) | |
| return f"{claim_text} {entity_terms}" # original context + entity emphasis | |
| if __name__ == "__main__": | |
| # Quick manual test -- run: python ner_utils.py | |
| test_claims = [ | |
| "Garlic cures COVID-19", | |
| "Vitamin C supplements prevent the common cold", | |
| "Ibuprofen worsens coronavirus symptoms", | |
| ] | |
| for claim in test_claims: | |
| print(f"\nClaim: '{claim}'") | |
| entities = extract_entities(claim) | |
| if entities: | |
| for e in entities: | |
| print(f" -> {e['text']} [{e['label']}] confidence={e['confidence']}") | |
| print(f" Search query: '{entities_to_search_query(claim, entities)}'") | |
| else: | |
| print(" No entities extracted above confidence threshold.") |