QLoRA toxicity annotator, Qwen3-1.7B

A LoRA adapter that turns Qwen3-1.7B into a yes/no toxicity annotator for news-site reader comments.

It exists as the worked example in an undergraduate teaching notebook about using open-weight LLMs as text annotators. The point it makes is that a 1.7B model on one consumer GPU, given 1,000 labelled examples and about half an hour, can be brought from barely-better-than-chance agreement with a human annotation panel to agreement well above what a single human rater manages.

Twenty minutes of fine-tuning moves Cohen's kappa against the human majority label from 0.150 to 0.700.

  • Base model: unsloth/Qwen3-1.7B-unsloth-bnb-4bit, a 4-bit NF4 build
  • Adapter: 78 MB, 17.4M trainable parameters (1.66% of the model)
  • Training data: 1,000 Civil Comments comments, balanced 50/50
  • Hardware: one RTX 2070 SUPER (8 GB), 31 minutes for 200 steps, 4.4 GB peak VRAM
  • Licence: Apache 2.0, inherited from Qwen3

The prompt is part of the model

This adapter was trained against one exact prompt and one exact reply format, with thinking disabled. It answers LABEL: yes or LABEL: no followed by a one-sentence reason. Ask it with a different rubric and none of the numbers below apply.

You are rating reader comments from a news website for toxicity.

The people who labelled these comments were asked: how toxic is this comment? They answered on a four-point scale.

  Very Toxic     a very hateful, aggressive or disrespectful comment, or one very likely to make someone leave the discussion.
  Toxic          a rude, disrespectful or unreasonable comment, or one somewhat likely to make someone leave the discussion.
  Hard to Say    you cannot tell.
  Not Toxic      none of the above.

Answer yes if the comment is Toxic or Very Toxic. Answer no otherwise.

Comment:
"""{comment}"""

Reply in exactly this format and nothing else:
LABEL: yes
REASON: <one short sentence>

The literal LABEL: yes in that last block is a formatting example, not an instruction to answer yes. It is what the model saw during training, so leave it alone.

Usage

import torch
from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

BASE = "unsloth/Qwen3-1.7B-unsloth-bnb-4bit"
RUBRIC = """..."""    # the prompt above, with {comment} left as a placeholder

tok = AutoTokenizer.from_pretrained(BASE)
tok.padding_side = "left"
tok.pad_token = tok.pad_token or tok.eos_token

# The checkpoint carries its own quantization config asking for bfloat16, and
# transformers honours that over anything passed in. Pre-Ampere cards (Turing and
# older) only emulate bf16, so overwrite it before loading.
cfg = AutoConfig.from_pretrained(BASE)
if getattr(cfg, "quantization_config", None):
    cfg.quantization_config["bnb_4bit_compute_dtype"] = "float16"

model = AutoModelForCausalLM.from_pretrained(
    BASE, config=cfg, dtype=torch.float16, device_map={"": 0})
model = PeftModel.from_pretrained(model, "Alexr951/qlora-toxicity-qwen3-1.7b")
model.eval()
model.generation_config.max_length = None


def label(comment):
    text = tok.apply_chat_template(
        [{"role": "user", "content": RUBRIC.format(comment=comment)}],
        tokenize=False, add_generation_prompt=True, enable_thinking=False)
    enc = tok(text, return_tensors="pt").to("cuda")
    with torch.inference_mode():
        out = model.generate(**enc, max_new_tokens=96, do_sample=False,
                             pad_token_id=tok.pad_token_id)
    return tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)


print(label("This is the stupidest thing I have ever read."))

enable_thinking=False matters. Qwen3 is a hybrid reasoning model, and with thinking left on it emits a <think> block before answering, which is slower and is not what the adapter was trained on.

The unsloth package is not required. The base model id says unsloth because Unsloth published the 4-bit build, but this loads through transformers and peft alone.

Results

Cohen's kappa against the human majority label, on held-out comments. Both evaluation sets are balanced 50/50, so accuracy is directly readable and kappa is not being flattered by a skewed base rate.

Civil Comments, 200 held-out comments. Split by article, so no comment from a training article appears here.

kappa accuracy precision recall F1
Base Qwen3-1.7B 0.150 0.575 0.549 0.840 0.664
With this adapter 0.700 0.850 0.880 0.810 0.844
One human rater vs the panel majority 0.420 0.695

