Disease NER on Apple Silicon with OpenMed and MLX

⬅️ Back to Projects

🔧 Disease NER on Apple Silicon

What it isLocal disease entity extraction (B-DISEASE / I-DISEASE) from clinical-ish text
StackOpenMed + MLX on Apple Silicon; model OpenMed-NER-DiseaseDetect-BioMed-335M
StatusWorking pipeline; one-time MLX conversion per machine
Docsopenmed.life/docs · Model Registry

You want disease names pulled out of notes on your Mac without sending text to a cloud API. OpenMed is built for that: curated biomedical models, analyze_text() for one-liners, BatchProcessor for files, optional PII de-ID and REST later. The catch most Mac tinkerers hit first: this checkpoint is a 335M BERT token-classifier, not a chat LLM—mlx-lm and GGUF are the wrong tools. Use pip install "openmed[mlx]" and OpenMed’s MLX converter instead.

Not medical advice. This tags text spans. It does not diagnose, triage, or replace a clinician. Below uses 100% synthetic sentences only.

What I learned

  1. mlx-lm is for generation; NER needs token classification. OpenMed routes BERT-family checkpoints through openmed.mlx.convert and OpenMedConfig(backend="mlx"). Trying to load this model in Ollama or llama.cpp wastes an afternoon.
  2. Plan for a one-time conversion. There is no prebuilt -mlx Hub repo for this exact checkpoint yet. Conversion on your Mac takes a few minutes and ~670 MB (BF16); optional --quantize 8 shrinks RAM with a small accuracy tradeoff.
  3. analyze_text hides BIO decoding. Raw mlx-transformers works but you rebuild span grouping yourself. OpenMed’s grouping, confidence thresholds, and export helpers are worth the dependency—see Advanced NER & Output Formatting in their docs.
  4. Synthetic notes are enough to validate the pipe. Before you touch real charts, run invented sentences through batch mode and inspect false positives (family history phrasing, negation). NER is literal; “ruled out myocardial infarction” may still tag myocardial infarction.
  5. Intel Mac = CPU PyTorch, not MLX GPU. pip install "openmed[hf]" still runs; Apple Silicon is where this shines. Check uname -marm64.

End-to-end pipeline

Flow: venv → install → convert model → write synthetic notes.jsonl → batch NER → entities.csv.

1. Environment (Apple Silicon)

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install "openmed[mlx]"

Optional Hub cache: pip install huggingface_hub && hf auth login

2. One-time MLX conversion

python -m openmed.mlx.convert \
  --model OpenMed/OpenMed-NER-DiseaseDetect-BioMed-335M \
  --output ./mlx-models/disease-biomed-335m

Expect config.json, id2label.json, openmed-mlx.json, weights.safetensors, tokenizer files. Tight on RAM? Add --quantize 8 to the output path.

Shortcut: pass the Hub ID directly to analyze_text on first run—OpenMed may auto-prepare MLX. If it falls back to PyTorch, run the convert step explicitly.

3. Synthetic input (data/notes.jsonl)

Invented records only—one JSON object per line:

{"id": "syn-001", "text": "The patient was diagnosed with diabetes mellitus type 2 and started metformin."}
{"id": "syn-002", "text": "Family history is notable for Alzheimer's disease and hypertension."}
{"id": "syn-003", "text": "MRI was negative; Crohn's disease was ruled out after colonoscopy."}
{"id": "syn-004", "text": "Pediatric workup considered cystic fibrosis given recurrent pulmonary infections."}

4. Batch script (run_disease_ner.py)

#!/usr/bin/env python3
"""Synthetic clinical notes → disease entities CSV. Not for real PHI."""
import csv
import json
from pathlib import Path

from openmed import BatchProcessor
from openmed.core.config import OpenMedConfig

MODEL = "./mlx-models/disease-biomed-335m"
NOTES = Path("data/notes.jsonl")
OUT = Path("data/entities.csv")

def load_notes(path: Path) -> list[tuple[str, str]]:
    rows = []
    for line in path.read_text().splitlines():
        if not line.strip():
            continue
        obj = json.loads(line)
        rows.append((obj["id"], obj["text"]))
    return rows

def main() -> None:
    notes = load_notes(NOTES)
    processor = BatchProcessor(
        model_name=MODEL,
        config=OpenMedConfig(backend="mlx"),
        group_entities=True,
    )
    texts = [t for _, t in notes]
    ids = [i for i, _ in notes]

    OUT.parent.mkdir(parents=True, exist_ok=True)
    with OUT.open("w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["note_id", "label", "text", "confidence"])
        for note_id, result in zip(ids, processor.process_texts(texts, batch_size=8)):
            for e in result.entities:
                w.writerow([note_id, e.label, e.text, f"{e.confidence:.3f}"])

    print(f"Wrote {OUT}")

if __name__ == "__main__":
    main()
python run_disease_ner.py

Example rows you should see:

note_id,label,text,confidence
syn-001,DISEASE,diabetes mellitus type 2,0.9xx
syn-002,DISEASE,Alzheimer's disease,0.9xx
syn-002,DISEASE,hypertension,0.8xx

syn-003 is the sanity check: if Crohn's disease tags despite “ruled out”, you have seen NER’s negation blind spot—do not ship that to production without a second pass.

5. Single-string smoke test

from openmed import analyze_text
from openmed.core.config import OpenMedConfig

result = analyze_text(
    "Symptoms of rheumatoid arthritis worsened over three months.",
    model_name="./mlx-models/disease-biomed-335m",
    config=OpenMedConfig(backend="mlx"),
    confidence_threshold=0.55,
    group_entities=True,
)
for e in result.entities:
    print(e.label, e.text, f"{e.confidence:.3f}")

What not to use

ToolWhy
mlx-lmCausal LLMs only
llama.cpp / GGUFWrong architecture
Default PyTorch on M-seriesWorks, skips MLX GPU

Troubleshooting

IssueFix
ImportError: mlxpip install "openmed[mlx]"
Slow downloadhf download OpenMed/OpenMed-NER-DiseaseDetect-BioMed-335M --local-dir ./models/biomed-335m
OOMRe-convert with --quantize 8
Verify MLXActivity Monitor GPU during analyze_text; config must set backend="mlx"

OpenMed v1.7 adds REST, PII de-ID, FHIR helpers, and OpenMedKit for Swift—this article stops at Python MLX NER. That is already enough for indexing synthetic corpora or prototyping chart miners.

Steal this if: you are building a local health-text lab on a Mac and need disease spans before summarization, search, or de-identification—not if you need a chatbot (pick an LLM stack instead).

Related TMFNK Content

Crepi il lupo! 🐺