mill-screen / inference_example.py
Deeptanshuu's picture
Rename to mill-screen: update repo ID and links
1e37e3c verified
Raw History Blame Contribute Delete
5.99 kB
#!/usr/bin/env python3
"""Runnable example: batch toxicity prediction with Mill, using tuned per-class thresholds.
python inference_example.py # pull from the Hub
python inference_example.py /path/to/local/repo # use a local checkout
python inference_example.py --device cuda
Only needs `transformers`, `torch` and `huggingface_hub`. Nothing from the
training repo is imported; `modeling_toxic_xlmr.py` ships alongside this file
and is loaded for you by `trust_remote_code=True`.
The three things people get wrong with this model:
1. It needs `lang_ids`. Omit it and every row is scored as English (id 0),
silently and with no error.
2. The six labels are independent, not one choice out of six. Any number of
them can fire on the same comment.
3. 0.5 is the wrong cut. Use `thresholds.json`; the rare classes need cuts
near 0.37.
"""
import argparse
import json
import os
import sys
import torch
from transformers import AutoModel, AutoTokenizer
REPO_ID = "Deeptanshuu/mill-screen"
# Baked into the trained language embedding table. Do not renumber.
LANGUAGE_IDS = {"en": 0, "ru": 1, "tr": 2, "es": 3, "fr": 4, "it": 5, "pt": 6}
# Classifier output order. Index 1 is severe_toxic and index 2 is obscene,
# which is neither alphabetical nor the order most people would guess.
LABEL_NAMES = ["toxic", "severe_toxic", "obscene", "threat", "insult", "identity_hate"]
def load_thresholds(source: str) -> dict:
"""Read thresholds.json from a local directory or from the Hub."""
if os.path.isdir(source):
path = os.path.join(source, "thresholds.json")
else:
from huggingface_hub import hf_hub_download
path = hf_hub_download(repo_id=source, filename="thresholds.json")
with open(path) as f:
return json.load(f)["thresholds"]
@torch.no_grad()
def predict(model, tokenizer, texts, langs, thresholds, batch_size=8, max_length=512):
"""Score a list of texts and apply the per-class thresholds.
Args:
texts: list of strings.
langs: list of language codes, one per text, from LANGUAGE_IDS.
thresholds: {label_name: float}.
Returns:
list of dicts, one per input text.
"""
if len(texts) != len(langs):
raise ValueError(f"{len(texts)} texts but {len(langs)} language codes")
unknown = sorted({l for l in langs if l not in LANGUAGE_IDS})
if unknown:
raise ValueError(
f"Unsupported language codes {unknown}. This model knows "
f"{sorted(LANGUAGE_IDS)}. Pick the closest supported language rather "
"than letting it default silently."
)
device = next(model.parameters()).device
cut = torch.tensor([thresholds[name] for name in LABEL_NAMES], device=device)
results = []
for start in range(0, len(texts), batch_size):
chunk = texts[start : start + batch_size]
chunk_langs = langs[start : start + batch_size]
enc = tokenizer(
chunk,
padding=True,
truncation=True,
max_length=max_length, # must be 512; training used 512
return_tensors="pt",
).to(device)
lang_ids = torch.tensor(
[LANGUAGE_IDS[l] for l in chunk_langs], dtype=torch.long, device=device
)
out = model(
input_ids=enc["input_ids"],
attention_mask=enc["attention_mask"],
lang_ids=lang_ids,
)
probs = out["probabilities"]
fired = probs >= cut
for text, lang, prob_row, fired_row in zip(chunk, chunk_langs, probs, fired):
labels = [n for n, f in zip(LABEL_NAMES, fired_row.tolist()) if f]
results.append(
{
"text": text,
"language": lang,
"probabilities": {
n: round(float(p), 4) for n, p in zip(LABEL_NAMES, prob_row.tolist())
},
"labels": labels,
"any_toxic": bool(labels),
}
)
return results
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
parser.add_argument(
"source",
nargs="?",
default=REPO_ID,
help="Hub repo id or a local directory holding config.json and pytorch_model.bin",
)
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
parser.add_argument("--batch-size", type=int, default=8)
args = parser.parse_args()
print(f"loading {args.source} on {args.device}", file=sys.stderr)
model = AutoModel.from_pretrained(args.source, trust_remote_code=True)
model.to(args.device).eval()
# The tokenizer is stock xlm-roberta-large, unmodified. It ships in this
# repo so the whole thing is self-contained, but the line below works
# equally well against "xlm-roberta-large".
tokenizer = AutoTokenizer.from_pretrained(args.source)
thresholds = load_thresholds(args.source)
samples = [
("You are an absolute idiot and everyone here hates you.", "en"),
("Thanks for catching that typo, much appreciated.", "en"),
("Sei un cretino, vattene via da qui.", "it"),
("Merci beaucoup pour ton aide, c'etait tres clair.", "fr"),
("Defol git buradan, seni asagilik herif.", "tr"),
]
texts = [t for t, _ in samples]
langs = [l for _, l in samples]
for row in predict(model, tokenizer, texts, langs, thresholds, batch_size=args.batch_size):
verdict = ", ".join(row["labels"]) if row["labels"] else "clean"
print(f"\n[{row['language']}] {row['text']}")
print(f" fires: {verdict}")
print(
" probs: "
+ " ".join(f"{n}={row['probabilities'][n]:.3f}" for n in LABEL_NAMES)
)
print("\nthresholds used: " + json.dumps(thresholds))
return 0
if __name__ == "__main__":
raise SystemExit(main())