Qwen3-4B fine-tuned with QLoRA on a
2026-vintage 3-generator reproduction of the EditLens dataset, with the LoRA adapter merged into the base in bf16. Successor to
editlens-qwen3-4b-merged-v2: adds Gemini-3 Flash as a third generator (alongside Claude Sonnet 4.6 and GPT-5.3) by replacing 1/3 of the existing v2 ai_edited/ai_generated rows with Gemini-generated equivalents, preserving the 1:1:1 human:edited:generated ratio.
Trained for the EditLens task (
arXiv:2510.03154): classify text by
how much AI editing it has received. Predicts a continuous score in [0, 1] from a 4-bucket softmax (
bucket_pred ∈ {0, 1, 2, 3} mapped to score = bucket / 3).
For ensembling: v1 + v3 covers most generator vintages from 2022 → 2026.
1import torch
2import torch.nn as nn
3from safetensors import safe_open
4from transformers import AutoModelForSequenceClassification, AutoTokenizer
5
6class NormedLinear(nn.Module):
7 def __init__(self, hidden_size, num_labels, dtype=torch.bfloat16):
8 super().__init__()
9 self.norm = nn.LayerNorm(hidden_size, dtype=dtype)
10 self.linear = nn.Linear(hidden_size, num_labels, bias=False, dtype=dtype)
11 def forward(self, x):
12 return self.linear(self.norm(x))
13
14MODEL = "DarrenJiaImbue/editlens-qwen3-4b-merged-v3"
15tok = AutoTokenizer.from_pretrained(MODEL)
16if tok.pad_token is None:
17 tok.pad_token = tok.eos_token
18 tok.padding_side = "left"
19
20model = AutoModelForSequenceClassification.from_pretrained(MODEL, dtype=torch.bfloat16).to("cuda")
21n = model.config.num_labels
22model.score = NormedLinear(model.config.hidden_size, n).to("cuda", dtype=torch.bfloat16)
23
24from huggingface_hub import hf_hub_download
25sf = hf_hub_download(MODEL, "model.safetensors")
26with safe_open(sf, framework="pt") as f:
27 model.score.norm.weight.data.copy_(f.get_tensor("score.norm.weight"))
28 model.score.norm.bias.data.copy_(f.get_tensor("score.norm.bias"))
29 model.score.linear.weight.data.copy_(f.get_tensor("score.linear.weight"))
30model.config.pad_token_id = tok.pad_token_id
31model.eval()
32
33text = "The original text..."
34enc = tok(text, return_tensors="pt", truncation=True, max_length=1024).to("cuda")
35with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
36 logits = model(**enc).logits
37probs = logits.float().softmax(-1).cpu().numpy()[0]
38bucket = int(probs.argmax())
39score = float(probs @ [0, 1, 2, 3]) / 3
40print(f"bucket={bucket} score={score:.3f}")
Threshold calibrated on each model's own val.csv. Numbers are ternary accuracy (4-class output collapsed to 3-class human/edited/AI-gen).
The v2 model's notable weakness was Gemini-2.5 ai_generated detection, where it scored 0.852 vs v1's 0.951. v3's 3-way generator mix closes that gap and then some:
CC BY-NC-SA 4.0 (matches the original EditLens release).
1@misc{thai2025editlensquantifyingextentai,
2 title={EditLens: Quantifying the Extent of AI Editing in Text},
3 author={Katherine Thai and Bradley Emi and Elyas Masrour and Mohit Iyyer},
4 year={2025},
5 eprint={2510.03154},
6 archivePrefix={arXiv},
7 primaryClass={cs.CL},
8}