Views
No views yet
Qwen/Qwen2.5-VL-3B-Instruct. We add a
scalar score head that pools the last non-pad token's hidden state
through a single nn.Linear(2048, 1, bias=False) and train the whole
thing with the standard Bradley-Terry loss
-log σ(score(chosen) - score(rejected)).rm-qwen25vl-3b-20kprompt field which carries
GPT-4o-generated Scene/Twist/Location/Entities descriptions for the
specific 813 cartoons in caption_sft_train. Including them was
hand-feeding the answer to a vision-language model and made the RM
unusable on cartoons without those annotations.| metric | value |
|---|---|
| pairwise accuracy | 0.6635 |
| reward margin (chosen − rejected) | +0.626 ± 1.42 std |
| BT loss | 0.626 |
| backbone | Qwen/Qwen2.5-VL-3B-Instruct (LoRA-adapted) |
| LoRA | r=32, α=32, target_modules="all-linear", bias=none |
| score head | nn.Linear(2048, 1, bias=False), zero-initialized |
| pooling | last non-pad token of the (single) user turn |
| message format | one user turn: image + "Write a funny one-line caption ... Candidate caption: {X} ... Judge how funny this caption is for the cartoon." |
| optimizer | AdamW (fused), weight_decay=0 |
| LR | 2e-4 constant, no warmup |
| max_grad_norm | 1.0 |
| effective batch size | 32 (per-device 4 × accum 8 × 1 GPU) |
| precision | bf16, FlashAttention-2, no gradient checkpointing |
| epochs | 1 |
| image preprocessing | long-edge resized to 448 px |
| training pairs | 20 000 BT pairs (3-σ filter, ≤1000/contest, from caption_sft_train) |
| hardware | 1 × NVIDIA A100-SXM4-80GB |
| wall clock | ~62 min |
backbone_adapter/ — LoRA adapter on Qwen2.5-VL-3B-Instructprocessor/ — Qwen2.5-VL processor (image processor + tokenizer)reward_head.pt — nn.Linear(2048, 1, bias=False) state_dictreward_model_config.json — base model id + score head shapeeval_2k.json — 2K-pair eval JSON1import torch
2from huggingface_hub import snapshot_download
3from peft import PeftModel
4from PIL import Image
5from torch import nn
6from transformers import AutoModel, AutoProcessor
7
8local = snapshot_download("HumorR1/rm-qwen25vl-3b-nodesc")
9base = AutoModel.from_pretrained(
10 "Qwen/Qwen2.5-VL-3B-Instruct",
11 dtype=torch.bfloat16, attn_implementation="sdpa",
12)
13backbone = PeftModel.from_pretrained(base, f"{local}/backbone_adapter")
14score_head = nn.Linear(base.config.text_config.hidden_size, 1, bias=False).to(torch.bfloat16)
15score_head.load_state_dict(torch.load(f"{local}/reward_head.pt"))
16
17processor = AutoProcessor.from_pretrained(f"{local}/processor")
18backbone.eval().to("cuda"); score_head.eval().to("cuda")
19
20
21@torch.no_grad()
22def score(image: Image.Image, caption: str) -> float:
23 text = (
24 "Write a funny one-line caption for this New Yorker-style cartoon.\n\n"
25 f"Candidate caption: {caption}\n\n"
26 "Judge how funny this caption is for the cartoon."
27 )
28 messages = [
29 {"role": "user", "content": [
30 {"type": "image"}, {"type": "text", "text": text},
31 ]}
32 ]
33 chat = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
34 inputs = processor(text=[chat], images=[image], return_tensors="pt", padding=True).to("cuda")
35 out = backbone(**inputs, return_dict=True)
36 last_hidden = out.last_hidden_state
37 last_idx = inputs["attention_mask"].long().sum(dim=1) - 1
38 pooled = last_hidden[
39 torch.arange(last_hidden.size(0), device=last_hidden.device), last_idx
40 ]
41 return float(score_head(pooled.to(score_head.weight.dtype)).item())