utfunderscore commited on
Commit
9c841b0
·
verified ·
1 Parent(s): 1116510

Upload Themis DeBERTa moderation model

Browse files
README.md ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - en
5
+ library_name: transformers
6
+ pipeline_tag: text-classification
7
+ base_model: microsoft/deberta-v3-base
8
+ tags:
9
+ - text-classification
10
+ - content-moderation
11
+ - knowledge-distillation
12
+ - synthetic-data
13
+ - minecraft
14
+ - deberta-v3
15
+ - onnx
16
+ datasets:
17
+ - declip/Minecraft-Server-Chat
18
+ ---
19
+
20
+ # Themis: DeBERTa-v3 Minecraft chat moderation
21
+
22
+ Themis is a small, fast moderation model for Minecraft chat. It is
23
+ [`microsoft/deberta-v3-base`](https://huggingface.co/microsoft/deberta-v3-base),
24
+ fine-tuned on **synthetic labels produced by a decision model**, in this case
25
+ Perplexity's
26
+ [`perplexity-ai/pplx-decider-v1.1-27b`](https://huggingface.co/perplexity-ai/pplx-decider-v1.1-27b).
27
+
28
+ The 27B decision model is accurate but expensive to run on every chat message.
29
+ We used it once, offline, to label 100k real chat messages, then distilled its
30
+ judgements into a 184M-parameter student. The student runs on a CPU at roughly
31
+ 87 messages/second (ONNX, batch size 1, Ryzen 5 7600X).
32
+
33
+ For each message, the model returns two independent predictions:
34
+
35
+ | Task | Labels | Meaning |
36
+ | --- | --- | --- |
37
+ | `category` | `none`, `profanity`, `insult`, `slur`, `sexual`, `threat` | Which kind of offensive content the message contains, if any. |
38
+ | `action` | `allow`, `block_and_review` | Whether to let the message through or hold it for a human moderator, given a game setting with combat, banter and fictional violence. |
39
+
40
+ The action is predicted **independently** of the category. A message can be
41
+ `profanity` and still be `allow`. For example, "gg you absolute noob, I'll kill
42
+ you next round" is ordinary gameplay trash talk.
43
+
44
+ ## How the training data was made
45
+
46
+ 1. **Collect chat.** Public Minecraft chat from the
47
+ [Minecraft Server Chat](https://huggingface.co/datasets/declip/Minecraft-Server-Chat)
48
+ dataset and the 2025 [2b2t](https://2b2t.vc) chat archive, deduplicated.
49
+ 2. **Sample for balance.** Most chat is harmless, so every message was
50
+ pre-scored with
51
+ [`KoalaAI/Text-Moderation`](https://huggingface.co/KoalaAI/Text-Moderation).
52
+ A 100k sample was drawn using those scores to over-represent rare harmful
53
+ content (about 50% "OK", the rest spread over hate, violence, sexual,
54
+ self-harm and harassment). KoalaAI's labels are used only for sampling, not
55
+ for training.
56
+ 3. **Label pass 1: category.** `pplx-decider-v1.1-27b` was asked "Which single
57
+ type of offensive content is this message, if any?", with a short criterion
58
+ per label. We kept its full calibrated probability distribution, not just the
59
+ top answer.
60
+ 4. **Label pass 2: action.** The decider was run again on each message,
61
+ told the pass 1 category and the game context (combat, friendly insults,
62
+ fictional violence). It was asked whether to `allow` the message or
63
+ `block_and_review` it, separating banter and in-game threats from targeted
64
+ abuse, hate speech and credible real-world threats. Again we kept the full
65
+ probability distribution.
66
+ 5. **Distil.** DeBERTa-v3-base was trained on the message text alone, with no
67
+ category hint. It has one 8-logit head: 6 category logits followed by 2
68
+ action logits. The loss is the sum of two soft-target cross-entropies, one
69
+ per task, against the teacher's probabilities. This teaches the student how
70
+ confident the teacher was, not only which answer it picked.
71
+
72
+ Splits are grouped by normalised message text, so near-duplicates never cross
73
+ splits. They are also stratified by teacher category: 79,998 train / 10,002
74
+ validation / 9,999 test.
75
+
76
+ ### Training setup
77
+
78
+ | Setting | Value |
79
+ | --- | --- |
80
+ | Base model | `microsoft/deberta-v3-base` @ `8ccc9b6f` |
81
+ | Teacher | `perplexity-ai/pplx-decider-v1.1-27b` @ `6195aa55`, unquantised BF16 |
82
+ | Max sequence length | 256 tokens |
83
+ | Epochs | 3 (best epoch chosen by validation loss) |
84
+ | Optimiser | AdamW, lr 2e-5, weight decay 0.01, linear schedule, 10% warmup |
85
+ | Batch size | 16 |
86
+ | Precision | FP32 |
87
+ | Hardware | 1× H100 (about 15 minutes) |
88
+
89
+ ## Evaluation
90
+
91
+ These numbers measure **agreement with the teacher** on the held-out test
92
+ split. The "ground truth" is the argmax of the decider's probabilities.
93
+ They are not measured against human annotations.
94
+
95
+ | Test metric | Category | Action |
96
+ | --- | --- | --- |
97
+ | Agreement with teacher | 93.6% | 95.2% |
98
+ | Macro F1 | 0.840 | 0.920 |
99
+ | KL(teacher ‖ student) | 0.063 | 0.037 |
100
+
101
+ Per-class F1 (test):
102
+
103
+ | none | profanity | insult | slur | sexual | threat | | allow | block_and_review |
104
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
105
+ | 0.968 | 0.908 | 0.720 | 0.813 | 0.891 | 0.742 | | 0.971 | 0.869 |
106
+
107
+ ## Usage
108
+
109
+ > [!IMPORTANT]
110
+ > The model outputs 8 logits. Apply softmax **separately** to `logits[:, :6]`
111
+ > (category) and `logits[:, 6:]` (action). A single softmax over all 8 logits
112
+ > gives meaningless probabilities. Load the tokenizer with `use_fast=False` to
113
+ > match training.
114
+
115
+ ### Transformers (PyTorch)
116
+
117
+ ```sh
118
+ pip install torch transformers sentencepiece protobuf
119
+ ```
120
+
121
+ ```python
122
+ import torch
123
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
124
+
125
+ REPO = "utfunderscore/themis-moderation"
126
+ CATEGORIES = ["none", "profanity", "insult", "slur", "sexual", "threat"]
127
+ ACTIONS = ["allow", "block_and_review"]
128
+
129
+ tokenizer = AutoTokenizer.from_pretrained(REPO, use_fast=False)
130
+ model = AutoModelForSequenceClassification.from_pretrained(REPO).eval()
131
+
132
+
133
+ def moderate(texts: list[str]) -> list[dict]:
134
+ inputs = tokenizer(texts, padding=True, truncation=True, max_length=256,
135
+ return_tensors="pt")
136
+ with torch.inference_mode():
137
+ logits = model(**inputs).logits
138
+ categories = logits[:, :6].softmax(dim=-1) # independent softmax per task
139
+ actions = logits[:, 6:].softmax(dim=-1)
140
+ return [
141
+ {
142
+ "category": CATEGORIES[c.argmax()],
143
+ "action": ACTIONS[a.argmax()],
144
+ "category_probabilities": dict(zip(CATEGORIES, c.tolist())),
145
+ "action_probabilities": dict(zip(ACTIONS, a.tolist())),
146
+ }
147
+ for c, a in zip(categories, actions)
148
+ ]
149
+
150
+
151
+ for result in moderate(["gg wp everyone", "anyone want to trade diamonds?"]):
152
+ print(result["category"], result["action"], result["action_probabilities"])
153
+ ```
154
+
155
+ To trade recall for precision, use your own threshold on
156
+ `action_probabilities["block_and_review"]` instead of the argmax.
157
+
158
+ ### ONNX Runtime (CPU, no PyTorch)
159
+
160
+ An FP32 ONNX export is in `onnx/`. It takes `input_ids` and `attention_mask`
161
+ (int64, `[batch, sequence]`) and returns `logits` (float32, `[batch, 8]`).
162
+ Tokenization happens outside the graph.
163
+
164
+ ```sh
165
+ pip install onnxruntime transformers sentencepiece protobuf huggingface_hub
166
+ ```
167
+
168
+ ```python
169
+ import numpy as np
170
+ import onnxruntime as ort
171
+ from huggingface_hub import hf_hub_download
172
+ from transformers import AutoTokenizer
173
+
174
+ REPO = "utfunderscore/themis-moderation"
175
+ CATEGORIES = ["none", "profanity", "insult", "slur", "sexual", "threat"]
176
+ ACTIONS = ["allow", "block_and_review"]
177
+
178
+ tokenizer = AutoTokenizer.from_pretrained(REPO, use_fast=False)
179
+ session = ort.InferenceSession(hf_hub_download(REPO, "onnx/model.onnx"),
180
+ providers=["CPUExecutionProvider"])
181
+
182
+
183
+ def softmax(x: np.ndarray) -> np.ndarray:
184
+ e = np.exp(x - x.max(axis=-1, keepdims=True))
185
+ return e / e.sum(axis=-1, keepdims=True)
186
+
187
+
188
+ texts = ["gg wp everyone", "anyone want to trade diamonds?"]
189
+ encoded = tokenizer(texts, padding=True, truncation=True, max_length=256,
190
+ return_tensors="np", return_token_type_ids=False)
191
+ (logits,) = session.run(["logits"], {
192
+ "input_ids": encoded["input_ids"].astype(np.int64),
193
+ "attention_mask": encoded["attention_mask"].astype(np.int64),
194
+ })
195
+ for text, c, a in zip(texts, softmax(logits[:, :6]), softmax(logits[:, 6:])):
196
+ print(text, CATEGORIES[c.argmax()], ACTIONS[a.argmax()], float(a[1]))
197
+ ```
198
+
199
+ `onnx/inference_config.json` records the logit groups, label order, token
200
+ limit and tokenizer settings. Use it to port the model to other runtimes.
201
+ The [Themis Java library](https://github.com/utfunderscore/themis/tree/main/java)
202
+ runs this bundle from Java 21 with ONNX Runtime.
203
+
204
+ ## Limitations
205
+
206
+ - **Teacher-bounded.** The student learns the decider's judgement, including
207
+ its mistakes and biases. The metrics above show fidelity to the teacher, not
208
+ real-world accuracy.
209
+ - **Domain.** Trained on Minecraft server chat: short, informal, mostly
210
+ English, full of game slang. Expect weaker results on other platforms,
211
+ long-form text or other languages.
212
+ - **Weaker classes.** `insult` and `threat` are the hardest categories
213
+ (F1 ≈ 0.72–0.74 against the teacher). Here, banter and real harassment are
214
+ hard to tell apart.
215
+ - **No context.** Each message is classified alone, with no conversation
216
+ history, sender or server rules.
217
+ - **Human in the loop.** `block_and_review` is meant to send a message to a
218
+ human moderator, not to punish anyone automatically.
219
+
220
+ ## Source
221
+
222
+ Training, labelling and export code: <https://github.com/utfunderscore/themis>.
added_tokens.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "[MASK]": 128000
3
+ }
config.json ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DebertaV2ForSequenceClassification"
4
+ ],
5
+ "attention_probs_dropout_prob": 0.1,
6
+ "bos_token_id": 1,
7
+ "dtype": "float32",
8
+ "eos_token_id": 2,
9
+ "hidden_act": "gelu",
10
+ "hidden_dropout_prob": 0.1,
11
+ "hidden_size": 768,
12
+ "id2label": {
13
+ "0": "none",
14
+ "1": "profanity",
15
+ "2": "insult",
16
+ "3": "slur",
17
+ "4": "sexual",
18
+ "5": "threat",
19
+ "6": "allow",
20
+ "7": "block_and_review"
21
+ },
22
+ "initializer_range": 0.02,
23
+ "intermediate_size": 3072,
24
+ "label2id": {
25
+ "allow": 6,
26
+ "block_and_review": 7,
27
+ "insult": 2,
28
+ "none": 0,
29
+ "profanity": 1,
30
+ "sexual": 4,
31
+ "slur": 3,
32
+ "threat": 5
33
+ },
34
+ "layer_norm_eps": 1e-07,
35
+ "legacy": true,
36
+ "max_position_embeddings": 512,
37
+ "max_relative_positions": -1,
38
+ "model_type": "deberta-v2",
39
+ "norm_rel_ebd": "layer_norm",
40
+ "num_attention_heads": 12,
41
+ "num_hidden_layers": 12,
42
+ "pad_token_id": 0,
43
+ "pooler_dropout": 0,
44
+ "pooler_hidden_act": "gelu",
45
+ "pooler_hidden_size": 768,
46
+ "pos_att_type": [
47
+ "p2c",
48
+ "c2p"
49
+ ],
50
+ "position_biased_input": false,
51
+ "position_buckets": 256,
52
+ "relative_attention": true,
53
+ "share_att_key": true,
54
+ "task_label_groups": {
55
+ "action": [
56
+ "allow",
57
+ "block_and_review"
58
+ ],
59
+ "category": [
60
+ "none",
61
+ "profanity",
62
+ "insult",
63
+ "slur",
64
+ "sexual",
65
+ "threat"
66
+ ]
67
+ },
68
+ "transformers_version": "4.57.1",
69
+ "type_vocab_size": 0,
70
+ "vocab_size": 128100
71
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b2f7a9c4c5527339a551a49ff632350dc71a5bb48e932430fe58b3e6d2b7fb57
3
+ size 737737736
onnx/inference_config.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "format_version": 1,
3
+ "max_length": 256,
4
+ "inputs": {
5
+ "input_ids": "int64[batch,sequence]",
6
+ "attention_mask": "int64[batch,sequence]"
7
+ },
8
+ "output": "logits",
9
+ "output_groups": {
10
+ "category": {
11
+ "start": 0,
12
+ "end": 6,
13
+ "labels": [
14
+ "none",
15
+ "profanity",
16
+ "insult",
17
+ "slur",
18
+ "sexual",
19
+ "threat"
20
+ ]
21
+ },
22
+ "action": {
23
+ "start": 6,
24
+ "end": 8,
25
+ "labels": [
26
+ "allow",
27
+ "block_and_review"
28
+ ]
29
+ }
30
+ },
31
+ "postprocessing": "independent_softmax_per_group",
32
+ "tokenizer": {
33
+ "use_fast": false,
34
+ "padding_side": "right",
35
+ "truncation_side": "right",
36
+ "pad_token_id": 0,
37
+ "cls_token_id": 1,
38
+ "sep_token_id": 2
39
+ }
40
+ }
onnx/model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:770c4d7902f6d736b869741c7b485706f8b761106b919caae6750517690b20d3
3
+ size 738695089
special_tokens_map.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "[CLS]",
3
+ "cls_token": "[CLS]",
4
+ "eos_token": "[SEP]",
5
+ "mask_token": "[MASK]",
6
+ "pad_token": "[PAD]",
7
+ "sep_token": "[SEP]",
8
+ "unk_token": {
9
+ "content": "[UNK]",
10
+ "lstrip": false,
11
+ "normalized": true,
12
+ "rstrip": false,
13
+ "single_word": false
14
+ }
15
+ }
spm.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c679fbf93643d19aab7ee10c0b99e460bdbc02fedf34b92b05af343b4af586fd
3
+ size 2464616
tokenizer_config.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "[PAD]",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "[CLS]",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "[SEP]",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "3": {
28
+ "content": "[UNK]",
29
+ "lstrip": false,
30
+ "normalized": true,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "128000": {
36
+ "content": "[MASK]",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ }
43
+ },
44
+ "bos_token": "[CLS]",
45
+ "clean_up_tokenization_spaces": false,
46
+ "cls_token": "[CLS]",
47
+ "do_lower_case": false,
48
+ "eos_token": "[SEP]",
49
+ "extra_special_tokens": {},
50
+ "mask_token": "[MASK]",
51
+ "model_max_length": 1000000000000000019884624838656,
52
+ "pad_token": "[PAD]",
53
+ "sep_token": "[SEP]",
54
+ "sp_model_kwargs": {},
55
+ "split_by_punct": false,
56
+ "tokenizer_class": "DebertaV2Tokenizer",
57
+ "unk_token": "[UNK]",
58
+ "vocab_type": "spm"
59
+ }