| """LLM-based humanizer attack: rewrite AI text with an instruction-tuned LLM prompted to sound like |
| a casual human, preserving meaning. A stronger, more natural evasion than seq2seq paraphrasers, |
| closer to how a real user would obfuscate AI text. Default Qwen2.5-1.5B-Instruct (Colab-friendly).""" |
| from __future__ import annotations |
|
|
|
|
| class LLMHumanizer: |
| name = "llm-humanizer" |
|
|
| def __init__(self, model_name: str = "Qwen/Qwen2.5-1.5B-Instruct", |
| device: str | None = None, max_new_tokens: int = 400): |
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
| self.torch = torch |
| self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") |
| self.max_new_tokens = max_new_tokens |
| self.tok = AutoTokenizer.from_pretrained(model_name) |
| dtype = torch.float16 if self.device == "cuda" else torch.float32 |
| self.model = AutoModelForCausalLM.from_pretrained( |
| model_name, torch_dtype=dtype).to(self.device).eval() |
|
|
| def humanize(self, text: str, temperature: float = 0.9) -> str: |
| msg = [ |
| {"role": "system", "content": "You rewrite text so it reads like a casual human wrote " |
| "it, preserving the original meaning and approximate length. Output only the rewrite."}, |
| {"role": "user", "content": f"Rewrite this:\n\n{text}"}, |
| ] |
| prompt = self.tok.apply_chat_template(msg, tokenize=False, add_generation_prompt=True) |
| ids = self.tok(prompt, return_tensors="pt", truncation=True, max_length=2048).to(self.device) |
| with self.torch.no_grad(): |
| out = self.model.generate(**ids, max_new_tokens=self.max_new_tokens, |
| do_sample=True, temperature=temperature, top_p=0.95) |
| gen = out[0][ids["input_ids"].shape[1]:] |
| return self.tok.decode(gen, skip_special_tokens=True).strip() |
|
|
| def humanize_many(self, texts: list[str]) -> list[str]: |
| out = [] |
| for i, t in enumerate(texts): |
| out.append(self.humanize(t)) |
| if (i + 1) % 25 == 0: |
| print(f" humanized {i+1}/{len(texts)}") |
| return out |
|
|