Text Classification
PEFT
Safetensors
qwen3
moderation
toxicity
jailbreak-detection
multi-label
encoder-conversion
Instructions to use opus-research/opus-moderation-3 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use opus-research/opus-moderation-3 with PEFT:
from peft import PeftModel from transformers import AutoModelForSequenceClassification base_model = AutoModelForSequenceClassification.from_pretrained("Qwen/Qwen3-1.7B") model = PeftModel.from_pretrained(base_model, "opus-research/opus-moderation-3") - Notebooks
- Google Colab
- Kaggle
| 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. | |