jeff-coreml / runtime.py
alexwengg's picture
Publish validated Jeff GLiFormer Large L128 FP16 Core ML classifier
0deb31c verified
Raw
History Blame Contribute Delete
3.15 kB
"""Standalone Jeff classification inference from the Core ML package.
No original PyTorch checkpoint is loaded. Tokenization and the classification
prompt formatting use the pinned GLiFormer processor and tokenizer files.
"""
from __future__ import annotations
import json
from pathlib import Path
import coremltools as ct
import numpy as np
from gliformer.config import GLiFormerConfig
from gliformer.processing.collator import resolve_gliformer_collator_class
from gliformer.processing.processor import resolve_gliformer_processor_class
from gliner.data_processing.tokenizer import WordsSplitter
from transformers import AutoTokenizer
from jeff_decision import marker_positions
class JeffCoreML:
"""One-group, one-text Jeff classifier for 1–8 labels and at most 128 tokens."""
def __init__(self, asset_dir: str | Path, package: str | Path, compute_units=ct.ComputeUnit.ALL):
asset_dir = Path(asset_dir)
config = GLiFormerConfig(**json.loads((asset_dir / "gliner_config.json").read_text()))
tokenizer = AutoTokenizer.from_pretrained(asset_dir, local_files_only=True)
splitter = WordsSplitter(config.words_splitter_type)
processor_cls = resolve_gliformer_processor_class(config)
processor = processor_cls(config, tokenizer, splitter)
collator_cls = resolve_gliformer_collator_class(config)
self.collator = collator_cls(config, data_processor=processor, return_tokens=True, prepare_labels=False)
self.splitter = splitter
self.config = config
self.model = ct.models.MLModel(str(package), compute_units=compute_units)
self.output_name = self.model.get_spec().description.output[0].name
def score(self, text: str, labels: list[str], name: str = "", description: str = "") -> list[float]:
if not 1 <= len(labels) <= 8:
raise ValueError("JeffCoreML accepts 1 to 8 labels")
tokens = [word for word, _, _ in self.splitter(text)]
batch = self.collator([{
"tokenized_text": tokens,
"classification": [{
"name": name,
"description": description,
"all_labels": labels,
"true_labels": [],
}],
}])
ids = batch["input_ids"]
mask = batch["attention_mask"]
if ids.shape[1] > 128:
raise ValueError(f"JeffCoreML L128 bucket cannot fit {ids.shape[1]} tokens")
parent, children, count = marker_positions(ids, self.config, 8)
if count != len(labels):
raise ValueError("tokenizer label markers disagree with the supplied label count")
inputs = {
"input_ids": np.pad(ids.numpy().astype(np.int32), ((0, 0), (0, 128 - ids.shape[1]))),
"attention_mask": np.pad(mask.numpy().astype(np.int32), ((0, 0), (0, 128 - mask.shape[1]))),
"parent_position": parent.numpy(),
"category_positions": children.numpy(),
}
logits = self.model.predict(inputs)[self.output_name][0, :count].astype(np.float32)
probabilities = 1.0 / (1.0 + np.exp(-logits))
return probabilities.tolist()