File size: 5,985 Bytes
7fb84fb
9bec9b1
7fb84fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1e37e3c
7fb84fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/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())