# Running Zeroshot Ner

> Extract arbitrary, custom entity types from clinical or biomedical text with no fine-tuning using OpenMed's GLiNER / GLiNER2 zero-shot support. Use when the user wants to define their own labels on the fly (e.g. Drug, Symptom, Device, Procedure), has no labelled data or a label set not covered by a fine-tuned model, or asks about openmed zero deps/index/infer, the gliner extra, or GLiNER. Pairs adjacent to extracting-clinical-entities (use that for high-accuracy fixed-schema NER) and loading-openmed-models.

- Skill: `maziyarpanahi/running-zeroshot-ner` (Agent Skill)
- Install (CLI): `npx skillmds@latest add maziyarpanahi/running-zeroshot-ner`
- Raw SKILL.md: https://api.skillmd.com/api/skills/maziyarpanahi/running-zeroshot-ner/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: Apache-2.0
- Author: maziyarpanahi (https://skillmd.com/u/maziyarpanahi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/maziyarpanahi/running-zeroshot-ner

---


# Running Zero-Shot NER

Zero-shot NER lets you extract entity types you **name at inference time** — no
training, no labelled data. OpenMed wraps GLiNER (v1) and GLiNER2 behind a small
index + inference layer, exposed via the `openmed zero` CLI and the `openmed.ner`
Python API. It runs on-device.

## When to use

- Your label set is **custom or evolving** ("Device", "Implant", "Allergen") and no
  fine-tuned OpenMed model emits exactly those labels.
- You have **no labelled data** to fine-tune with.
- You need a quick prototype or a one-off extraction over an unusual schema.

**When to prefer a fine-tuned model instead** (`extracting-clinical-entities`):
for a fixed, well-supported schema (diseases, drugs, anatomy), a fine-tuned
OpenMed model is more accurate and faster than zero-shot. Zero-shot trades some
accuracy for total label flexibility — use it for coverage of new types, then
graduate to a fine-tuned model once the schema stabilises.

## Install

```bash
pip install "openmed[gliner]"   # pulls GLiNER (and GLiNER2 if a recent gliner is installed)
openmed zero deps               # diagnostic: prints "GLiNER v1: ok" / "GLiNER v2: ok"
```

`openmed zero deps` only **checks** availability — it does not install anything.

## The two-step workflow: index, then infer

GLiNER checkpoints live as local model directories. OpenMed resolves them by a
short `model_id` via an `index.json`, so you build the index once and run inference
many times.

1. **`openmed zero index <models_dir>`** — scan a directory of downloaded GLiNER /
   GLiNER2 checkpoints and write `index.json` (model ids, family, domains, paths).
2. **`openmed zero infer "<text>" --model-id <id>`** — run extraction against a
   model from the index, with labels you supply.

```bash
# 1) Build the index over your local models (writes <models_dir>/index.json)
openmed zero index /models/gliner --output /models/gliner/index.json

# 2) Run zero-shot NER with your OWN labels (comma-separated)
openmed zero infer "Patient on insulin glargine via an insulin pump for type 1 diabetes." \
  --model-id gliner-biomedical \
  --labels "Drug,Device,Disease" \
  --threshold 0.5 \
  --index-path /models/gliner/index.json
```

Output is JSON: each entity has `text`, `start`, `end`, `label`, and `score`.

CLI flags:

- `zero infer`: positional `text`; `--model-id/-m` (required, an id from the
  index), `--labels/-l` (comma-separated custom labels), `--domain/-d` (label
  preset hint), `--threshold/-c` (default `0.5`), `--index-path/-i`.
- `zero index`: positional `models_dir`; `--output/-o`, `--pretty/--compact`.

If you omit `--labels`, OpenMed falls back to the `--domain` defaults (or generic
defaults). Passing explicit `--labels` is what makes it truly zero-shot.

## Python API

The same flow in code via `openmed.ner`:

```python
from openmed.ner import infer, NerRequest

request = NerRequest(
    model_id="gliner-biomedical",          # id from your index.json
    text="Started on insulin glargine via an insulin pump for type 1 diabetes.",
    labels=["Drug", "Device", "Disease"],  # your custom labels — no fine-tuning
    threshold=0.5,
)
response = infer(request, index_path="/models/gliner/index.json")

for ent in response.entities:
    print(f"{ent.label:8} {ent.text!r:30} {ent.score:.2f} [{ent.start}:{ent.end}]")
```

`NerRequest` fields: `model_id`, `text`, `labels` (None ⇒ domain/default labels),
`domain`, `threshold`. `infer(...)` returns a `NerResponse` whose `.entities` are
`Entity` objects with `.text`, `.start`, `.end`, `.label`, `.score`.

Build / load the index from Python too:

```python
from openmed.ner import build_index, write_index, load_index, is_gliner_available

if is_gliner_available():
    index = build_index("/models/gliner")
    write_index(index, "/models/gliner/index.json")
    index = load_index("/models/gliner/index.json")
```

Helpful label utilities:

```python
from openmed.ner import get_default_labels, available_domains
available_domains()              # domains with built-in label presets
get_default_labels("clinical")   # default labels for a domain hint
```

## Writing good labels

Zero-shot quality hinges on label phrasing. Prefer natural, specific noun phrases:

- Good: `["Drug", "Medical Device", "Disease", "Symptom", "Procedure"]`
- Weak: `["X", "thing", "misc"]`

Tune `threshold` to trade recall for precision. Start at `0.5` and raise it if you
see spurious spans.

## Hand-off to / from OpenMed

- **From `loading-openmed-models`:** zero-shot uses local GLiNER checkpoints rather
  than the OpenMed registry; download them once, then point `zero index` at the
  directory.
- **To `extracting-clinical-entities`:** once your label schema stabilises and a
  fine-tuned OpenMed model covers it, switch to `openmed.analyze_text` for higher
  accuracy and speed. The output shape (label + offsets + score) is parallel, so
  downstream code changes little.
- **To de-identification:** run `openmed.deidentify` **before** zero-shot NER in a
  PHI workflow, then extract entities from the redacted text.

## Edge cases & gotchas

- **`zero infer` needs an index.** Run `zero index <models_dir>` first, or pass a
  valid `--index-path`; the `--model-id` must exist in that index.
- **`zero deps` doesn't install.** It reports status only — install with
  `pip install "openmed[gliner]"`.
- **GLiNER2 needs a recent `gliner`** (≈0.3.0+) and a GLiNER2/Fastino checkpoint;
  `openmed zero deps` shows whether v2 is available.
- **Accuracy vs. flexibility.** Zero-shot is for coverage of new/custom types, not
  for squeezing out maximum F1 on a standard schema.
- **Permissive licensing & local-first.** Use permissively licensed GLiNER
  checkpoints; keep everything on-device and out of PHI logs.

## Standards & references

- GLiNER (zero-shot NER): https://github.com/urchade/GLiNER
- GLiNER paper: https://arxiv.org/abs/2311.08526
- OpenMed model org: https://huggingface.co/OpenMed

