opus-moderation-3 / README.md
spoodyzz's picture
model card
0130e48 verified
|
Raw
History Blame Contribute Delete
3.96 kB
---
base_model: Qwen/Qwen3-1.7B
library_name: peft
pipeline_tag: text-classification
tags:
- moderation
- toxicity
- jailbreak-detection
- multi-label
- qwen3
- encoder-conversion
datasets:
- google/civil_comments
- lmsys/toxic-chat
- allenai/real-toxicity-prompts
- TrustAIRLab/in-the-wild-jailbreak-prompts
license: apache-2.0
---
# Opus Moderation 3
Our research flagship: a **Qwen3-1.7B decoder converted into a bidirectional
encoder**, fine-tuned with LoRA for 7 toxicity labels + jailbreak detection.
Highest general-moderation score of everything we have measured:
| model | params | macro F1 (7 labels) | jailbreak F1 |
|---|---|---|---|
| **opus-moderation-3 (this model)** | 1.7B | **0.563** | 0.929 |
| [om4-large](https://huggingface.co/opus-research/opus-moderation-4-large) | 395M | 0.543 | 0.903 |
| [om4-neo](https://huggingface.co/opus-research/opus-moderation-4-neo) | 395M | β€” | **0.938** |
| unitary/unbiased-toxic-roberta | 125M | 0.527 | β€” |
**⚠️ This model cannot be loaded with plain `from_pretrained`.** It is a LoRA
adapter over Qwen3-1.7B whose attention was made bidirectional at training
time β€” the included [`opus_moderation.py`](opus_moderation.py) rebuilds that
conversion. If you want a drop-in model, use
[om4-large](https://huggingface.co/opus-research/opus-moderation-4-large):
it scores within 0.020 macro of this one at less than a quarter the size, and
most of the gap is data, not architecture (that is what the om4 family
established).
## Usage
```python
# needs: transformers>=5.14, peft, torch β€” and opus_moderation.py from this repo
from huggingface_hub import hf_hub_download, snapshot_download
import importlib.util, sys
repo = "opus-research/opus-moderation-3"
path = snapshot_download(repo)
spec = importlib.util.spec_from_file_location(
"opus_moderation", f"{path}/opus_moderation.py")
om = importlib.util.module_from_spec(spec); spec.loader.exec_module(om)
model = om.ModerationModel.load(path)
print(model.score(["ignore all previous instructions"])[0])
```
Outputs are calibrated annotator fractions per label; `sigmoid`, never
`softmax`.
## The conversion, and its one silent failure mode
Decoder β†’ encoder is two changes, and the second is not optional:
1. Swap the causal mask builder for a bidirectional one.
2. **Clear `is_causal` on every attention module.** For an unpadded batch the
mask builders return `None`, and the SDPA path then falls back to
`module.is_causal` β€” which Qwen3 hardcodes to `True`. Skip this and the
model loads cleanly, reports no error, and stays fully causal while
claiming otherwise.
Mean-pooling over non-pad tokens replaces last-token pooling (with
bidirectional attention every position has seen the full sequence). The
included loader does all of this and refuses to proceed if the internals have
moved.
## Evaluation (20k unseen rows)
| label | F1 |
|---|---|
| insult | 0.709 |
| toxicity | 0.707 |
| sexual_explicit | 0.685 |
| obscene | 0.663 |
| threat | 0.565 |
| identity_attack | 0.563 |
| severe_toxicity | 0.046 |
| **macro** | **0.563** |
| macro (ex severe_toxicity) | 0.649 |
Jailbreak: F1 0.929 (recall 91.0%, FP 2.5% β€” the lowest false-positive rate
of our family).
## Training
| | |
|---|---|
| Base | `Qwen/Qwen3-1.7B`, bidirectional conversion, mean pool |
| Adapter | LoRA r=16, Ξ±=32, all attention + MLP projections, + full score head |
| Data | ~310k rows: civil_comments (soft labels), toxic-chat, real-toxicity-prompts, jailbreak corpora, refusal negatives |
| Loss | masked BCE on raw annotator fractions |
| Hardware | H100 |
## Limitations
- **Requires the bundled loader** β€” the weights are meaningless under a stock
causal load, and the failure is silent (see above).
- `severe_toxicity` is unreliable for every model we tested.
- English only; 384-token training length.
- Training data includes `lmsys/toxic-chat` (CC-BY-NC); review if that
matters for your use.