mill-screen / README.md
Deeptanshuu's picture
Rename to mill-screen: update repo ID and links
1e37e3c verified
|
Raw History Blame Contribute Delete
9.02 kB
---
language:
- en
- ru
- tr
- es
- fr
- it
- pt
license: apache-2.0
library_name: transformers
pipeline_tag: text-classification
base_model: xlm-roberta-large
tags:
- toxicity
- content-moderation
- multilingual
- multi-label
- xlm-roberta
- custom_code
metrics:
- roc_auc
- f1
model-index:
- name: Mill Screen
results:
- task:
type: text-classification
name: Multi-label toxicity classification
dataset:
type: custom
name: Multilingual Jigsaw-derived toxicity corpus (7 languages)
split: test
metrics:
- type: roc_auc
name: Macro AUC-ROC
value: 0.9852
- type: f1
name: Macro F1 (tuned thresholds)
value: 0.8814
- type: f1
name: Macro F1 (threshold 0.5)
value: 0.8821
---
> **Known bias: short benign self-descriptions that name an identity are flagged as toxic.**
>
> | Probe, one short sentence | P(toxic) |
> |---|---|
> | Sexual orientation, two probes | **0.891**, **0.668** |
> | Race | **0.612** |
> | LGBTQ identity | **0.531** |
> | Disability | **0.485** |
> | Control, gender only, two probes | 0.067, 0.038 |
>
> The `toxic` threshold is 0.472, so the first five fire and the controls do not. Benign
> identity probes are flagged at 0.41 (en) to 0.74 (pt); controls at 0.00 in all seven languages.
> On real held-out rows with no positive label, identity-term rows are flagged 2.84x as often
> (95% CI [1.96, 3.82]). One paragraph of neutral filler in front drops the top probe to 0.042.
> The cause is the training data: benign identity usage is almost absent from it. Do not run this
> on user content without mitigation or human review. [Analysis](https://github.com/Deeptanshuu/mill-screen/blob/main/experiments/identity_bias.md).
# Mill Screen
**Unstructured comment in, typed probabilistic decisions out.**
Six independent toxicity probabilities per comment, in one forward pass, for English, Russian,
Turkish, Spanish, French, Italian and Portuguese. XLM-RoBERTa-large plus one language-conditioned
attention block. Similar in idea to TypeSafe's
[Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev); not affiliated.
| Label | Index | Meaning |
|---|---|---|
| `toxic` | 0 | Rude or disrespectful |
| `severe_toxic` | 1 | Extreme `toxic` (a subset of it) |
| `obscene` | 2 | Obscene or vulgar |
| `threat` | 3 | Threat of violence |
| `insult` | 4 | Insulting toward a person |
| `identity_hate` | 5 | Hatred toward an identity group |
Labels are not exclusive: each has its own sigmoid and its own threshold.
## Quick start
```python
import json, torch
from huggingface_hub import hf_hub_download
from transformers import AutoModel, AutoTokenizer
REPO = "Deeptanshuu/mill-screen"
LANGUAGE_IDS = {"en": 0, "ru": 1, "tr": 2, "es": 3, "fr": 4, "it": 5, "pt": 6}
LABELS = ["toxic", "severe_toxic", "obscene", "threat", "insult", "identity_hate"]
model = AutoModel.from_pretrained(REPO, trust_remote_code=True).eval()
tokenizer = AutoTokenizer.from_pretrained(REPO)
with open(hf_hub_download(REPO, "thresholds.json")) as f:
thresholds = json.load(f)["thresholds"]
cut = torch.tensor([thresholds[name] for name in LABELS])
texts, langs = ["You are an absolute idiot.", "Sei un cretino, vattene via."], ["en", "it"]
enc = tokenizer(texts, padding=True, truncation=True, max_length=512, return_tensors="pt")
with torch.no_grad():
probs = model(**enc, lang_ids=torch.tensor([LANGUAGE_IDS[l] for l in langs]))["probabilities"]
for text, row in zip(texts, probs):
print(text, "->", [n for n, p, t in zip(LABELS, row, cut) if p >= t] or ["clean"])
```
`trust_remote_code` runs `modeling_toxic_xlmr.py` from this repo. To avoid it, download the repo
and call `load_model(path)` from that file. `inference_example.py` is a batched version.
**`lang_ids`** is one integer per row (table above). Leave it out and every row is scored as
English, with one warning; the ablation found no accuracy cost to that. Ids outside 0-6 are clamped.
**Thresholds:** do not use 0.5. `thresholds.json` has one per class (0.47 to 0.56), tuned on
validation. There are no per-language thresholds.
## Use
| Use it for | Do not use it for |
|---|---|
| Flagging comments for human review | Deleting, banning or penalising with no human check |
| Ranking a review backlog | Threat assessment or safety escalation |
| Research, and as a baseline | Languages outside the seven, long documents, spam, misinformation, self-harm |
Also expect: quoted abuse and counter-speech score like abuse; reclaimed slurs and in-group
speech are unmeasured; scores are ranks, not calibrated probabilities.
## Results
Held-out test split, 35,658 rows, thresholds tuned on validation only: macro AUC **0.9852**, macro
F1 **0.8814**, exact match **0.8772**.
Against public models, same test rows:
| Model | Size | `toxic` AUC | Comments/s | Latency |
|---|---|---|---|---|
| **Mill** | 565M | **0.9921** | 871 | 11.8 ms |
| Detoxify multilingual | 278M | 0.9692 | 2,424 | 6.2 ms |
| textdetox XLM-R large | 560M | 0.9327 | 896 | 11.0 ms |
| citizenlab mDistilBERT | 135M | 0.8513 | 3,700 | 2.9 ms |
| toxic-bert (English only) | 110M | 0.9797 (Mill 0.9797) | 1,362 | 5.5 ms |
![AUC against public models](images/baseline_auc.png)
![Throughput](images/baseline_speed.png)
Mill is the most accurate and runs at the speed of other XLM-R-large models, about a third of
Detoxify's throughput. One Quadro RTX 6000, fp16, median batch-1 latency. 89% of English test
rows are in Jigsaw data the Detoxify models trained on; removing them keeps every AUC gap. Mill
trained on this test set's distribution; the baselines did not.
[Details](https://github.com/Deeptanshuu/mill-screen/blob/main/experiments/baselines.md).
Per class, tuned thresholds:
| Class | AUC | Threshold | Precision | Recall | F1 |
|---|---|---|---|---|---|
| `toxic` | 0.9921 | 0.4724 | 0.9534 | 0.9750 | 0.9641 |
| `severe_toxic` | 0.9863 | 0.4724 | 0.7139 | 0.8010 | 0.7549 |
| `obscene` | 0.9899 | 0.5276 | 0.9419 | 0.9338 | 0.9378 |
| `threat` | 0.9755 | 0.5276 | 0.8846 | 0.8003 | 0.8403 |
| `insult` | 0.9855 | 0.5643 | 0.9204 | 0.9266 | 0.9235 |
| `identity_hate` | 0.9818 | 0.5643 | 0.8959 | 0.8419 | 0.8680 |
Macro AUC by language: en 0.9902, ru 0.9790, tr 0.9726, es 0.9882, fr 0.9877, it 0.9893,
pt 0.9832. Full values in `metrics.json`.
### Compared with v1 (2025)
| Metric | Mill | v1 (2025) |
|---|---|---|
| Macro AUC | **0.9852** | 0.9147 |
| Macro F1, tuned thresholds | **0.8814** | 0.6036 |
| Weighted F1 | **0.9332** | 0.7732 |
| Exact match | **0.8772** | 0.6194 |
![F1 by class, v1 versus Mill](images/f1_gains_by_class.png)
![Per-language AUC and F1, v1 versus Mill](images/per_language_performance.png)
![Threat probability distributions, v1 versus Mill](images/threat_probability_shift.png)
## On real traffic
The test set is about 50% toxic; real streams are 1-5%. Precision falls with the base rate:
| Label | Recall | FPR | Precision @ 50% | @ 5% | @ 1% |
|---|---|---|---|---|---|
| toxic | 0.975 | 0.0470 | 0.953 | 0.522 | **0.173** |
| insult | 0.927 | 0.0321 | 0.920 | 0.603 | 0.226 |
| obscene | 0.934 | 0.0184 | 0.942 | 0.728 | 0.339 |
| severe_toxic | 0.801 | 0.0156 | 0.714 | 0.730 | 0.342 |
| identity_hate | 0.842 | 0.0055 | 0.896 | 0.890 | 0.608 |
| threat | 0.800 | 0.0023 | 0.885 | 0.948 | **0.779** |
At 1% toxic, the `toxic` head flags about six comments per real one. Tune thresholds on your own
traffic if precision matters.
## Limitations
- Language conditioning is a measured null: +0.0003 macro AUC, 95% CI [-0.0007, +0.0012].
- The step that made the corpus multilingual is not in the source repo and cannot be audited.
- Rare classes are partly synthetic (Mistral-7B-Instruct-v0.3), labelled by prompt, not by people.
- Near-duplicates are 3.8% of English validation rows, so held-out numbers are slightly optimistic.
- `severe_toxic` is a subset of `toxic`; the independent sigmoids do not know that.
- Weights are a pickled state dict, not safetensors.
## Files
| File | Contents |
|---|---|
| `modeling_toxic_xlmr.py` | Model code, for `trust_remote_code` or `load_model()` |
| `config.json` | Architecture config with `auto_map` |
| `pytorch_model.bin` | Weights, about 2.2 GB |
| `thresholds.json` | Per-class thresholds |
| `metrics.json` | Evaluation results |
| `training_config.json` | Training configuration |
| `inference_example.py` | Batched prediction example |
| tokenizer files | Stock `xlm-roberta-large` tokenizer |
## License
Apache 2.0 for the weights and code here. The base model is MIT. The training data (Jigsaw,
Wikipedia talk pages, Mistral-generated text) keeps its own terms.
## Citation
```bibtex
@misc{mill_toxicity,
author = {Deeptanshu Lal},
title = {Mill: multilingual multi-label toxicity classification},
year = {2026},
howpublished = {\url{https://huggingface.co/Deeptanshuu/mill-screen}}
}
```