from __future__ import annotations import argparse import json from pathlib import Path import torch from peft import LoraConfig, TaskType, get_peft_model from torch.utils.data import Dataset from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments class ChatJsonlDataset(Dataset): def __init__(self, path: Path, tokenizer, max_length: int) -> None: self.rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] self.tokenizer = tokenizer self.max_length = max_length def __len__(self) -> int: return len(self.rows) def __getitem__(self, index: int) -> dict[str, torch.Tensor]: row = self.rows[index] messages = row["messages"] prompt_messages = messages[:-1] assistant_message = messages[-1] prompt = self.tokenizer.apply_chat_template( prompt_messages, tokenize=False, add_generation_prompt=True, ) full_text = prompt + assistant_message["content"] + self.tokenizer.eos_token full = self.tokenizer( full_text, truncation=True, max_length=self.max_length, padding=False, return_tensors="pt", ) prompt_tokens = self.tokenizer( prompt, truncation=True, max_length=self.max_length, padding=False, return_tensors="pt", ) input_ids = full["input_ids"][0] attention_mask = full["attention_mask"][0] labels = input_ids.clone() labels[: prompt_tokens["input_ids"].shape[-1]] = -100 return { "input_ids": input_ids, "attention_mask": attention_mask, "labels": labels, } class DataCollator: def __init__(self, tokenizer) -> None: self.tokenizer = tokenizer def __call__(self, features: list[dict[str, torch.Tensor]]) -> dict[str, torch.Tensor]: max_len = max(feature["input_ids"].shape[0] for feature in features) batch = {"input_ids": [], "attention_mask": [], "labels": []} for feature in features: pad_len = max_len - feature["input_ids"].shape[0] batch["input_ids"].append( torch.nn.functional.pad(feature["input_ids"], (0, pad_len), value=self.tokenizer.pad_token_id) ) batch["attention_mask"].append(torch.nn.functional.pad(feature["attention_mask"], (0, pad_len), value=0)) batch["labels"].append(torch.nn.functional.pad(feature["labels"], (0, pad_len), value=-100)) return {key: torch.stack(value) for key, value in batch.items()} def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="LoRA fine-tune MiniCPM for Jawbreaker JSON scam analysis.") parser.add_argument("--model-id", default="openbmb/MiniCPM4.1-8B") parser.add_argument("--train-file", type=Path, default=Path("training/data/train.jsonl")) parser.add_argument("--dev-file", type=Path, default=Path("training/data/dev.jsonl")) parser.add_argument("--output-dir", type=Path, default=Path("training/output/jawbreaker-minicpm-lora")) parser.add_argument("--max-length", type=int, default=768) parser.add_argument("--epochs", type=float, default=1.0) parser.add_argument("--batch-size", type=int, default=1) parser.add_argument("--grad-accum", type=int, default=16) parser.add_argument("--learning-rate", type=float, default=2e-4) parser.add_argument("--warmup-ratio", type=float, default=0.0) parser.add_argument("--weight-decay", type=float, default=0.0) parser.add_argument("--lr-scheduler-type", default="linear") parser.add_argument("--lora-r", type=int, default=16) parser.add_argument("--lora-alpha", type=int, default=32) parser.add_argument("--lora-dropout", type=float, default=0.05) parser.add_argument("--trust-remote-code", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--push-to-hub", action="store_true") parser.add_argument("--hub-model-id", default=None) return parser.parse_args() def main() -> None: args = parse_args() tokenizer = AutoTokenizer.from_pretrained(args.model_id, trust_remote_code=args.trust_remote_code) if tokenizer.pad_token_id is None: tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained( args.model_id, dtype="auto", device_map="auto", trust_remote_code=args.trust_remote_code, ) model.config.use_cache = False lora_config = LoraConfig( r=args.lora_r, lora_alpha=args.lora_alpha, lora_dropout=args.lora_dropout, bias="none", task_type=TaskType.CAUSAL_LM, target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", ], ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() train_dataset = ChatJsonlDataset(args.train_file, tokenizer, args.max_length) eval_dataset = ChatJsonlDataset(args.dev_file, tokenizer, args.max_length) training_args = TrainingArguments( output_dir=str(args.output_dir), num_train_epochs=args.epochs, per_device_train_batch_size=args.batch_size, per_device_eval_batch_size=args.batch_size, gradient_accumulation_steps=args.grad_accum, learning_rate=args.learning_rate, warmup_ratio=args.warmup_ratio, weight_decay=args.weight_decay, lr_scheduler_type=args.lr_scheduler_type, logging_steps=10, eval_strategy="steps", eval_steps=50, save_steps=100, save_total_limit=2, bf16=torch.cuda.is_available(), report_to="none", push_to_hub=args.push_to_hub, hub_model_id=args.hub_model_id, ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, data_collator=DataCollator(tokenizer), ) trainer.train() trainer.save_model(str(args.output_dir)) tokenizer.save_pretrained(str(args.output_dir)) if args.push_to_hub: trainer.push_to_hub() if __name__ == "__main__": main()