Wikipedia Detox, 800 comments. A different corpus, platform and decade, never seen during training. This is the transfer check.

kappa accuracy precision recall F1
Base Qwen3-1.7B 0.515 0.757 0.732 0.812 0.770
With this adapter 0.742 0.871 0.971 0.765 0.856
One human rater vs the panel majority 0.447 0.747

Three things worth reading off these tables.

The gain survives the move to a corpus the adapter never saw, which is the result that matters: it learned the annotation task rather than the idiom of one comment section.

The base model's failure mode is over-calling toxicity. High recall, poor precision, and on Civil Comments it agrees with the panel barely better than chance. Fine-tuning mostly buys precision.

The adapter scores above the single-rater ceiling, and that needs care in interpretation. It does not mean the model judges toxicity better than a person. The majority label is a smoothed target that averages out individual rater noise, and the model is trained to predict exactly that, while a lone rater is one noisy draw. The honest reading is that the adapter has learned the panel's central tendency, which is a lower bar than judging toxicity well.

Training also makes the model roughly 4.5x faster at inference, about 120 comments a minute before against 550 after on the same card, because it learned to stop once it has answered instead of continuing into a paragraph.

Training

1,000 Civil Comments comments sampled 50/50 toxic and non-toxic. The natural toxic rate is about 11%, which at this sample size leaves too few positives to learn from.

Comments were split by article, not by comment, so no article contributes to both training and evaluation. Splitting by comment would let the model see other comments from the same thread and inflate the score.

A comment counts as toxic when at least half its human panel said so. The Civil Comments label is a share of the panel, so a comment rated by four people arrives as 0, 0.25, 0.5, 0.75 or 1.

LoRA rank / alpha / dropout 16 / 32 / 0.05
Target modules q, k, v, o, gate, up, down projections
Trainable parameters 17,432,576 (1.66%)
Steps 200
Batch size 2, gradient accumulation 8 (effective 16)
Learning rate 2e-4, cosine schedule, 10 warmup steps
Precision fp16
Optimiser paged_adamw_8bit
Max sequence length 512 tokens
Gradient checkpointing enabled
Seed 20260810
Final loss 0.212, from 1.346 at step 20
Peak VRAM 4,420 MiB of 8,191
Wall clock 31.1 minutes, 9.3 s/step

Loss on the prompt is masked out, so only the LABEL: yes / LABEL: no answer tokens contribute. Where a comment overflows the 512-token cap the comment is truncated and never the answer, because cutting the tail off the prompt removes the "reply in this format" instruction and the model starts rambling.

Limitations and bias

This is a teaching artefact, not a moderation system. It was trained on 1,000 comments to demonstrate a method. Do not deploy it to act on real people's speech.

  • "Toxic" here means what one particular crowd panel meant by it. The label is a majority vote of Civil Comments raters against the four-point rubric above. It is not a neutral or universal definition, and reasonable people disagree with it comment by comment. The panels do not agree unanimously among themselves, as the ceiling rows show.
  • Known identity-term bias. Civil Comments is the corpus the Jigsaw unintended-bias work was built around. Models trained on it tend to over-flag non-toxic comments that merely mention identity groups, because those terms co-occur with abuse in the training text. This adapter was not debiased and should be assumed to carry that behaviour.
  • English only, and specifically English-language news and Wikipedia comment sections from roughly 2004 to 2017.
  • Binary output collapses "Very Toxic", "Toxic" and "Hard to Say" into a single decision, discarding the uncertain middle.
  • Short text. Comments are truncated to 400 tokens at inference and 512 during training.
  • Not adversarially robust. There was no testing against deliberate evasion, obfuscated slurs, or prompt injection inside the comment text.

Data licensing

Civil Comments and Wikipedia Detox are both released CC0, text included. Qwen3-1.7B is Apache 2.0, and this adapter is a derivative of it under the same licence.

Provenance

Built for a teaching notebook in prAxIs, a UBC open educational resources project on critical AI literacies for arts students. The notebook, the data-building and training scripts, and the cached model outputs behind every number above live in the project repository under project/docs/llm_annotations/.

Downloads last month
30
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for Alexr951/qlora-toxicity-qwen3-1.7b

Finetuned
Qwen/Qwen3-1.7B
Adapter
(21)
this model

Dataset used to train Alexr951/qlora-toxicity-qwen3-1.7b