Disease NER on Apple Silicon with OpenMed and MLX
🔧 Disease NER on Apple Silicon
| What it is | Local disease entity extraction (B-DISEASE / I-DISEASE) from clinical-ish text |
| Stack | OpenMed + MLX on Apple Silicon; model OpenMed-NER-DiseaseDetect-BioMed-335M |
| Status | Working pipeline; one-time MLX conversion per machine |
| Docs | openmed.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
mlx-lmis for generation; NER needs token classification. OpenMed routes BERT-family checkpoints throughopenmed.mlx.convertandOpenMedConfig(backend="mlx"). Trying to load this model in Ollama or llama.cpp wastes an afternoon.- Plan for a one-time conversion. There is no prebuilt
-mlxHub repo for this exact checkpoint yet. Conversion on your Mac takes a few minutes and ~670 MB (BF16); optional--quantize 8shrinks RAM with a small accuracy tradeoff. analyze_texthides BIO decoding. Rawmlx-transformersworks 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.- 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. - Intel Mac = CPU PyTorch, not MLX GPU.
pip install "openmed[hf]"still runs; Apple Silicon is where this shines. Checkuname -m→arm64.
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-335mExpect 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.pyExample 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.8xxsyn-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
| Tool | Why |
|---|---|
mlx-lm | Causal LLMs only |
| llama.cpp / GGUF | Wrong architecture |
| Default PyTorch on M-series | Works, skips MLX GPU |
Troubleshooting
| Issue | Fix |
|---|---|
ImportError: mlx | pip install "openmed[mlx]" |
| Slow download | hf download OpenMed/OpenMed-NER-DiseaseDetect-BioMed-335M --local-dir ./models/biomed-335m |
| OOM | Re-convert with --quantize 8 |
| Verify MLX | Activity 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
- Fine-Tune a Local SLM to Clean Master Data on Your Mac Same “local model on Apple Silicon” lane, different task (record cleaning vs. clinical NER).
- Running Local LLMs: From First Run to Fine-Tuned When you actually do need
mlx-lm/ Ollama—and how not to confuse that path with classifiers. - llmfit: Find Which LLM Models Run on Your Hardware Right-size generative models; NER at 335M is tiny by comparison (~1 GB class).
Crepi il lupo! 🐺