Upload folder using huggingface_hub
Browse files- evaluation/inference_aesthetic.py +436 -0
- qwenvl/__init__.py +0 -0
- qwenvl/data/__init__.py +77 -0
- qwenvl/data/aesthetic_dataset.py +419 -0
- qwenvl/data/data_processor.py +693 -0
- qwenvl/train/__init__.py +0 -0
- qwenvl/train/argument.py +70 -0
- qwenvl/train/logging_callback.py +141 -0
- qwenvl/train/score_loss.py +247 -0
- qwenvl/train/score_tokens.py +222 -0
- qwenvl/train/train_qwen.py +496 -0
- scripts/sft_aesthetic.sh +146 -0
- scripts/zero2.json +23 -0
evaluation/inference_aesthetic.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inference script for the aesthetic scoring model (score-token approach).
|
| 3 |
+
|
| 4 |
+
For each test image, runs 13 criterion queries + 1 total-score query,
|
| 5 |
+
maps score tokens to numeric values, then classifies into levels:
|
| 6 |
+
A β Poor (0 <= score < 5)
|
| 7 |
+
B β Medium (5 <= score < 7)
|
| 8 |
+
C β Good (7 <= score <= 10)
|
| 9 |
+
|
| 10 |
+
Also answers the per-image MCQ by feeding the question+options directly.
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
CUDA_VISIBLE_DEVICES=0,1,2,3 python evaluation/inference_aesthetic.py
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import json
|
| 19 |
+
import math
|
| 20 |
+
import re
|
| 21 |
+
import glob
|
| 22 |
+
import time
|
| 23 |
+
import torch
|
| 24 |
+
import multiprocessing as mp
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from tqdm import tqdm
|
| 27 |
+
from multiprocessing import Process
|
| 28 |
+
from PIL import Image
|
| 29 |
+
|
| 30 |
+
Image.MAX_IMAGE_PIXELS = None
|
| 31 |
+
mp.set_start_method("spawn", force=True)
|
| 32 |
+
|
| 33 |
+
project_root = Path(__file__).parent.parent
|
| 34 |
+
sys.path.insert(0, str(project_root))
|
| 35 |
+
|
| 36 |
+
from qwenvl.train.score_tokens import ScoreTokenRegistry
|
| 37 |
+
from qwenvl.data.aesthetic_dataset import CRITERIA_QUESTIONS, CRITERIA_ORDER, TOTAL_SCORE_QUESTION
|
| 38 |
+
|
| 39 |
+
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
+
CHECKPOINT = ""
|
| 41 |
+
|
| 42 |
+
IMAGE_DIR = "zijielou/PortraitCraft/main"
|
| 43 |
+
|
| 44 |
+
TEST_JSON = "PortraitCraft/track_1_test.json"
|
| 45 |
+
|
| 46 |
+
OUTPUT_JSON = "PortraitCraft/track_1_test_results_2.json"
|
| 47 |
+
|
| 48 |
+
WORKERS_PER_GPU = 1
|
| 49 |
+
MAX_NEW_TOKENS = 1024
|
| 50 |
+
|
| 51 |
+
# ββ Score token registries (must match training) βββββββββββββββββββββββββββββ
|
| 52 |
+
CRIT_BINS = 20
|
| 53 |
+
TOTAL_BINS = 101
|
| 54 |
+
|
| 55 |
+
crit_registry = ScoreTokenRegistry(n_bins=CRIT_BINS, score_min=1.0, score_max=10.0, prefix="score")
|
| 56 |
+
total_registry = ScoreTokenRegistry(n_bins=TOTAL_BINS, score_min=0.0, score_max=100.0, prefix="total")
|
| 57 |
+
|
| 58 |
+
# ββ Level classification βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
+
def score_to_level(score: float) -> str:
|
| 60 |
+
if score < 5.0:
|
| 61 |
+
return "A"
|
| 62 |
+
elif score < 7.0:
|
| 63 |
+
return "B"
|
| 64 |
+
else:
|
| 65 |
+
return "C"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ββ Image path resolver (test json has flat names, images live in subdirs) βββ
|
| 69 |
+
_image_index = None
|
| 70 |
+
|
| 71 |
+
def build_image_index(image_dir: str) -> dict:
|
| 72 |
+
index = {}
|
| 73 |
+
for subdir in sorted(os.listdir(image_dir)):
|
| 74 |
+
subdir_path = os.path.join(image_dir, subdir)
|
| 75 |
+
if not os.path.isdir(subdir_path):
|
| 76 |
+
continue
|
| 77 |
+
for fname in os.listdir(subdir_path):
|
| 78 |
+
index[fname] = os.path.join(subdir_path, fname)
|
| 79 |
+
return index
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def resolve_image_path(image_name: str, image_dir: str) -> str:
|
| 83 |
+
global _image_index
|
| 84 |
+
full = os.path.join(image_dir, image_name)
|
| 85 |
+
if os.path.exists(full):
|
| 86 |
+
return full
|
| 87 |
+
if _image_index is None:
|
| 88 |
+
_image_index = build_image_index(image_dir)
|
| 89 |
+
return _image_index.get(image_name, full)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ββ Model wrapper βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 93 |
+
class AestheticInferenceServer:
|
| 94 |
+
def __init__(self, gpu_id: int):
|
| 95 |
+
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
| 96 |
+
|
| 97 |
+
self.device = torch.device(f"cuda:{gpu_id}")
|
| 98 |
+
print(f"Loading model on GPU {gpu_id} from {CHECKPOINT}...")
|
| 99 |
+
|
| 100 |
+
self.model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 101 |
+
CHECKPOINT,
|
| 102 |
+
torch_dtype=torch.bfloat16,
|
| 103 |
+
attn_implementation="flash_attention_2",
|
| 104 |
+
).to(self.device).eval()
|
| 105 |
+
|
| 106 |
+
self.processor = AutoProcessor.from_pretrained(CHECKPOINT)
|
| 107 |
+
|
| 108 |
+
self.crit_token_ids = crit_registry.get_token_ids(self.processor.tokenizer)
|
| 109 |
+
self.crit_values = crit_registry.get_value_tensor(device=self.device)
|
| 110 |
+
self.total_token_ids = total_registry.get_token_ids(self.processor.tokenizer)
|
| 111 |
+
self.total_values = total_registry.get_value_tensor(device=self.device)
|
| 112 |
+
|
| 113 |
+
print(f"Model loaded on GPU {gpu_id}")
|
| 114 |
+
|
| 115 |
+
def _generate(self, image_path: str, text: str, max_new_tokens: int = None) -> tuple:
|
| 116 |
+
"""Returns (generated_text, generated_ids, scores_tuple)."""
|
| 117 |
+
if max_new_tokens is None:
|
| 118 |
+
max_new_tokens = MAX_NEW_TOKENS
|
| 119 |
+
|
| 120 |
+
img = Image.open(image_path).convert("RGB")
|
| 121 |
+
messages = [{
|
| 122 |
+
"role": "user",
|
| 123 |
+
"content": [
|
| 124 |
+
{"type": "image", "image": img},
|
| 125 |
+
{"type": "text", "text": text},
|
| 126 |
+
],
|
| 127 |
+
}]
|
| 128 |
+
|
| 129 |
+
inputs = self.processor.apply_chat_template(
|
| 130 |
+
messages, tokenize=True, add_generation_prompt=True,
|
| 131 |
+
return_dict=True, return_tensors="pt",
|
| 132 |
+
).to(self.device)
|
| 133 |
+
|
| 134 |
+
with torch.no_grad():
|
| 135 |
+
outputs = self.model.generate(
|
| 136 |
+
**inputs,
|
| 137 |
+
max_new_tokens=max_new_tokens,
|
| 138 |
+
do_sample=False,
|
| 139 |
+
return_dict_in_generate=True,
|
| 140 |
+
output_scores=True,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
generated_ids = outputs.sequences[0][inputs["input_ids"].shape[1]:]
|
| 144 |
+
generated_text = self.processor.tokenizer.decode(generated_ids, skip_special_tokens=False)
|
| 145 |
+
scores_tuple = outputs.scores
|
| 146 |
+
|
| 147 |
+
return generated_text, generated_ids, scores_tuple
|
| 148 |
+
|
| 149 |
+
def predict_criterion(self, image_path: str, criterion: str) -> dict:
|
| 150 |
+
question = CRITERIA_QUESTIONS[criterion]
|
| 151 |
+
gen_text, gen_ids, scores_tuple = self._generate(image_path, question)
|
| 152 |
+
|
| 153 |
+
score_token_id_set = set(self.crit_token_ids)
|
| 154 |
+
expected_score = None
|
| 155 |
+
argmax_score = None
|
| 156 |
+
argmax_token = None
|
| 157 |
+
|
| 158 |
+
gen_ids_list = gen_ids.tolist()
|
| 159 |
+
for step_idx, tid in enumerate(gen_ids_list):
|
| 160 |
+
if tid in score_token_id_set:
|
| 161 |
+
token_str = self.processor.tokenizer.decode([tid])
|
| 162 |
+
argmax_token = token_str.strip()
|
| 163 |
+
argmax_score = crit_registry.token_to_score(argmax_token)
|
| 164 |
+
logits_at_step = scores_tuple[step_idx]
|
| 165 |
+
expected_score = crit_registry.expected_score(
|
| 166 |
+
logits_at_step[0], self.crit_token_ids, self.crit_values
|
| 167 |
+
).item()
|
| 168 |
+
break
|
| 169 |
+
|
| 170 |
+
final_score = expected_score if expected_score is not None else argmax_score
|
| 171 |
+
if final_score is None:
|
| 172 |
+
print(f" WARNING: no score token found for [{criterion}], raw output: {gen_text[:200]}", flush=True)
|
| 173 |
+
final_score = 5.0
|
| 174 |
+
|
| 175 |
+
return {
|
| 176 |
+
"score": round(final_score, 2),
|
| 177 |
+
"level": score_to_level(final_score),
|
| 178 |
+
"argmax_token": argmax_token,
|
| 179 |
+
"argmax_score": round(argmax_score, 2) if argmax_score is not None else None,
|
| 180 |
+
"expected_score": round(expected_score, 2) if expected_score is not None else None,
|
| 181 |
+
"raw_text": gen_text,
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
def predict_total(self, image_path: str) -> dict:
|
| 185 |
+
gen_text, gen_ids, scores_tuple = self._generate(image_path, TOTAL_SCORE_QUESTION)
|
| 186 |
+
|
| 187 |
+
total_token_id_set = set(self.total_token_ids)
|
| 188 |
+
expected_score = None
|
| 189 |
+
argmax_score = None
|
| 190 |
+
argmax_token = None
|
| 191 |
+
|
| 192 |
+
gen_ids_list = gen_ids.tolist()
|
| 193 |
+
for step_idx, tid in enumerate(gen_ids_list):
|
| 194 |
+
if tid in total_token_id_set:
|
| 195 |
+
token_str = self.processor.tokenizer.decode([tid])
|
| 196 |
+
argmax_token = token_str.strip()
|
| 197 |
+
argmax_score = total_registry.token_to_score(argmax_token)
|
| 198 |
+
logits_at_step = scores_tuple[step_idx]
|
| 199 |
+
expected_score = total_registry.expected_score(
|
| 200 |
+
logits_at_step[0], self.total_token_ids, self.total_values
|
| 201 |
+
).item()
|
| 202 |
+
break
|
| 203 |
+
|
| 204 |
+
final_score = expected_score if expected_score is not None else argmax_score
|
| 205 |
+
if final_score is None:
|
| 206 |
+
print(f" WARNING: no total token found, raw output: {gen_text[:200]}", flush=True)
|
| 207 |
+
final_score = 50.0
|
| 208 |
+
|
| 209 |
+
return {
|
| 210 |
+
"total_score": round(final_score, 1),
|
| 211 |
+
"argmax_token": argmax_token,
|
| 212 |
+
"argmax_score": round(argmax_score, 1) if argmax_score is not None else None,
|
| 213 |
+
"expected_score": round(expected_score, 1) if expected_score is not None else None,
|
| 214 |
+
"raw_text": gen_text,
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
def answer_mcq(self, image_path: str, question: str, options: dict) -> dict:
|
| 218 |
+
options_text = "\n".join([f"{k}. {v}" for k, v in options.items()])
|
| 219 |
+
prompt = (
|
| 220 |
+
f"{question}\n\n"
|
| 221 |
+
f"Options:\n{options_text}\n\n"
|
| 222 |
+
f"Answer with a single letter (A, B, C, or D)."
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
img = Image.open(image_path).convert("RGB")
|
| 226 |
+
messages = [{
|
| 227 |
+
"role": "user",
|
| 228 |
+
"content": [
|
| 229 |
+
{"type": "image", "image": img},
|
| 230 |
+
{"type": "text", "text": prompt},
|
| 231 |
+
],
|
| 232 |
+
}]
|
| 233 |
+
inputs = self.processor.apply_chat_template(
|
| 234 |
+
messages, tokenize=True, add_generation_prompt=True,
|
| 235 |
+
return_dict=True, return_tensors="pt",
|
| 236 |
+
).to(self.device)
|
| 237 |
+
|
| 238 |
+
think_ids = self.processor.tokenizer.encode("<think>\n", add_special_tokens=False)
|
| 239 |
+
think_tensor = torch.tensor([think_ids], device=self.device)
|
| 240 |
+
inputs["input_ids"] = torch.cat([inputs["input_ids"], think_tensor], dim=1)
|
| 241 |
+
if "attention_mask" in inputs:
|
| 242 |
+
inputs["attention_mask"] = torch.cat(
|
| 243 |
+
[inputs["attention_mask"], torch.ones_like(think_tensor)], dim=1
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
with torch.no_grad():
|
| 247 |
+
outputs = self.model.generate(
|
| 248 |
+
**inputs,
|
| 249 |
+
max_new_tokens=2048,
|
| 250 |
+
do_sample=False,
|
| 251 |
+
return_dict_in_generate=True,
|
| 252 |
+
output_scores=True,
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
generated_ids = outputs.sequences[0][inputs["input_ids"].shape[1]:]
|
| 256 |
+
gen_text = self.processor.tokenizer.decode(generated_ids, skip_special_tokens=False)
|
| 257 |
+
|
| 258 |
+
text_to_search = gen_text.split("</think>")[-1] if "</think>" in gen_text else gen_text
|
| 259 |
+
match = re.search(r'\b([A-D])\b', text_to_search)
|
| 260 |
+
answer = match.group(1) if match else "A"
|
| 261 |
+
return {"answer": answer, "raw_text": gen_text}
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
# ββ Worker ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 265 |
+
def worker_run(worker_id: int, gpu_id: int, data_chunk: list):
|
| 266 |
+
server = AestheticInferenceServer(gpu_id)
|
| 267 |
+
|
| 268 |
+
out_path = OUTPUT_JSON + f".part{worker_id}.json"
|
| 269 |
+
|
| 270 |
+
results = []
|
| 271 |
+
if os.path.exists(out_path):
|
| 272 |
+
try:
|
| 273 |
+
with open(out_path, "r", encoding="utf-8") as f:
|
| 274 |
+
results = json.load(f)
|
| 275 |
+
print(f"Worker {worker_id} resuming from {len(results)} existing results")
|
| 276 |
+
except Exception:
|
| 277 |
+
results = []
|
| 278 |
+
|
| 279 |
+
done_images = {r["image_path"] for r in results}
|
| 280 |
+
|
| 281 |
+
total_to_process = len([it for it in data_chunk if it["image_path"] not in done_images])
|
| 282 |
+
processed_count = 0
|
| 283 |
+
t_start_all = time.time()
|
| 284 |
+
|
| 285 |
+
for item_idx, item in enumerate(data_chunk):
|
| 286 |
+
if item["image_path"] in done_images:
|
| 287 |
+
continue
|
| 288 |
+
|
| 289 |
+
image_path = resolve_image_path(item["image_path"], IMAGE_DIR)
|
| 290 |
+
if not os.path.exists(image_path):
|
| 291 |
+
print(f"[W{worker_id}] IMAGE NOT FOUND: {image_path}", flush=True)
|
| 292 |
+
continue
|
| 293 |
+
|
| 294 |
+
t_img_start = time.time()
|
| 295 |
+
|
| 296 |
+
try:
|
| 297 |
+
criteria_results = {}
|
| 298 |
+
print(f"\n[W{worker_id}] ({'='*60})", flush=True)
|
| 299 |
+
print(f"[W{worker_id}] Image {processed_count+1}/{total_to_process}: {item['image_path']}", flush=True)
|
| 300 |
+
print(f"[W{worker_id}] {'-'*60}", flush=True)
|
| 301 |
+
|
| 302 |
+
for crit_name in CRITERIA_ORDER:
|
| 303 |
+
t_crit = time.time()
|
| 304 |
+
pred = server.predict_criterion(image_path, crit_name)
|
| 305 |
+
criteria_results[crit_name] = {
|
| 306 |
+
"level": pred["level"],
|
| 307 |
+
"score": pred["score"],
|
| 308 |
+
"argmax_token": pred.get("argmax_token"),
|
| 309 |
+
"argmax_score": pred.get("argmax_score"),
|
| 310 |
+
"expected_score": pred.get("expected_score"),
|
| 311 |
+
"raw_text": pred.get("raw_text", ""),
|
| 312 |
+
}
|
| 313 |
+
elapsed = time.time() - t_crit
|
| 314 |
+
argmax_info = f"argmax={pred.get('argmax_token','?')}({pred.get('argmax_score','?')})" if pred.get('argmax_token') else "NO_TOKEN"
|
| 315 |
+
print(f"[W{worker_id}] {crit_name:45s} => E[score]={pred['score']:5.2f} level={pred['level']:6s} {argmax_info} ({elapsed:.1f}s)", flush=True)
|
| 316 |
+
print(f"[W{worker_id}] raw_text: {pred.get('raw_text', '')[:300]}", flush=True)
|
| 317 |
+
|
| 318 |
+
total_pred = server.predict_total(image_path)
|
| 319 |
+
t_argmax = f"argmax={total_pred.get('argmax_token','?')}({total_pred.get('argmax_score','?')})" if total_pred.get('argmax_token') else "NO_TOKEN"
|
| 320 |
+
print(f"[W{worker_id}] {'Total Score':45s} => E[score]={total_pred['total_score']:5.1f}/100 {t_argmax}", flush=True)
|
| 321 |
+
print(f"[W{worker_id}] raw_text: {total_pred.get('raw_text', '')[:300]}", flush=True)
|
| 322 |
+
|
| 323 |
+
mcq_result = server.answer_mcq(
|
| 324 |
+
image_path,
|
| 325 |
+
item.get("question", ""),
|
| 326 |
+
item.get("options", {}),
|
| 327 |
+
)
|
| 328 |
+
mcq_answer = mcq_result["answer"]
|
| 329 |
+
mcq_raw = mcq_result["raw_text"]
|
| 330 |
+
print(f"[W{worker_id}] {'MCQ Answer':45s} => {mcq_answer}", flush=True)
|
| 331 |
+
print(f"[W{worker_id}] raw_text: {mcq_raw[:500]}", flush=True)
|
| 332 |
+
|
| 333 |
+
t_img_elapsed = time.time() - t_img_start
|
| 334 |
+
processed_count += 1
|
| 335 |
+
avg_time = (time.time() - t_start_all) / processed_count
|
| 336 |
+
eta = avg_time * (total_to_process - processed_count)
|
| 337 |
+
print(f"[W{worker_id}] {'-'*60}", flush=True)
|
| 338 |
+
print(f"[W{worker_id}] Done in {t_img_elapsed:.1f}s | Progress: {processed_count}/{total_to_process} | ETA: {eta/60:.1f}min", flush=True)
|
| 339 |
+
|
| 340 |
+
level_summary = {v["level"]: 0 for v in criteria_results.values()}
|
| 341 |
+
for v in criteria_results.values():
|
| 342 |
+
level_summary[v["level"]] += 1
|
| 343 |
+
print(f"[W{worker_id}] Level distribution: {dict(level_summary)} | total={total_pred['total_score']} | mcq={mcq_answer}", flush=True)
|
| 344 |
+
|
| 345 |
+
record = {
|
| 346 |
+
"image_path": item["image_path"],
|
| 347 |
+
"criteria": criteria_results,
|
| 348 |
+
"total_score": total_pred["total_score"],
|
| 349 |
+
"total_score_detail": {
|
| 350 |
+
"argmax_token": total_pred.get("argmax_token"),
|
| 351 |
+
"argmax_score": total_pred.get("argmax_score"),
|
| 352 |
+
"expected_score": total_pred.get("expected_score"),
|
| 353 |
+
"raw_text": total_pred.get("raw_text", ""),
|
| 354 |
+
},
|
| 355 |
+
"question": item.get("question", ""),
|
| 356 |
+
"options": item.get("options", {}),
|
| 357 |
+
"answer": mcq_answer,
|
| 358 |
+
"mcq_raw_text": mcq_raw,
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
results.append(record)
|
| 362 |
+
|
| 363 |
+
tmp_path = out_path + ".tmp"
|
| 364 |
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
| 365 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 366 |
+
os.replace(tmp_path, out_path)
|
| 367 |
+
print(f"[W{worker_id}] Saved to {out_path} ({len(results)} total records)", flush=True)
|
| 368 |
+
|
| 369 |
+
except Exception as e:
|
| 370 |
+
print(f"[W{worker_id}] ERROR on {item['image_path']}: {e}", flush=True)
|
| 371 |
+
import traceback
|
| 372 |
+
traceback.print_exc()
|
| 373 |
+
continue
|
| 374 |
+
|
| 375 |
+
total_elapsed = time.time() - t_start_all
|
| 376 |
+
print(f"\n[W{worker_id}] ALL DONE | {len(results)} records | {total_elapsed/60:.1f}min total", flush=True)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
# ββ Merge βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 380 |
+
def merge_results():
|
| 381 |
+
files = glob.glob(OUTPUT_JSON + ".part*.json")
|
| 382 |
+
unique = {}
|
| 383 |
+
for f in files:
|
| 384 |
+
with open(f, "r", encoding="utf-8") as fp:
|
| 385 |
+
for x in json.load(fp):
|
| 386 |
+
unique[x["image_path"]] = x
|
| 387 |
+
all_results = list(unique.values())
|
| 388 |
+
with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
|
| 389 |
+
json.dump(all_results, f, ensure_ascii=False, indent=2)
|
| 390 |
+
print(f"Merged {len(all_results)} results to {OUTPUT_JSON}")
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
# ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 394 |
+
def main():
|
| 395 |
+
with open(TEST_JSON, "r", encoding="utf-8") as f:
|
| 396 |
+
data = json.load(f)
|
| 397 |
+
|
| 398 |
+
done_set = set()
|
| 399 |
+
for part_file in glob.glob(OUTPUT_JSON + ".part*.json"):
|
| 400 |
+
try:
|
| 401 |
+
with open(part_file, "r") as fp:
|
| 402 |
+
for x in json.load(fp):
|
| 403 |
+
done_set.add(x["image_path"])
|
| 404 |
+
except Exception:
|
| 405 |
+
continue
|
| 406 |
+
|
| 407 |
+
remaining = [item for item in data if item["image_path"] not in done_set]
|
| 408 |
+
print(f"Total: {len(data)}, already done: {len(done_set)}, remaining: {len(remaining)}")
|
| 409 |
+
|
| 410 |
+
if len(remaining) == 0:
|
| 411 |
+
print("All done, merging...")
|
| 412 |
+
merge_results()
|
| 413 |
+
return
|
| 414 |
+
|
| 415 |
+
NUM_GPUS = torch.cuda.device_count()
|
| 416 |
+
TOTAL_WORKERS = NUM_GPUS * WORKERS_PER_GPU
|
| 417 |
+
print(f"GPUs: {NUM_GPUS}, workers: {TOTAL_WORKERS}")
|
| 418 |
+
|
| 419 |
+
chunk_size = math.ceil(len(remaining) / TOTAL_WORKERS)
|
| 420 |
+
chunks = [remaining[i:i + chunk_size] for i in range(0, len(remaining), chunk_size)]
|
| 421 |
+
|
| 422 |
+
processes = []
|
| 423 |
+
for i, chunk in enumerate(chunks):
|
| 424 |
+
gpu_id = i // WORKERS_PER_GPU
|
| 425 |
+
p = Process(target=worker_run, args=(i, gpu_id, chunk))
|
| 426 |
+
p.start()
|
| 427 |
+
processes.append(p)
|
| 428 |
+
|
| 429 |
+
for p in processes:
|
| 430 |
+
p.join()
|
| 431 |
+
|
| 432 |
+
merge_results()
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
if __name__ == "__main__":
|
| 436 |
+
main()
|
qwenvl/__init__.py
ADDED
|
File without changes
|
qwenvl/data/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
# Define placeholders for dataset paths
|
| 4 |
+
CAMBRIAN_737K = {
|
| 5 |
+
"annotation_path": "PATH_TO_CAMBRIAN_737K_ANNOTATION",
|
| 6 |
+
"data_path": "",
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
CAMBRIAN_737K_PACK = {
|
| 10 |
+
"annotation_path": f"PATH_TO_CAMBRIAN_737K_ANNOTATION_PACKED",
|
| 11 |
+
"data_path": f"",
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
MP_DOC = {
|
| 15 |
+
"annotation_path": "PATH_TO_MP_DOC_ANNOTATION",
|
| 16 |
+
"data_path": "PATH_TO_MP_DOC_DATA",
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
CLEVR_MC = {
|
| 20 |
+
"annotation_path": "PATH_TO_CLEVR_MC_ANNOTATION",
|
| 21 |
+
"data_path": "PATH_TO_CLEVR_MC_DATA",
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
VIDEOCHATGPT = {
|
| 25 |
+
"annotation_path": "PATH_TO_VIDEOCHATGPT_ANNOTATION",
|
| 26 |
+
"data_path": "PATH_TO_VIDEOCHATGPT_DATA",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
SINGLEIMAGES = {
|
| 30 |
+
"annotation_path": "./demo/single_images_train_convert.json",
|
| 31 |
+
"data_path": "./demo/images",
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
AESTHETIC_TRACK1 = {
|
| 35 |
+
# image_path in json uses subdir format: images_00/xxx.jpg
|
| 36 |
+
"annotation_path": "PortraitCraft/track_1_train_subdir.json",
|
| 37 |
+
"data_path": "zijielou/PortraitCraft/main",
|
| 38 |
+
"dataset_type": "aesthetic",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
data_dict = {
|
| 42 |
+
"cambrian_737k": CAMBRIAN_737K,
|
| 43 |
+
"cambrian_737k_pack": CAMBRIAN_737K_PACK,
|
| 44 |
+
"mp_doc": MP_DOC,
|
| 45 |
+
"clevr_mc": CLEVR_MC,
|
| 46 |
+
"videochatgpt": VIDEOCHATGPT,
|
| 47 |
+
"singleimages": SINGLEIMAGES,
|
| 48 |
+
"aesthetic_track1": AESTHETIC_TRACK1,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def parse_sampling_rate(dataset_name):
|
| 53 |
+
match = re.search(r"%(\d+)$", dataset_name)
|
| 54 |
+
if match:
|
| 55 |
+
return int(match.group(1)) / 100.0
|
| 56 |
+
return 1.0
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def data_list(dataset_names):
|
| 60 |
+
config_list = []
|
| 61 |
+
for dataset_name in dataset_names:
|
| 62 |
+
sampling_rate = parse_sampling_rate(dataset_name)
|
| 63 |
+
dataset_name = re.sub(r"%(\d+)$", "", dataset_name)
|
| 64 |
+
if dataset_name in data_dict.keys():
|
| 65 |
+
config = data_dict[dataset_name].copy()
|
| 66 |
+
config["sampling_rate"] = sampling_rate
|
| 67 |
+
config_list.append(config)
|
| 68 |
+
else:
|
| 69 |
+
raise ValueError(f"do not find {dataset_name}")
|
| 70 |
+
return config_list
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
dataset_names = ["cambrian_737k"]
|
| 75 |
+
configs = data_list(dataset_names)
|
| 76 |
+
for config in configs:
|
| 77 |
+
print(config)
|
qwenvl/data/aesthetic_dataset.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dataset for image aesthetic scoring using generic integer-index score tokens.
|
| 3 |
+
|
| 4 |
+
Each image produces N+1 independent training samples:
|
| 5 |
+
- one sample per criterion (N=13)
|
| 6 |
+
- one sample for total score
|
| 7 |
+
|
| 8 |
+
Every sample is a single-turn VQA pair:
|
| 9 |
+
user: <image> + criterion-specific question
|
| 10 |
+
assistant: <think>reason</think>\nThe score is <scoreK>
|
| 11 |
+
|
| 12 |
+
The <scoreK> token is what we use for the logits-weighted prediction.
|
| 13 |
+
The <think>...</think> block is labelled for CE loss so the model learns to reason.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import json
|
| 17 |
+
import time
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Dict, Any, List, Tuple
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
from torch.utils.data import Dataset
|
| 23 |
+
|
| 24 |
+
from .rope2d import get_rope_index_25, get_rope_index_2, get_rope_index_3
|
| 25 |
+
from ..train.score_tokens import ScoreTokenRegistry
|
| 26 |
+
|
| 27 |
+
IGNORE_INDEX = -100
|
| 28 |
+
|
| 29 |
+
# ββ Per-criterion questions βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
# Each question is carefully designed to probe the specific visual dimension.
|
| 31 |
+
# Default instruction uses the original <score1>..<score20> tokens.
|
| 32 |
+
# When existing vocab tokens are used, build_criteria_questions() overrides this.
|
| 33 |
+
CRIT_SCORE_INSTRUCTION = (
|
| 34 |
+
"Rate this aspect using a score token from <score1> (lowest quality) to <score20> (highest quality)."
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
_CRITERIA_QUESTION_BODIES: Dict[str, str] = {
|
| 38 |
+
"Color Harmony": (
|
| 39 |
+
"Evaluate the color harmony of this image. "
|
| 40 |
+
"Consider how well the colors work together in terms of palette coherence, "
|
| 41 |
+
"contrast balance, and emotional consistency. "
|
| 42 |
+
),
|
| 43 |
+
"Visual Style Consistency": (
|
| 44 |
+
"Assess the visual style consistency of this image. "
|
| 45 |
+
"Does the overall aesthetic β including lighting, color grading, subject treatment, "
|
| 46 |
+
"and compositional choices β feel unified and purposeful throughout? "
|
| 47 |
+
),
|
| 48 |
+
"Sharpness": (
|
| 49 |
+
"Evaluate the sharpness of this image. "
|
| 50 |
+
"Focus on whether the intended subject is in crisp focus, "
|
| 51 |
+
"the level of fine detail preserved, and whether any blur is intentional or distracting. "
|
| 52 |
+
),
|
| 53 |
+
"Light and Shadow Modeling": (
|
| 54 |
+
"Assess the light and shadow modeling in this image. "
|
| 55 |
+
"Consider the quality of tonal transitions, whether shadows flatter the subject, "
|
| 56 |
+
"and how well the lighting reveals three-dimensional form. "
|
| 57 |
+
),
|
| 58 |
+
"Creativity and Originality": (
|
| 59 |
+
"Evaluate the creativity and originality of this image. "
|
| 60 |
+
"Does it demonstrate a distinctive perspective, novel visual language, "
|
| 61 |
+
"or an unexpected approach that sets it apart from typical photography? "
|
| 62 |
+
),
|
| 63 |
+
"Exposure Control": (
|
| 64 |
+
"Assess the exposure control of this image. "
|
| 65 |
+
"Consider whether highlights are blown out, shadows are crushed, "
|
| 66 |
+
"and whether the overall tonal range is balanced and intentional. "
|
| 67 |
+
),
|
| 68 |
+
"Application of Classical Composition Principles": (
|
| 69 |
+
"Evaluate how well classical composition principles are applied in this image. "
|
| 70 |
+
"Consider elements such as rule of thirds, leading lines, framing, "
|
| 71 |
+
"symmetry, and the intentionality of the subject placement within the frame. "
|
| 72 |
+
),
|
| 73 |
+
"Depth of Field and Layering": (
|
| 74 |
+
"Assess the depth of field and visual layering in this image. "
|
| 75 |
+
"Consider whether foreground, midground, and background are purposefully separated, "
|
| 76 |
+
"and whether the depth of field effectively isolates the subject. "
|
| 77 |
+
),
|
| 78 |
+
"Visual Center Stability": (
|
| 79 |
+
"Evaluate the visual center stability of this image. "
|
| 80 |
+
"Consider whether there is a clear focal point that anchors the viewer's gaze, "
|
| 81 |
+
"and whether competing elements undermine or reinforce that center. "
|
| 82 |
+
),
|
| 83 |
+
"Visual Flow Guidance": (
|
| 84 |
+
"Assess the visual flow guidance in this image. "
|
| 85 |
+
"Does the composition lead the viewer's eye naturally through the frame "
|
| 86 |
+
"using lines, shapes, contrast, or subject placement? "
|
| 87 |
+
),
|
| 88 |
+
"Structural Support Stability": (
|
| 89 |
+
"Evaluate the structural support and stability of this image's composition. "
|
| 90 |
+
"Consider whether the arrangement of elements creates a balanced, grounded feel "
|
| 91 |
+
"or whether the structure feels unresolved or tension-inducing. "
|
| 92 |
+
),
|
| 93 |
+
"Appropriateness of Negative Space": (
|
| 94 |
+
"Assess the use of negative space in this image. "
|
| 95 |
+
"Consider whether empty areas enhance the subject's presence, "
|
| 96 |
+
"create breathing room, and serve a compositional or emotional purpose. "
|
| 97 |
+
),
|
| 98 |
+
"Subject Integrity": (
|
| 99 |
+
"Evaluate the subject integrity of this image. "
|
| 100 |
+
"Are the main subjects fully visible, clearly defined, and free from "
|
| 101 |
+
"awkward cropping, occlusion, or distracting intrusions that compromise their presence? "
|
| 102 |
+
),
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def build_criteria_questions(crit_registry) -> Dict[str, str]:
|
| 107 |
+
"""Build criteria questions with the actual token names from the registry."""
|
| 108 |
+
first_token = crit_registry.tokens[0]
|
| 109 |
+
last_token = crit_registry.tokens[-1]
|
| 110 |
+
instruction = (
|
| 111 |
+
f"Rate this aspect using a score token from {first_token} (lowest quality) "
|
| 112 |
+
f"to {last_token} (highest quality)."
|
| 113 |
+
)
|
| 114 |
+
return {k: v + instruction for k, v in _CRITERIA_QUESTION_BODIES.items()}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def build_total_score_question(total_registry) -> str:
|
| 118 |
+
"""Build total score question with the actual token names from the registry."""
|
| 119 |
+
first_token = total_registry.tokens[0]
|
| 120 |
+
last_token = total_registry.tokens[-1]
|
| 121 |
+
return (
|
| 122 |
+
"Give an overall aesthetic quality score for this image. "
|
| 123 |
+
"Consider all aspects: composition, exposure, sharpness, color, creativity, "
|
| 124 |
+
"and subject presentation. "
|
| 125 |
+
f"Rate using a score token from {first_token} (lowest, 0) to {last_token} (highest, 100)."
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
# Default questions (backward compatible with original <score1>..<score20> tokens)
|
| 130 |
+
CRITERIA_QUESTIONS: Dict[str, str] = {
|
| 131 |
+
k: v + CRIT_SCORE_INSTRUCTION for k, v in _CRITERIA_QUESTION_BODIES.items()
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
TOTAL_SCORE_QUESTION = (
|
| 135 |
+
"Give an overall aesthetic quality score for this image. "
|
| 136 |
+
"Consider all aspects: composition, exposure, sharpness, color, creativity, "
|
| 137 |
+
"and subject presentation. "
|
| 138 |
+
"Rate using a score token from <total1> (lowest, 0) to <total101> (highest, 100)."
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# Ordered list used to keep sample ordering deterministic
|
| 142 |
+
CRITERIA_ORDER: List[str] = list(CRITERIA_QUESTIONS.keys())
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# ββ Text builders βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 146 |
+
|
| 147 |
+
def build_crit_assistant_text(
|
| 148 |
+
reason: str,
|
| 149 |
+
score: float,
|
| 150 |
+
crit_registry: ScoreTokenRegistry,
|
| 151 |
+
) -> str:
|
| 152 |
+
token = crit_registry.score_to_token(score)
|
| 153 |
+
return f"<think>\n{reason.strip()}\n</think>\nThe score is {token}"
|
| 154 |
+
# return f"The score is {token}"
|
| 155 |
+
|
| 156 |
+
def build_total_assistant_text(
|
| 157 |
+
total_score: float,
|
| 158 |
+
total_registry: ScoreTokenRegistry,
|
| 159 |
+
) -> str:
|
| 160 |
+
token = total_registry.score_to_token(total_score)
|
| 161 |
+
return f"The score is {token}"
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def build_mcq_messages(
|
| 166 |
+
image_path: str,
|
| 167 |
+
question: str,
|
| 168 |
+
options: dict,
|
| 169 |
+
answer: str,
|
| 170 |
+
) -> List[Dict]:
|
| 171 |
+
options_text = "\n".join(f"{k}. {v}" for k, v in sorted(options.items()))
|
| 172 |
+
user_text = f"{question}\n{options_text}"
|
| 173 |
+
return [
|
| 174 |
+
{
|
| 175 |
+
"role": "user",
|
| 176 |
+
"content": [
|
| 177 |
+
{"type": "image", "image": image_path},
|
| 178 |
+
{"type": "text", "text": user_text},
|
| 179 |
+
],
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"role": "assistant",
|
| 183 |
+
"content": [
|
| 184 |
+
{"type": "text", "text": answer},
|
| 185 |
+
],
|
| 186 |
+
},
|
| 187 |
+
]
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def build_crit_messages(
|
| 191 |
+
image_path: str,
|
| 192 |
+
criterion: str,
|
| 193 |
+
reason: str,
|
| 194 |
+
score: float,
|
| 195 |
+
crit_registry,
|
| 196 |
+
criteria_questions: Dict[str, str] = None,
|
| 197 |
+
) -> List[Dict]:
|
| 198 |
+
questions = criteria_questions if criteria_questions is not None else CRITERIA_QUESTIONS
|
| 199 |
+
return [
|
| 200 |
+
{
|
| 201 |
+
"role": "user",
|
| 202 |
+
"content": [
|
| 203 |
+
{"type": "image", "image": image_path},
|
| 204 |
+
{"type": "text", "text": questions[criterion]},
|
| 205 |
+
],
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"role": "assistant",
|
| 209 |
+
"content": [
|
| 210 |
+
{"type": "text", "text": build_crit_assistant_text(reason, score, crit_registry)},
|
| 211 |
+
],
|
| 212 |
+
},
|
| 213 |
+
]
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def build_total_messages(
|
| 217 |
+
image_path: str,
|
| 218 |
+
total_score: float,
|
| 219 |
+
total_registry,
|
| 220 |
+
total_score_question: str = None,
|
| 221 |
+
) -> List[Dict]:
|
| 222 |
+
question = total_score_question if total_score_question is not None else TOTAL_SCORE_QUESTION
|
| 223 |
+
return [
|
| 224 |
+
{
|
| 225 |
+
"role": "user",
|
| 226 |
+
"content": [
|
| 227 |
+
{"type": "image", "image": image_path},
|
| 228 |
+
{"type": "text", "text": question},
|
| 229 |
+
],
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"role": "assistant",
|
| 233 |
+
"content": [
|
| 234 |
+
{"type": "text", "text": build_total_assistant_text(total_score, total_registry)},
|
| 235 |
+
],
|
| 236 |
+
},
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
# ββ Tokenisation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 241 |
+
|
| 242 |
+
def _tokenise_messages(messages: List[Dict], processor) -> Dict:
|
| 243 |
+
full_result = processor.apply_chat_template(
|
| 244 |
+
messages, tokenize=True, return_dict=True, return_tensors="pt"
|
| 245 |
+
)
|
| 246 |
+
input_ids = full_result["input_ids"]
|
| 247 |
+
if isinstance(input_ids, list):
|
| 248 |
+
input_ids = torch.tensor(input_ids).unsqueeze(0)
|
| 249 |
+
|
| 250 |
+
labels = torch.full_like(input_ids, IGNORE_INDEX)
|
| 251 |
+
|
| 252 |
+
# Label the assistant turn only (after token 77091 = "assistant")
|
| 253 |
+
ids_flat = input_ids[0].tolist()
|
| 254 |
+
L = len(ids_flat)
|
| 255 |
+
pos = 0
|
| 256 |
+
while pos < L:
|
| 257 |
+
if ids_flat[pos] == 77091:
|
| 258 |
+
ans_start = pos + 2
|
| 259 |
+
ans_end = ans_start
|
| 260 |
+
while ans_end < L and ids_flat[ans_end] != 151645:
|
| 261 |
+
ans_end += 1
|
| 262 |
+
if ans_end < L:
|
| 263 |
+
labels[0, ans_start : ans_end + 2] = input_ids[0, ans_start : ans_end + 2]
|
| 264 |
+
pos = ans_end
|
| 265 |
+
pos += 1
|
| 266 |
+
|
| 267 |
+
full_result["labels"] = labels
|
| 268 |
+
full_result["input_ids"] = input_ids
|
| 269 |
+
return full_result
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
# ββ Dataset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 273 |
+
|
| 274 |
+
class AestheticScoringDataset(Dataset):
|
| 275 |
+
"""
|
| 276 |
+
Each image in track_1_train.json expands into len(criteria) + 1 samples.
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
processor: HF processor with score tokens already added.
|
| 280 |
+
annotation_path: Path to track_1_train.json.
|
| 281 |
+
image_dir: Root directory for image files.
|
| 282 |
+
data_args: DataArguments (needs model_type).
|
| 283 |
+
crit_registry: ScoreTokenRegistry for criteria scores (1β10).
|
| 284 |
+
total_registry: ScoreTokenRegistry for total score (0β100).
|
| 285 |
+
include_total: Whether to include total-score samples (default True).
|
| 286 |
+
"""
|
| 287 |
+
|
| 288 |
+
def __init__(
|
| 289 |
+
self,
|
| 290 |
+
processor,
|
| 291 |
+
annotation_path: str,
|
| 292 |
+
image_dir: str,
|
| 293 |
+
data_args,
|
| 294 |
+
crit_registry,
|
| 295 |
+
total_registry,
|
| 296 |
+
include_total: bool = True,
|
| 297 |
+
):
|
| 298 |
+
super().__init__()
|
| 299 |
+
with open(annotation_path) as f:
|
| 300 |
+
raw = json.load(f)
|
| 301 |
+
|
| 302 |
+
self.image_dir = Path(image_dir)
|
| 303 |
+
self.processor = processor
|
| 304 |
+
self.crit_registry = crit_registry
|
| 305 |
+
self.total_registry = total_registry
|
| 306 |
+
|
| 307 |
+
# Build questions dynamically based on registry token names
|
| 308 |
+
self.criteria_questions = build_criteria_questions(crit_registry)
|
| 309 |
+
self.total_score_question = build_total_score_question(total_registry)
|
| 310 |
+
|
| 311 |
+
model_type = getattr(data_args, "model_type", "qwen3vl")
|
| 312 |
+
if model_type == "qwen3vl":
|
| 313 |
+
self.get_rope_index = get_rope_index_3
|
| 314 |
+
elif model_type == "qwen2.5vl":
|
| 315 |
+
self.get_rope_index = get_rope_index_25
|
| 316 |
+
elif model_type == "qwen2vl":
|
| 317 |
+
self.get_rope_index = get_rope_index_2
|
| 318 |
+
else:
|
| 319 |
+
raise ValueError(f"Unsupported model_type: {model_type}")
|
| 320 |
+
|
| 321 |
+
self.merge_size = getattr(processor.image_processor, "merge_size", 2)
|
| 322 |
+
|
| 323 |
+
# Expand: each item β (image_path, type_key, score, text_field, question, options)
|
| 324 |
+
# type_key: criterion name | "__total__" | "__mcq__"
|
| 325 |
+
self.samples: List[Tuple] = []
|
| 326 |
+
for item in raw:
|
| 327 |
+
img = item["image_path"]
|
| 328 |
+
if "question" in item:
|
| 329 |
+
# MCQ entry: single sample
|
| 330 |
+
self.samples.append((
|
| 331 |
+
img, "__mcq__", 0.0,
|
| 332 |
+
item["answer"], item["question"], item["options"],
|
| 333 |
+
))
|
| 334 |
+
else:
|
| 335 |
+
# Scoring entry: 13 criteria + 1 total
|
| 336 |
+
criteria = item.get("criteria", {})
|
| 337 |
+
for key in CRITERIA_ORDER:
|
| 338 |
+
if key in criteria:
|
| 339 |
+
entry = criteria[key]
|
| 340 |
+
self.samples.append((
|
| 341 |
+
img,
|
| 342 |
+
key,
|
| 343 |
+
float(entry["score"]),
|
| 344 |
+
entry.get("reason", ""),
|
| 345 |
+
None, None,
|
| 346 |
+
))
|
| 347 |
+
if include_total:
|
| 348 |
+
self.samples.append((
|
| 349 |
+
img,
|
| 350 |
+
"__total__",
|
| 351 |
+
float(item["total_score"]),
|
| 352 |
+
"",
|
| 353 |
+
None, None,
|
| 354 |
+
))
|
| 355 |
+
|
| 356 |
+
def __len__(self) -> int:
|
| 357 |
+
return len(self.samples)
|
| 358 |
+
|
| 359 |
+
def __getitem__(self, i: int) -> Dict[str, torch.Tensor]:
|
| 360 |
+
for attempt in range(3):
|
| 361 |
+
try:
|
| 362 |
+
return self._get_item(i)
|
| 363 |
+
except Exception as e:
|
| 364 |
+
img_name = self.samples[i][0]
|
| 365 |
+
print(f"[Attempt {attempt}] Error loading sample {i} (image={img_name}): {e}")
|
| 366 |
+
# skip to a random different sample instead of retrying the same broken image
|
| 367 |
+
import random
|
| 368 |
+
i = random.randint(0, len(self.samples) - 1)
|
| 369 |
+
time.sleep(0.1)
|
| 370 |
+
return self._get_item(random.randint(0, len(self.samples) - 1))
|
| 371 |
+
|
| 372 |
+
def _get_item(self, i: int) -> Dict[str, torch.Tensor]:
|
| 373 |
+
img_name, criterion, score, text_field, question, options = self.samples[i]
|
| 374 |
+
|
| 375 |
+
if criterion == "__mcq__":
|
| 376 |
+
image_path = str((self.image_dir / img_name).resolve())
|
| 377 |
+
messages = build_mcq_messages(image_path, question, options, text_field)
|
| 378 |
+
elif criterion == "__total__":
|
| 379 |
+
image_path = str((self.image_dir / img_name).resolve())
|
| 380 |
+
messages = build_total_messages(
|
| 381 |
+
image_path, score, self.total_registry,
|
| 382 |
+
total_score_question=self.total_score_question,
|
| 383 |
+
)
|
| 384 |
+
else:
|
| 385 |
+
image_path = str((self.image_dir / img_name).resolve())
|
| 386 |
+
messages = build_crit_messages(
|
| 387 |
+
image_path, criterion, text_field, score, self.crit_registry,
|
| 388 |
+
criteria_questions=self.criteria_questions,
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
data_dict = _tokenise_messages(messages, self.processor)
|
| 392 |
+
|
| 393 |
+
seq_len = data_dict["input_ids"][0].size(0)
|
| 394 |
+
# print(f"seq_length:{seq_len}")
|
| 395 |
+
grid_thw = data_dict.get("image_grid_thw")
|
| 396 |
+
if grid_thw is not None and not isinstance(grid_thw, (list, tuple)):
|
| 397 |
+
grid_thw = [grid_thw]
|
| 398 |
+
|
| 399 |
+
position_ids, _ = self.get_rope_index(
|
| 400 |
+
self.merge_size,
|
| 401 |
+
data_dict["input_ids"],
|
| 402 |
+
image_grid_thw=torch.cat(grid_thw, dim=0) if grid_thw else None,
|
| 403 |
+
video_grid_thw=None,
|
| 404 |
+
second_per_grid_ts=None,
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
data_dict["position_ids"] = position_ids
|
| 408 |
+
data_dict["attention_mask"] = [seq_len]
|
| 409 |
+
# sample_type: 0=total, 1=crit, 2=mcq (mcq skips regression loss)
|
| 410 |
+
if criterion == "__mcq__":
|
| 411 |
+
data_dict["sample_type"] = 2
|
| 412 |
+
elif criterion == "__total__":
|
| 413 |
+
data_dict["sample_type"] = 0
|
| 414 |
+
else:
|
| 415 |
+
data_dict["sample_type"] = 1
|
| 416 |
+
data_dict["gt_score"] = score
|
| 417 |
+
return data_dict
|
| 418 |
+
|
| 419 |
+
|
qwenvl/data/data_processor.py
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
import logging
|
| 4 |
+
import re
|
| 5 |
+
import time
|
| 6 |
+
import itertools
|
| 7 |
+
from dataclasses import dataclass, field
|
| 8 |
+
from typing import Dict, Optional, Sequence, List, Tuple, Any
|
| 9 |
+
from collections.abc import Sequence
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
from torch.utils.data import Dataset
|
| 15 |
+
|
| 16 |
+
import transformers
|
| 17 |
+
|
| 18 |
+
from . import data_list
|
| 19 |
+
from .rope2d import get_rope_index_25, get_rope_index_2, get_rope_index_3
|
| 20 |
+
|
| 21 |
+
IGNORE_INDEX = -100
|
| 22 |
+
IMAGE_TOKEN_INDEX = 151655
|
| 23 |
+
VIDEO_TOKEN_INDEX = 151656
|
| 24 |
+
DEFAULT_IMAGE_TOKEN = "<image>"
|
| 25 |
+
DEFAULT_VIDEO_TOKEN = "<video>"
|
| 26 |
+
|
| 27 |
+
local_rank = None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def rank0_print(*args):
|
| 31 |
+
if local_rank == 0:
|
| 32 |
+
print(*args)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def read_jsonl(path):
|
| 36 |
+
with open(path, "r") as f:
|
| 37 |
+
return [json.loads(line) for line in f]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _make_abs_paths(base: Path, files: str) -> str:
|
| 41 |
+
return f"{(base / files).resolve()}"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def update_processor_pixels(processor, data_args):
|
| 45 |
+
logger = logging.getLogger(__name__)
|
| 46 |
+
|
| 47 |
+
# --- Image Processor ---
|
| 48 |
+
ip = processor.image_processor
|
| 49 |
+
rank0_print("=== BEFORE IMAGE PROCESSOR PARAMETERS ===")
|
| 50 |
+
rank0_print(f"Image min_pixels: {getattr(ip, 'min_pixels', 'N/A')}")
|
| 51 |
+
rank0_print(f"Image max_pixels: {getattr(ip, 'max_pixels', 'N/A')}")
|
| 52 |
+
rank0_print(f"ip.size: {ip.size}")
|
| 53 |
+
rank0_print(f"Image size (shortest_edge): {ip.size.get('shortest_edge', 'N/A')}")
|
| 54 |
+
rank0_print(f"Image size (longest_edge): {ip.size.get('longest_edge', 'N/A')}")
|
| 55 |
+
|
| 56 |
+
if hasattr(ip, "min_pixels") and hasattr(ip, "max_pixels"):
|
| 57 |
+
ip.min_pixels = data_args.min_pixels
|
| 58 |
+
ip.max_pixels = data_args.max_pixels
|
| 59 |
+
rank0_print(f"β
Updated image_processor min_pixels to {data_args.min_pixels}")
|
| 60 |
+
rank0_print(f"β
Updated image_processor max_pixels to {data_args.max_pixels}")
|
| 61 |
+
|
| 62 |
+
if hasattr(ip, "size") and isinstance(ip.size, dict):
|
| 63 |
+
ip.size["shortest_edge"] = data_args.min_pixels
|
| 64 |
+
ip.size["longest_edge"] = data_args.max_pixels
|
| 65 |
+
rank0_print(
|
| 66 |
+
f"β
Updated image_processor size['shortest_edge'] to {data_args.min_pixels}"
|
| 67 |
+
)
|
| 68 |
+
rank0_print(
|
| 69 |
+
f"β
Updated image_processor size['longest_edge'] to {data_args.max_pixels}"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
rank0_print("=== AFTER IMAGE PROCESSOR PARAMETERS ===")
|
| 73 |
+
rank0_print(f"Image min_pixels: {getattr(ip, 'min_pixels', 'N/A')}")
|
| 74 |
+
rank0_print(f"Image max_pixels: {getattr(ip, 'max_pixels', 'N/A')}")
|
| 75 |
+
rank0_print(f"Image size (shortest_edge): {ip.size.get('shortest_edge', 'N/A')}")
|
| 76 |
+
rank0_print(f"Image size (longest_edge): {ip.size.get('longest_edge', 'N/A')}")
|
| 77 |
+
|
| 78 |
+
# --- Video Processor ---
|
| 79 |
+
if hasattr(processor, "video_processor") and processor.video_processor is not None:
|
| 80 |
+
vp = processor.video_processor
|
| 81 |
+
rank0_print("\n=== BEFORE VIDEO PROCESSOR PARAMETERS ===")
|
| 82 |
+
rank0_print(f"Video min_pixels: {getattr(vp, 'min_pixels', 'N/A')}")
|
| 83 |
+
rank0_print(f"Video max_pixels: {getattr(vp, 'max_pixels', 'N/A')}")
|
| 84 |
+
rank0_print(f"Video min_frames: {getattr(vp, 'min_frames', 'N/A')}")
|
| 85 |
+
rank0_print(f"Video max_frames: {getattr(vp, 'max_frames', 'N/A')}")
|
| 86 |
+
rank0_print(f"Video fps: {getattr(vp, 'fps', 'N/A')}")
|
| 87 |
+
rank0_print(
|
| 88 |
+
f"Video size (shortest_edge): {vp.size.get('shortest_edge', 'N/A')}"
|
| 89 |
+
)
|
| 90 |
+
rank0_print(f"Video size (longest_edge): {vp.size.get('longest_edge', 'N/A')}")
|
| 91 |
+
|
| 92 |
+
if hasattr(vp, "min_pixels") and hasattr(vp, "max_pixels"):
|
| 93 |
+
vp.min_pixels = data_args.video_min_pixels
|
| 94 |
+
vp.max_pixels = data_args.video_max_pixels
|
| 95 |
+
rank0_print(
|
| 96 |
+
f"β
Updated Qwen2-VL video_processor min_pixels to {data_args.video_min_pixels}"
|
| 97 |
+
)
|
| 98 |
+
rank0_print(
|
| 99 |
+
f"β
Updated Qwen2-VL video_processor max_pixels to {data_args.video_max_pixels}"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
if hasattr(vp, "min_frames") and hasattr(vp, "max_frames"):
|
| 103 |
+
vp.min_frames = data_args.video_min_frames
|
| 104 |
+
vp.max_frames = data_args.video_max_frames
|
| 105 |
+
rank0_print(
|
| 106 |
+
f"β
Updated video_processor min_frames to {data_args.video_min_frames}"
|
| 107 |
+
)
|
| 108 |
+
rank0_print(
|
| 109 |
+
f"β
Updated video_processor max_frames to {data_args.video_max_frames}"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if hasattr(vp, "fps"):
|
| 113 |
+
vp.fps = data_args.video_fps
|
| 114 |
+
rank0_print(f"β
Updated video_processor fps to {data_args.video_fps}")
|
| 115 |
+
|
| 116 |
+
if hasattr(vp, "size") and isinstance(vp.size, dict):
|
| 117 |
+
vp.size["shortest_edge"] = data_args.video_min_pixels
|
| 118 |
+
vp.size["longest_edge"] = data_args.video_max_pixels
|
| 119 |
+
rank0_print(
|
| 120 |
+
f"β
Updated Video size (shortest_edge): {vp.size.get('shortest_edge', 'N/A')}"
|
| 121 |
+
)
|
| 122 |
+
rank0_print(
|
| 123 |
+
f"β
Updated Video size (longest_edge): {vp.size.get('longest_edge', 'N/A')}"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
rank0_print("=== AFTER VIDEO PROCESSOR PARAMETERS ===")
|
| 127 |
+
rank0_print(f"Video min_pixels: {getattr(vp, 'min_pixels', 'N/A')}")
|
| 128 |
+
rank0_print(f"Video max_pixels: {getattr(vp, 'max_pixels', 'N/A')}")
|
| 129 |
+
rank0_print(f"Video min_frames: {getattr(vp, 'min_frames', 'N/A')}")
|
| 130 |
+
rank0_print(f"Video max_frames: {getattr(vp, 'max_frames', 'N/A')}")
|
| 131 |
+
rank0_print(f"Video fps: {getattr(vp, 'fps', 'N/A')}")
|
| 132 |
+
rank0_print(
|
| 133 |
+
f"Video size (shortest_edge): {vp.size.get('shortest_edge', 'N/A')}"
|
| 134 |
+
)
|
| 135 |
+
rank0_print(f"Video size (longest_edge): {vp.size.get('longest_edge', 'N/A')}")
|
| 136 |
+
|
| 137 |
+
return processor
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _build_messages(item: Dict[str, Any], base_path: Path) -> List[Dict[str, Any]]:
|
| 141 |
+
# Extract and normalize images and videos
|
| 142 |
+
images = item.get("image") or []
|
| 143 |
+
if isinstance(images, str):
|
| 144 |
+
images = [images]
|
| 145 |
+
|
| 146 |
+
videos = item.get("video") or []
|
| 147 |
+
if isinstance(videos, str):
|
| 148 |
+
videos = [videos]
|
| 149 |
+
|
| 150 |
+
# Build media pools with absolute paths
|
| 151 |
+
image_pool = [
|
| 152 |
+
{"type": "image", "image": _make_abs_paths(base_path, img)} for img in images
|
| 153 |
+
]
|
| 154 |
+
video_pool = [
|
| 155 |
+
{"type": "video", "video": _make_abs_paths(base_path, vid)} for vid in videos
|
| 156 |
+
]
|
| 157 |
+
|
| 158 |
+
messages = []
|
| 159 |
+
for turn in item["conversations"]:
|
| 160 |
+
role = "user" if turn["from"] == "human" else "assistant"
|
| 161 |
+
text: str = turn["value"]
|
| 162 |
+
|
| 163 |
+
if role == "user":
|
| 164 |
+
content = []
|
| 165 |
+
# Split text by <image> or <video> placeholders while keeping delimiters
|
| 166 |
+
text_parts = re.split(r"(<image>|<video>)", text)
|
| 167 |
+
|
| 168 |
+
for seg in text_parts:
|
| 169 |
+
if seg == "<image>":
|
| 170 |
+
if not image_pool:
|
| 171 |
+
raise ValueError(
|
| 172 |
+
"Number of <image> placeholders exceeds the number of provided images"
|
| 173 |
+
)
|
| 174 |
+
content.append(image_pool.pop(0))
|
| 175 |
+
elif seg == "<video>":
|
| 176 |
+
if not video_pool:
|
| 177 |
+
raise ValueError(
|
| 178 |
+
"Number of <video> placeholders exceeds the number of provided videos"
|
| 179 |
+
)
|
| 180 |
+
content.append(video_pool.pop(0))
|
| 181 |
+
elif seg.strip():
|
| 182 |
+
content.append({"type": "text", "text": seg.strip()})
|
| 183 |
+
|
| 184 |
+
messages.append({"role": role, "content": content})
|
| 185 |
+
else:
|
| 186 |
+
# Assistant messages contain only text
|
| 187 |
+
messages.append({"role": role, "content": [{"type": "text", "text": text}]})
|
| 188 |
+
|
| 189 |
+
# Check for unused media files
|
| 190 |
+
if image_pool:
|
| 191 |
+
raise ValueError(
|
| 192 |
+
f"{len(image_pool)} image(s) remain unused (not consumed by placeholders)"
|
| 193 |
+
)
|
| 194 |
+
if video_pool:
|
| 195 |
+
raise ValueError(
|
| 196 |
+
f"{len(video_pool)} video(s) remain unused (not consumed by placeholders)"
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
return messages
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def preprocess_qwen_visual(
|
| 203 |
+
sources,
|
| 204 |
+
processor,
|
| 205 |
+
) -> Dict:
|
| 206 |
+
if len(sources) != 1:
|
| 207 |
+
raise ValueError(f"Expected 1 source, got {len(sources)}")
|
| 208 |
+
|
| 209 |
+
source = sources[0]
|
| 210 |
+
base_path = Path(source.get("data_path", ""))
|
| 211 |
+
messages = _build_messages(source, base_path)
|
| 212 |
+
|
| 213 |
+
full_result = processor.apply_chat_template(
|
| 214 |
+
messages, tokenize=True, return_dict=True, return_tensors="pt"
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
input_ids = full_result["input_ids"]
|
| 218 |
+
if isinstance(input_ids, list):
|
| 219 |
+
input_ids = torch.tensor(input_ids).unsqueeze(0)
|
| 220 |
+
|
| 221 |
+
labels = torch.full_like(input_ids, IGNORE_INDEX)
|
| 222 |
+
|
| 223 |
+
input_ids_flat = input_ids[0].tolist()
|
| 224 |
+
L = len(input_ids_flat)
|
| 225 |
+
pos = 0
|
| 226 |
+
while pos < L:
|
| 227 |
+
if input_ids_flat[pos] == 77091:
|
| 228 |
+
ans_start = pos + 2
|
| 229 |
+
ans_end = ans_start
|
| 230 |
+
while ans_end < L and input_ids_flat[ans_end] != 151645:
|
| 231 |
+
ans_end += 1
|
| 232 |
+
if ans_end < L:
|
| 233 |
+
labels[0, ans_start : ans_end + 2] = input_ids[
|
| 234 |
+
0, ans_start : ans_end + 2
|
| 235 |
+
]
|
| 236 |
+
pos = ans_end
|
| 237 |
+
pos += 1
|
| 238 |
+
|
| 239 |
+
full_result["labels"] = labels
|
| 240 |
+
full_result["input_ids"] = input_ids
|
| 241 |
+
return full_result
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
class LazySupervisedDataset(Dataset):
|
| 245 |
+
"""Dataset for supervised fine-tuning."""
|
| 246 |
+
|
| 247 |
+
def __init__(self, processor, data_args):
|
| 248 |
+
super(LazySupervisedDataset, self).__init__()
|
| 249 |
+
|
| 250 |
+
dataset = data_args.dataset_use.split(",")
|
| 251 |
+
dataset_list = data_list(dataset)
|
| 252 |
+
rank0_print(f"Loading datasets: {dataset_list}")
|
| 253 |
+
self.video_max_total_pixels = getattr(
|
| 254 |
+
data_args, "video_max_total_pixels", 1664 * 28 * 28
|
| 255 |
+
)
|
| 256 |
+
self.video_min_total_pixels = getattr(
|
| 257 |
+
data_args, "video_min_total_pixels", 256 * 28 * 28
|
| 258 |
+
)
|
| 259 |
+
self.model_type = data_args.model_type
|
| 260 |
+
if data_args.model_type == "qwen3vl":
|
| 261 |
+
self.get_rope_index = get_rope_index_3
|
| 262 |
+
elif data_args.model_type == "qwen2.5vl":
|
| 263 |
+
self.get_rope_index = get_rope_index_25
|
| 264 |
+
elif data_args.model_type == "qwen2vl":
|
| 265 |
+
self.get_rope_index = get_rope_index_2
|
| 266 |
+
else:
|
| 267 |
+
raise ValueError(f"model_type: {data_args.model_type} not supported")
|
| 268 |
+
|
| 269 |
+
list_data_dict = []
|
| 270 |
+
|
| 271 |
+
for data in dataset_list:
|
| 272 |
+
file_format = data["annotation_path"].split(".")[-1]
|
| 273 |
+
if file_format == "jsonl":
|
| 274 |
+
annotations = read_jsonl(data["annotation_path"])
|
| 275 |
+
else:
|
| 276 |
+
annotations = json.load(open(data["annotation_path"], "r"))
|
| 277 |
+
sampling_rate = data.get("sampling_rate", 1.0)
|
| 278 |
+
if sampling_rate < 1.0:
|
| 279 |
+
annotations = random.sample(
|
| 280 |
+
annotations, int(len(annotations) * sampling_rate)
|
| 281 |
+
)
|
| 282 |
+
rank0_print(f"sampling {len(annotations)} examples from dataset {data}")
|
| 283 |
+
else:
|
| 284 |
+
rank0_print(f"dataset name: {data}")
|
| 285 |
+
for ann in annotations:
|
| 286 |
+
if isinstance(ann, list):
|
| 287 |
+
for sub_ann in ann:
|
| 288 |
+
sub_ann["data_path"] = data["data_path"]
|
| 289 |
+
else:
|
| 290 |
+
ann["data_path"] = data["data_path"]
|
| 291 |
+
list_data_dict += annotations
|
| 292 |
+
|
| 293 |
+
rank0_print(f"Total training samples: {len(list_data_dict)}")
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
rank0_print("Formatting inputs...Skip in lazy mode")
|
| 297 |
+
processor = update_processor_pixels(processor, data_args)
|
| 298 |
+
self.processor = processor
|
| 299 |
+
self.tokenizer = processor.tokenizer
|
| 300 |
+
self.data_args = data_args
|
| 301 |
+
self.merge_size = getattr(processor.image_processor, "merge_size", 2)
|
| 302 |
+
self.list_data_dict = list_data_dict
|
| 303 |
+
|
| 304 |
+
if data_args.data_packing:
|
| 305 |
+
self.item_fn = self._get_packed_item
|
| 306 |
+
else:
|
| 307 |
+
self.item_fn = self._get_item
|
| 308 |
+
|
| 309 |
+
def __len__(self):
|
| 310 |
+
return len(self.list_data_dict)
|
| 311 |
+
|
| 312 |
+
@property
|
| 313 |
+
def lengths(self):
|
| 314 |
+
length_list = []
|
| 315 |
+
for sample in self.list_data_dict:
|
| 316 |
+
img_tokens = 128 if "image" in sample else 0
|
| 317 |
+
length_list.append(
|
| 318 |
+
sum(len(conv["value"].split()) for conv in sample["conversations"])
|
| 319 |
+
+ img_tokens
|
| 320 |
+
)
|
| 321 |
+
return length_list
|
| 322 |
+
|
| 323 |
+
@property
|
| 324 |
+
def modality_lengths(self):
|
| 325 |
+
length_list = []
|
| 326 |
+
for sample in self.list_data_dict:
|
| 327 |
+
cur_len = sum(
|
| 328 |
+
len(conv["value"].split()) for conv in sample["conversations"]
|
| 329 |
+
)
|
| 330 |
+
cur_len = (
|
| 331 |
+
cur_len if ("image" in sample) or ("video" in sample) else -cur_len
|
| 332 |
+
)
|
| 333 |
+
length_list.append(cur_len)
|
| 334 |
+
return length_list
|
| 335 |
+
|
| 336 |
+
@property
|
| 337 |
+
def pre_calculated_length(self):
|
| 338 |
+
if "num_tokens" in self.list_data_dict[0]:
|
| 339 |
+
length_list = [sample["num_tokens"] for sample in self.list_data_dict]
|
| 340 |
+
return np.array(length_list)
|
| 341 |
+
else:
|
| 342 |
+
print("No pre-calculated length available.")
|
| 343 |
+
return np.array([1] * len(self.list_data_dict))
|
| 344 |
+
|
| 345 |
+
def __getitem__(self, i) -> Dict[str, torch.Tensor]:
|
| 346 |
+
num_base_retries = 3
|
| 347 |
+
num_final_retries = 30
|
| 348 |
+
|
| 349 |
+
# try the current sample first
|
| 350 |
+
for attempt_idx in range(num_base_retries):
|
| 351 |
+
try:
|
| 352 |
+
sources = self.list_data_dict[i]
|
| 353 |
+
if isinstance(sources, dict):
|
| 354 |
+
sources = [sources]
|
| 355 |
+
sample = self.item_fn(sources)
|
| 356 |
+
return sample
|
| 357 |
+
except Exception as e:
|
| 358 |
+
# sleep 1s in case it is a cloud disk issue
|
| 359 |
+
print(f"[Try #{attempt_idx}] Failed to fetch sample {i}. Exception:", e)
|
| 360 |
+
time.sleep(1)
|
| 361 |
+
|
| 362 |
+
# try other samples, in case it is file corruption issue
|
| 363 |
+
for attempt_idx in range(num_base_retries):
|
| 364 |
+
try:
|
| 365 |
+
next_index = min(i + 1, len(self.list_data_dict) - 1)
|
| 366 |
+
sources = self.list_data_dict[next_index]
|
| 367 |
+
if isinstance(sources, dict):
|
| 368 |
+
sources = [sources]
|
| 369 |
+
|
| 370 |
+
sample = self.item_fn(sources)
|
| 371 |
+
return sample
|
| 372 |
+
except Exception as e:
|
| 373 |
+
# no need to sleep
|
| 374 |
+
print(
|
| 375 |
+
f"[Try other #{attempt_idx}] Failed to fetch sample {next_index}. Exception:",
|
| 376 |
+
e,
|
| 377 |
+
)
|
| 378 |
+
pass
|
| 379 |
+
|
| 380 |
+
try:
|
| 381 |
+
sources = self.list_data_dict[i]
|
| 382 |
+
if isinstance(sources, dict):
|
| 383 |
+
sources = [sources]
|
| 384 |
+
sample = self.item_fn(sources)
|
| 385 |
+
return sample
|
| 386 |
+
except Exception as e:
|
| 387 |
+
raise e
|
| 388 |
+
|
| 389 |
+
def _get_item(self, sources) -> Dict[str, torch.Tensor]:
|
| 390 |
+
data_dict = preprocess_qwen_visual(
|
| 391 |
+
sources,
|
| 392 |
+
self.processor,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
seq_len = data_dict["input_ids"][0].size(0)
|
| 396 |
+
|
| 397 |
+
if "image_grid_thw" in data_dict:
|
| 398 |
+
grid_thw = data_dict.get("image_grid_thw")
|
| 399 |
+
if not isinstance(grid_thw, Sequence):
|
| 400 |
+
grid_thw = [grid_thw]
|
| 401 |
+
else:
|
| 402 |
+
grid_thw = None
|
| 403 |
+
|
| 404 |
+
if "video_grid_thw" in data_dict:
|
| 405 |
+
video_grid_thw = data_dict.get("video_grid_thw")
|
| 406 |
+
if not isinstance(video_grid_thw, Sequence):
|
| 407 |
+
video_grid_thw = [video_grid_thw]
|
| 408 |
+
second_per_grid_ts = [
|
| 409 |
+
self.processor.video_processor.temporal_patch_size
|
| 410 |
+
/ self.processor.video_processor.fps
|
| 411 |
+
] * len(video_grid_thw)
|
| 412 |
+
else:
|
| 413 |
+
video_grid_thw = None
|
| 414 |
+
second_per_grid_ts = None
|
| 415 |
+
|
| 416 |
+
position_ids, _ = self.get_rope_index(
|
| 417 |
+
self.merge_size,
|
| 418 |
+
data_dict["input_ids"],
|
| 419 |
+
image_grid_thw=torch.cat(grid_thw, dim=0) if grid_thw else None,
|
| 420 |
+
video_grid_thw=(
|
| 421 |
+
torch.cat(video_grid_thw, dim=0) if video_grid_thw else None
|
| 422 |
+
),
|
| 423 |
+
second_per_grid_ts=second_per_grid_ts if second_per_grid_ts else None,
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
data_dict["position_ids"] = position_ids
|
| 427 |
+
data_dict["attention_mask"] = [seq_len]
|
| 428 |
+
|
| 429 |
+
text = self.processor.tokenizer.decode(
|
| 430 |
+
data_dict["input_ids"][0], skip_special_tokens=False
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
labels = data_dict["labels"][0]
|
| 434 |
+
labels = [
|
| 435 |
+
tid if tid != -100 else self.processor.tokenizer.pad_token_id
|
| 436 |
+
for tid in labels
|
| 437 |
+
]
|
| 438 |
+
label = self.processor.tokenizer.decode(labels, skip_special_tokens=False)
|
| 439 |
+
|
| 440 |
+
return data_dict
|
| 441 |
+
|
| 442 |
+
def _get_packed_item(self, sources) -> Dict[str, torch.Tensor]:
|
| 443 |
+
|
| 444 |
+
if isinstance(sources, dict):
|
| 445 |
+
if isinstance(source, dict):
|
| 446 |
+
sources = [sources]
|
| 447 |
+
assert len(sources) == 1, "Don't know why it is wrapped to a list" # FIXME
|
| 448 |
+
return self._get_item(sources)
|
| 449 |
+
|
| 450 |
+
if isinstance(sources, list):
|
| 451 |
+
data_list = []
|
| 452 |
+
new_data_dict = {}
|
| 453 |
+
for source in sources:
|
| 454 |
+
if isinstance(source, dict):
|
| 455 |
+
source = [source]
|
| 456 |
+
assert (
|
| 457 |
+
len(source) == 1
|
| 458 |
+
), f"Don't know why it is wrapped to a list.\n {source}" # FIXME
|
| 459 |
+
data_list.append(self._get_item(source))
|
| 460 |
+
|
| 461 |
+
input_ids = torch.cat([d["input_ids"] for d in data_list], dim=1)
|
| 462 |
+
labels = torch.cat([d["labels"] for d in data_list], dim=1)
|
| 463 |
+
position_ids = torch.cat([d["position_ids"] for d in data_list], dim=2)
|
| 464 |
+
attention_mask = [
|
| 465 |
+
d["attention_mask"][0] for d in data_list if "attention_mask" in d
|
| 466 |
+
]
|
| 467 |
+
new_data_dict = {
|
| 468 |
+
"input_ids": input_ids,
|
| 469 |
+
"labels": labels,
|
| 470 |
+
"position_ids": position_ids,
|
| 471 |
+
"attention_mask": attention_mask if attention_mask else None,
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
if any("pixel_values" in d for d in data_list):
|
| 475 |
+
new_data_dict.update(
|
| 476 |
+
{
|
| 477 |
+
"pixel_values": torch.cat(
|
| 478 |
+
[
|
| 479 |
+
d["pixel_values"]
|
| 480 |
+
for d in data_list
|
| 481 |
+
if "pixel_values" in d
|
| 482 |
+
],
|
| 483 |
+
dim=0,
|
| 484 |
+
),
|
| 485 |
+
"image_grid_thw": torch.cat(
|
| 486 |
+
[
|
| 487 |
+
d["image_grid_thw"]
|
| 488 |
+
for d in data_list
|
| 489 |
+
if "image_grid_thw" in d
|
| 490 |
+
],
|
| 491 |
+
dim=0,
|
| 492 |
+
),
|
| 493 |
+
}
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
if any("pixel_values_videos" in d for d in data_list):
|
| 497 |
+
new_data_dict.update(
|
| 498 |
+
{
|
| 499 |
+
"pixel_values_videos": torch.cat(
|
| 500 |
+
[
|
| 501 |
+
d["pixel_values_videos"]
|
| 502 |
+
for d in data_list
|
| 503 |
+
if "pixel_values_videos" in d
|
| 504 |
+
],
|
| 505 |
+
dim=0,
|
| 506 |
+
),
|
| 507 |
+
"video_grid_thw": torch.cat(
|
| 508 |
+
[
|
| 509 |
+
d["video_grid_thw"]
|
| 510 |
+
for d in data_list
|
| 511 |
+
if "video_grid_thw" in d
|
| 512 |
+
],
|
| 513 |
+
dim=0,
|
| 514 |
+
),
|
| 515 |
+
}
|
| 516 |
+
)
|
| 517 |
+
return new_data_dict
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def pad_and_cat(tensor_list):
|
| 521 |
+
max_length = max(tensor.shape[2] for tensor in tensor_list)
|
| 522 |
+
|
| 523 |
+
padded_tensors = []
|
| 524 |
+
for tensor in tensor_list:
|
| 525 |
+
pad_length = max_length - tensor.shape[2]
|
| 526 |
+
padded_tensor = torch.nn.functional.pad(tensor, (0, pad_length), "constant", 1)
|
| 527 |
+
padded_tensors.append(padded_tensor)
|
| 528 |
+
|
| 529 |
+
stacked_tensor = torch.cat(padded_tensors, dim=1)
|
| 530 |
+
|
| 531 |
+
return stacked_tensor
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
@dataclass
|
| 535 |
+
class DataCollatorForSupervisedDataset(object):
|
| 536 |
+
"""Collate examples for supervised fine-tuning."""
|
| 537 |
+
|
| 538 |
+
tokenizer: transformers.PreTrainedTokenizer
|
| 539 |
+
|
| 540 |
+
def __call__(self, instances: Sequence[Dict]) -> Dict[str, torch.Tensor]:
|
| 541 |
+
input_ids, labels, position_ids = tuple(
|
| 542 |
+
[instance[key] for instance in instances]
|
| 543 |
+
for key in ("input_ids", "labels", "position_ids")
|
| 544 |
+
)
|
| 545 |
+
input_ids = [ids.squeeze(0) for ids in input_ids]
|
| 546 |
+
labels = [ids.squeeze(0) for ids in labels]
|
| 547 |
+
input_ids = torch.nn.utils.rnn.pad_sequence(
|
| 548 |
+
input_ids, batch_first=True, padding_value=self.tokenizer.pad_token_id
|
| 549 |
+
)
|
| 550 |
+
labels = torch.nn.utils.rnn.pad_sequence(
|
| 551 |
+
labels, batch_first=True, padding_value=IGNORE_INDEX
|
| 552 |
+
)
|
| 553 |
+
position_ids = pad_and_cat(position_ids)
|
| 554 |
+
input_ids = input_ids[:, : self.tokenizer.model_max_length]
|
| 555 |
+
labels = labels[:, : self.tokenizer.model_max_length]
|
| 556 |
+
position_ids = position_ids[:, :, : self.tokenizer.model_max_length]
|
| 557 |
+
batch = dict(
|
| 558 |
+
input_ids=input_ids,
|
| 559 |
+
labels=labels,
|
| 560 |
+
attention_mask=input_ids.ne(self.tokenizer.pad_token_id),
|
| 561 |
+
)
|
| 562 |
+
images = list(
|
| 563 |
+
instance["pixel_values"]
|
| 564 |
+
for instance in instances
|
| 565 |
+
if "pixel_values" in instance
|
| 566 |
+
)
|
| 567 |
+
videos = list(
|
| 568 |
+
instance["pixel_values_videos"]
|
| 569 |
+
for instance in instances
|
| 570 |
+
if "pixel_values_videos" in instance
|
| 571 |
+
)
|
| 572 |
+
if len(images) != 0:
|
| 573 |
+
concat_images = torch.cat([image for image in images], dim=0)
|
| 574 |
+
grid_thw = [
|
| 575 |
+
instance["image_grid_thw"]
|
| 576 |
+
for instance in instances
|
| 577 |
+
if "image_grid_thw" in instance
|
| 578 |
+
]
|
| 579 |
+
grid_thw = torch.cat(grid_thw, dim=0)
|
| 580 |
+
else:
|
| 581 |
+
concat_images = None
|
| 582 |
+
grid_thw = None
|
| 583 |
+
|
| 584 |
+
if len(videos) != 0:
|
| 585 |
+
concat_videos = torch.cat([video for video in videos], dim=0)
|
| 586 |
+
video_grid_thw = [
|
| 587 |
+
instance["video_grid_thw"]
|
| 588 |
+
for instance in instances
|
| 589 |
+
if "video_grid_thw" in instance
|
| 590 |
+
]
|
| 591 |
+
video_grid_thw = torch.cat(video_grid_thw, dim=0)
|
| 592 |
+
else:
|
| 593 |
+
concat_videos = None
|
| 594 |
+
video_grid_thw = None
|
| 595 |
+
|
| 596 |
+
batch["pixel_values"] = concat_images
|
| 597 |
+
batch["image_grid_thw"] = grid_thw
|
| 598 |
+
batch["pixel_values_videos"] = concat_videos
|
| 599 |
+
batch["video_grid_thw"] = video_grid_thw
|
| 600 |
+
batch["position_ids"] = position_ids
|
| 601 |
+
return batch
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
@dataclass
|
| 605 |
+
class FlattenedDataCollatorForSupervisedDataset(DataCollatorForSupervisedDataset):
|
| 606 |
+
"""Collate examples into packed sequence with multi-modal support."""
|
| 607 |
+
|
| 608 |
+
tokenizer: transformers.PreTrainedTokenizer
|
| 609 |
+
|
| 610 |
+
def __call__(self, instances: Sequence[Dict]) -> Dict[str, torch.Tensor]:
|
| 611 |
+
input_ids, labels, position_ids, attention_mask = tuple(
|
| 612 |
+
[instance[key] for instance in instances]
|
| 613 |
+
for key in ("input_ids", "labels", "position_ids", "attention_mask")
|
| 614 |
+
)
|
| 615 |
+
attention_mask = list(
|
| 616 |
+
itertools.chain(
|
| 617 |
+
*(
|
| 618 |
+
instance["attention_mask"]
|
| 619 |
+
for instance in instances
|
| 620 |
+
if "attention_mask" in instance
|
| 621 |
+
)
|
| 622 |
+
)
|
| 623 |
+
)
|
| 624 |
+
seq_lens = torch.tensor([0] + attention_mask, dtype=torch.int32)
|
| 625 |
+
cumsum_seq_lens = torch.cumsum(seq_lens, dim=0, dtype=torch.int32)
|
| 626 |
+
input_ids = torch.cat(input_ids, dim=1)
|
| 627 |
+
labels = torch.cat(labels, dim=1)
|
| 628 |
+
position_ids = torch.cat(position_ids, dim=2)
|
| 629 |
+
|
| 630 |
+
batch = dict(
|
| 631 |
+
input_ids=input_ids,
|
| 632 |
+
labels=labels,
|
| 633 |
+
attention_mask=cumsum_seq_lens,
|
| 634 |
+
position_ids=position_ids,
|
| 635 |
+
)
|
| 636 |
+
images = list(
|
| 637 |
+
instance["pixel_values"]
|
| 638 |
+
for instance in instances
|
| 639 |
+
if "pixel_values" in instance
|
| 640 |
+
)
|
| 641 |
+
videos = list(
|
| 642 |
+
instance["pixel_values_videos"]
|
| 643 |
+
for instance in instances
|
| 644 |
+
if "pixel_values_videos" in instance
|
| 645 |
+
)
|
| 646 |
+
if len(images) != 0:
|
| 647 |
+
concat_images = torch.cat([image for image in images], dim=0)
|
| 648 |
+
grid_thw = [
|
| 649 |
+
instance["image_grid_thw"]
|
| 650 |
+
for instance in instances
|
| 651 |
+
if "image_grid_thw" in instance
|
| 652 |
+
]
|
| 653 |
+
grid_thw = torch.cat(grid_thw, dim=0)
|
| 654 |
+
else:
|
| 655 |
+
concat_images = None
|
| 656 |
+
grid_thw = None
|
| 657 |
+
|
| 658 |
+
if len(videos) != 0:
|
| 659 |
+
concat_videos = torch.cat([video for video in videos], dim=0)
|
| 660 |
+
video_grid_thw = [
|
| 661 |
+
instance["video_grid_thw"]
|
| 662 |
+
for instance in instances
|
| 663 |
+
if "video_grid_thw" in instance
|
| 664 |
+
]
|
| 665 |
+
video_grid_thw = torch.cat(video_grid_thw, dim=0)
|
| 666 |
+
else:
|
| 667 |
+
concat_videos = None
|
| 668 |
+
video_grid_thw = None
|
| 669 |
+
|
| 670 |
+
batch["pixel_values"] = concat_images
|
| 671 |
+
batch["image_grid_thw"] = grid_thw
|
| 672 |
+
batch["pixel_values_videos"] = concat_videos
|
| 673 |
+
batch["video_grid_thw"] = video_grid_thw
|
| 674 |
+
|
| 675 |
+
return batch
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
def make_supervised_data_module(processor, data_args) -> Dict:
|
| 679 |
+
"""Make dataset and collator for supervised fine-tuning."""
|
| 680 |
+
train_dataset = LazySupervisedDataset(processor, data_args=data_args)
|
| 681 |
+
if data_args.data_flatten or data_args.data_packing:
|
| 682 |
+
data_collator = FlattenedDataCollatorForSupervisedDataset(processor.tokenizer)
|
| 683 |
+
return dict(
|
| 684 |
+
train_dataset=train_dataset, eval_dataset=None, data_collator=data_collator
|
| 685 |
+
)
|
| 686 |
+
data_collator = DataCollatorForSupervisedDataset(processor.tokenizer)
|
| 687 |
+
return dict(
|
| 688 |
+
train_dataset=train_dataset, eval_dataset=None, data_collator=data_collator
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
+
|
| 692 |
+
if __name__ == "__main__":
|
| 693 |
+
pass
|
qwenvl/train/__init__.py
ADDED
|
File without changes
|
qwenvl/train/argument.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import transformers
|
| 2 |
+
from dataclasses import dataclass, field
|
| 3 |
+
from typing import Dict, Optional, Sequence, List
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class ModelArguments:
|
| 8 |
+
model_name_or_path: Optional[str] = field(default="Qwen/Qwen2.5-VL-3B-Instruct")
|
| 9 |
+
tune_mm_llm: bool = field(default=False)
|
| 10 |
+
tune_mm_mlp: bool = field(default=False)
|
| 11 |
+
tune_mm_vision: bool = field(default=False)
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class DataArguments:
|
| 15 |
+
dataset_use: str = field(default="")
|
| 16 |
+
data_flatten: bool = field(default=False)
|
| 17 |
+
data_packing: bool = field(default=False)
|
| 18 |
+
base_interval: int = field(default=2)
|
| 19 |
+
max_pixels: int = field(default=28 * 28 * 576)
|
| 20 |
+
min_pixels: int = field(default=28 * 28 * 16)
|
| 21 |
+
video_max_frames: Optional[int] = field(default=8)
|
| 22 |
+
video_min_frames: Optional[int] = field(default=4)
|
| 23 |
+
video_max_pixels: int = field(default=1024 * 28 * 28)
|
| 24 |
+
video_min_pixels: int = field(default=256 * 28 * 28)
|
| 25 |
+
video_fps: float = 2
|
| 26 |
+
# Aesthetic scoring
|
| 27 |
+
use_score_tokens: bool = field(default=False,
|
| 28 |
+
metadata={"help": "Enable custom score tokens for aesthetic scoring tasks."})
|
| 29 |
+
use_existing_vocab_tokens: bool = field(default=False,
|
| 30 |
+
metadata={"help": "Use existing vocab tokens (aa, ab, ...) as score bins instead of adding new tokens."})
|
| 31 |
+
use_placeholder: bool = field(default=True,
|
| 32 |
+
metadata={"help": "Use placeholder tokens for score regression. If False, use direct score bin tokens."})
|
| 33 |
+
crit_score_bins: int = field(default=20,
|
| 34 |
+
metadata={"help": "Number of bins for criteria scores (1-10). Tokens: <score1>..<scoreN>."})
|
| 35 |
+
total_score_bins: int = field(default=101,
|
| 36 |
+
metadata={"help": "Number of bins for total score (0-100). Tokens: <total1>..<totalN>."})
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class TrainingArguments(transformers.TrainingArguments):
|
| 41 |
+
cache_dir: Optional[str] = field(default=None)
|
| 42 |
+
optim: str = field(default="adamw_torch")
|
| 43 |
+
model_max_length: int = field(
|
| 44 |
+
default=512,
|
| 45 |
+
metadata={
|
| 46 |
+
"help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."
|
| 47 |
+
},
|
| 48 |
+
)
|
| 49 |
+
mm_projector_lr: Optional[float] = None
|
| 50 |
+
vision_tower_lr: Optional[float] = None
|
| 51 |
+
|
| 52 |
+
## Lora config
|
| 53 |
+
lora_enable: bool = field(default=False)
|
| 54 |
+
lora_r: int = field(default=64)
|
| 55 |
+
lora_alpha: int = field(default=128)
|
| 56 |
+
lora_dropout: float = field(default=0.0)
|
| 57 |
+
|
| 58 |
+
## Local logging
|
| 59 |
+
sample_every_steps: int = field(default=500,
|
| 60 |
+
metadata={"help": "Run model generation samples every N steps and save to output_dir/model_output_samples.txt."})
|
| 61 |
+
|
| 62 |
+
## Score loss config
|
| 63 |
+
score_ce_weight: float = field(default=1.0,
|
| 64 |
+
metadata={"help": "Weight for CE loss in aesthetic scoring."})
|
| 65 |
+
crit_reg_weight: float = field(default=0.5,
|
| 66 |
+
metadata={"help": "Weight for criteria (1-10) regression loss."})
|
| 67 |
+
total_reg_weight: float = field(default=0.5,
|
| 68 |
+
metadata={"help": "Weight for total-score (0-100) regression loss."})
|
| 69 |
+
score_regression_loss: str = field(default="huber",
|
| 70 |
+
metadata={"help": "Regression loss type: 'mse' or 'huber'."})
|
qwenvl/train/logging_callback.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Local training logger callback.
|
| 3 |
+
|
| 4 |
+
Writes two files to output_dir:
|
| 5 |
+
- train_metrics.jsonl : one JSON line per logging step with loss / grad_norm / lr / step
|
| 6 |
+
- model_output_samples.txt : periodic model generation samples showing <think> + score token
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import csv
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import time
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
from transformers import TrainerCallback, TrainerControl, TrainerState, TrainingArguments
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class LocalLoggingCallback(TrainerCallback):
|
| 21 |
+
"""
|
| 22 |
+
Args:
|
| 23 |
+
processor: AutoProcessor (for generation + decoding).
|
| 24 |
+
sample_dataset: A small list of dicts with keys image_path / question / gt_score,
|
| 25 |
+
used for periodic generation samples. Pass None to skip.
|
| 26 |
+
sample_every_steps: How often (in steps) to run generation samples.
|
| 27 |
+
output_dir: Where to write log files (defaults to training_args.output_dir).
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(
|
| 31 |
+
self,
|
| 32 |
+
processor,
|
| 33 |
+
sample_dataset=None,
|
| 34 |
+
sample_every_steps: int = 500,
|
| 35 |
+
output_dir: Optional[str] = None,
|
| 36 |
+
):
|
| 37 |
+
self.processor = processor
|
| 38 |
+
self.sample_dataset = sample_dataset or []
|
| 39 |
+
self.sample_every_steps = sample_every_steps
|
| 40 |
+
self._output_dir = output_dir
|
| 41 |
+
self._metrics_path = None
|
| 42 |
+
self._samples_path = None
|
| 43 |
+
self._csv_writer = None
|
| 44 |
+
self._csv_file = None
|
| 45 |
+
|
| 46 |
+
# ββ setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
|
| 48 |
+
def on_train_begin(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, **kwargs):
|
| 49 |
+
out = Path(self._output_dir or args.output_dir)
|
| 50 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
self._metrics_path = out / "train_metrics.jsonl"
|
| 52 |
+
self._samples_path = out / "model_output_samples.txt"
|
| 53 |
+
if args.local_rank in (-1, 0):
|
| 54 |
+
with open(self._samples_path, "a") as f:
|
| 55 |
+
f.write(f"\n{'='*60}\nTraining started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n{'='*60}\n")
|
| 56 |
+
|
| 57 |
+
# ββ metrics logging βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
|
| 59 |
+
def on_log(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, logs=None, **kwargs):
|
| 60 |
+
if args.local_rank not in (-1, 0):
|
| 61 |
+
return
|
| 62 |
+
if logs is None:
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
record = {
|
| 66 |
+
"step": state.global_step,
|
| 67 |
+
"epoch": round(state.epoch or 0, 4),
|
| 68 |
+
"loss": logs.get("loss"),
|
| 69 |
+
"grad_norm": logs.get("grad_norm"),
|
| 70 |
+
"lr": logs.get("learning_rate"),
|
| 71 |
+
"wall_time": time.time(),
|
| 72 |
+
}
|
| 73 |
+
record = {k: v for k, v in record.items() if v is not None}
|
| 74 |
+
|
| 75 |
+
with open(self._metrics_path, "a") as f:
|
| 76 |
+
f.write(json.dumps(record) + "\n")
|
| 77 |
+
|
| 78 |
+
# ββ generation samples ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 79 |
+
|
| 80 |
+
def on_step_end(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, model=None, **kwargs):
|
| 81 |
+
if args.local_rank not in (-1, 0):
|
| 82 |
+
return
|
| 83 |
+
if not self.sample_dataset:
|
| 84 |
+
return
|
| 85 |
+
if state.global_step % self.sample_every_steps != 0:
|
| 86 |
+
return
|
| 87 |
+
self._run_samples(model, state.global_step, args)
|
| 88 |
+
|
| 89 |
+
def on_train_end(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, model=None, **kwargs):
|
| 90 |
+
if args.local_rank not in (-1, 0):
|
| 91 |
+
return
|
| 92 |
+
if self.sample_dataset and model is not None:
|
| 93 |
+
self._run_samples(model, state.global_step, args, label="[FINAL]")
|
| 94 |
+
|
| 95 |
+
def _run_samples(self, model, step: int, args: TrainingArguments, label: str = ""):
|
| 96 |
+
model.eval()
|
| 97 |
+
lines = [f"\n--- Step {step} {label} {time.strftime('%H:%M:%S')} ---\n"]
|
| 98 |
+
|
| 99 |
+
with torch.no_grad():
|
| 100 |
+
for item in self.sample_dataset:
|
| 101 |
+
image_path = item["image_path"]
|
| 102 |
+
question = item["question"]
|
| 103 |
+
gt_score = item.get("gt_score", "?")
|
| 104 |
+
|
| 105 |
+
messages = [
|
| 106 |
+
{
|
| 107 |
+
"role": "user",
|
| 108 |
+
"content": [
|
| 109 |
+
{"type": "image", "image": image_path},
|
| 110 |
+
{"type": "text", "text": question},
|
| 111 |
+
],
|
| 112 |
+
}
|
| 113 |
+
]
|
| 114 |
+
try:
|
| 115 |
+
inputs = self.processor.apply_chat_template(
|
| 116 |
+
messages, tokenize=True, return_dict=True,
|
| 117 |
+
return_tensors="pt", add_generation_prompt=True,
|
| 118 |
+
)
|
| 119 |
+
inputs = {k: v.to(model.device) if hasattr(v, "to") else v
|
| 120 |
+
for k, v in inputs.items()}
|
| 121 |
+
|
| 122 |
+
out_ids = model.generate(
|
| 123 |
+
**inputs,
|
| 124 |
+
max_new_tokens=300,
|
| 125 |
+
do_sample=False,
|
| 126 |
+
)
|
| 127 |
+
# decode only the generated tokens
|
| 128 |
+
generated = out_ids[0][inputs["input_ids"].shape[1]:]
|
| 129 |
+
text = self.processor.tokenizer.decode(generated, skip_special_tokens=False)
|
| 130 |
+
|
| 131 |
+
lines.append(f"[Q] {question[:80]}")
|
| 132 |
+
lines.append(f"[GT score] {gt_score}")
|
| 133 |
+
lines.append(f"[Model output]\n{text}\n")
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
lines.append(f"[ERROR] {e}\n")
|
| 137 |
+
|
| 138 |
+
with open(self._samples_path, "a") as f:
|
| 139 |
+
f.write("\n".join(lines) + "\n")
|
| 140 |
+
|
| 141 |
+
model.train()
|
qwenvl/train/score_loss.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Score loss for aesthetic scoring tasks.
|
| 3 |
+
|
| 4 |
+
Total loss = ce_weight * CE_loss + reg_weight * regression_loss
|
| 5 |
+
|
| 6 |
+
The regression branch is selected by `sample_type`:
|
| 7 |
+
- sample_type == 1 ("crit"): use crit_reg_weight + criteria token ids/values
|
| 8 |
+
- sample_type == 0 ("total"): use total_reg_weight + total token ids/values
|
| 9 |
+
|
| 10 |
+
Because the dataset produces one criterion or one total-score question per sample,
|
| 11 |
+
a batch is homogeneous in type and we route once per forward pass β no scanning
|
| 12 |
+
of both heads.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
from typing import List, Optional
|
| 19 |
+
|
| 20 |
+
from .score_tokens import ScoreTokenRegistry
|
| 21 |
+
|
| 22 |
+
IGNORE_INDEX = -100
|
| 23 |
+
|
| 24 |
+
SAMPLE_TYPE_TOTAL = 0
|
| 25 |
+
SAMPLE_TYPE_CRIT = 1
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ScoreLoss(nn.Module):
|
| 29 |
+
"""
|
| 30 |
+
Args:
|
| 31 |
+
crit_registry: ScoreTokenRegistry for criteria scores (1β10).
|
| 32 |
+
total_registry: ScoreTokenRegistry for total score (0β100).
|
| 33 |
+
tokenizer: HF tokenizer with the custom score tokens already added.
|
| 34 |
+
ce_weight: Weight for the CE (language-modelling) loss.
|
| 35 |
+
crit_reg_weight: Weight for the criteria regression loss.
|
| 36 |
+
total_reg_weight: Weight for the total-score regression loss.
|
| 37 |
+
regression_loss: "mse" or "huber".
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def __init__(
|
| 41 |
+
self,
|
| 42 |
+
crit_registry: ScoreTokenRegistry,
|
| 43 |
+
total_registry: ScoreTokenRegistry,
|
| 44 |
+
tokenizer,
|
| 45 |
+
ce_weight: float = 1.0,
|
| 46 |
+
crit_reg_weight: float = 0.5,
|
| 47 |
+
total_reg_weight: float = 0.5,
|
| 48 |
+
regression_loss: str = "mse",
|
| 49 |
+
):
|
| 50 |
+
super().__init__()
|
| 51 |
+
self.ce_weight = ce_weight
|
| 52 |
+
self.crit_reg_weight = crit_reg_weight
|
| 53 |
+
self.total_reg_weight = total_reg_weight
|
| 54 |
+
self.regression_loss_type = regression_loss
|
| 55 |
+
|
| 56 |
+
# ββ criteria head βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 57 |
+
self.crit_ids = crit_registry.get_token_ids(tokenizer)
|
| 58 |
+
self.register_buffer("crit_vals", crit_registry.get_value_tensor())
|
| 59 |
+
self.crit_range = crit_registry.score_max - crit_registry.score_min
|
| 60 |
+
self.crit_use_placeholder = crit_registry.use_placeholder
|
| 61 |
+
self.crit_ph_id = crit_registry.get_placeholder_id(tokenizer) if crit_registry.use_placeholder else None
|
| 62 |
+
|
| 63 |
+
# ββ total head ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
+
self.total_ids = total_registry.get_token_ids(tokenizer)
|
| 65 |
+
self.register_buffer("total_vals", total_registry.get_value_tensor())
|
| 66 |
+
self.total_range = total_registry.score_max - total_registry.score_min
|
| 67 |
+
self.total_use_placeholder = total_registry.use_placeholder
|
| 68 |
+
self.total_ph_id = total_registry.get_placeholder_id(tokenizer) if total_registry.use_placeholder else None
|
| 69 |
+
|
| 70 |
+
# ββ helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 71 |
+
|
| 72 |
+
def _reg_loss(self, pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
| 73 |
+
# if self.regression_loss_type == "huber":
|
| 74 |
+
# return F.huber_loss(pred, target, delta=1.0)
|
| 75 |
+
return F.mse_loss(pred, target)
|
| 76 |
+
|
| 77 |
+
def _regression_for_head(
|
| 78 |
+
self,
|
| 79 |
+
flat_logits: torch.Tensor, # (B*(T-1), V)
|
| 80 |
+
flat_labels: torch.Tensor, # (B*(T-1),)
|
| 81 |
+
token_ids: List[int],
|
| 82 |
+
vals: torch.Tensor, # (n_bins,)
|
| 83 |
+
score_range: float,
|
| 84 |
+
gt_scores: Optional[torch.Tensor] = None, # (B,) continuous gt scores
|
| 85 |
+
placeholder_id: Optional[int] = None, # placeholder token id
|
| 86 |
+
batch_size: int = None, # needed for non-placeholder mode
|
| 87 |
+
) -> Optional[torch.Tensor]:
|
| 88 |
+
"""Compute regression loss at score positions.
|
| 89 |
+
|
| 90 |
+
If placeholder_id is set, find positions where label == placeholder_id,
|
| 91 |
+
then use logits over token_ids at those positions to compute expected score.
|
| 92 |
+
Otherwise, find the LAST score token position per sample to avoid matching
|
| 93 |
+
incidental occurrences of existing-vocab tokens in reasoning text.
|
| 94 |
+
"""
|
| 95 |
+
if placeholder_id is not None:
|
| 96 |
+
mask = (flat_labels == placeholder_id)
|
| 97 |
+
if not mask.any():
|
| 98 |
+
return None
|
| 99 |
+
if gt_scores is None:
|
| 100 |
+
return None
|
| 101 |
+
selected_logits = flat_logits[mask]
|
| 102 |
+
else:
|
| 103 |
+
# Existing vocab tokens (aa, ab..) may appear in reasoning text.
|
| 104 |
+
# Find the LAST matching position per sample to get the intended score token.
|
| 105 |
+
ids_t = torch.tensor(token_ids, device=flat_labels.device)
|
| 106 |
+
token_match = (flat_labels.unsqueeze(1) == ids_t.unsqueeze(0)).any(dim=1)
|
| 107 |
+
|
| 108 |
+
if not token_match.any():
|
| 109 |
+
return None
|
| 110 |
+
if gt_scores is None:
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
if batch_size is not None and batch_size > 0:
|
| 114 |
+
seq_len = flat_labels.shape[0] // batch_size
|
| 115 |
+
labels_2d = flat_labels.view(batch_size, seq_len)
|
| 116 |
+
match_2d = token_match.view(batch_size, seq_len)
|
| 117 |
+
|
| 118 |
+
# For each sample, find the last matching position
|
| 119 |
+
selected_indices = []
|
| 120 |
+
for b in range(batch_size):
|
| 121 |
+
positions = match_2d[b].nonzero(as_tuple=False).squeeze(-1)
|
| 122 |
+
if positions.numel() > 0:
|
| 123 |
+
last_pos = positions[-1].item()
|
| 124 |
+
selected_indices.append(b * seq_len + last_pos)
|
| 125 |
+
|
| 126 |
+
if not selected_indices:
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
idx_t = torch.tensor(selected_indices, device=flat_logits.device)
|
| 130 |
+
selected_logits = flat_logits[idx_t]
|
| 131 |
+
# Align gt_scores: only keep samples that had a match
|
| 132 |
+
gt_scores = gt_scores[:len(selected_indices)]
|
| 133 |
+
else:
|
| 134 |
+
# Fallback: use all matches (may cause size mismatch warning)
|
| 135 |
+
selected_logits = flat_logits[token_match]
|
| 136 |
+
|
| 137 |
+
ids_t = torch.tensor(token_ids, device=flat_labels.device)
|
| 138 |
+
with torch.amp.autocast('cuda', enabled=False):
|
| 139 |
+
logits_fp32 = selected_logits.float()
|
| 140 |
+
head_logits = logits_fp32[:, ids_t] # (M, n_bins) fp32
|
| 141 |
+
head_logits = torch.clamp(head_logits, min=-50.0, max=50.0)
|
| 142 |
+
probs = torch.softmax(head_logits, dim=-1)
|
| 143 |
+
pred_scores = (probs * vals.float()).sum(dim=-1) # (M,) fp32
|
| 144 |
+
target_scores = gt_scores.float().to(pred_scores.device)
|
| 145 |
+
reg = self._reg_loss(pred_scores / score_range, target_scores / score_range)
|
| 146 |
+
return reg
|
| 147 |
+
|
| 148 |
+
# ββ forward βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 149 |
+
|
| 150 |
+
def forward(
|
| 151 |
+
self,
|
| 152 |
+
logits: torch.Tensor, # (B, T, V)
|
| 153 |
+
labels: torch.Tensor, # (B, T)
|
| 154 |
+
sample_type: Optional[torch.Tensor] = None, # (B,) int, 1=crit 0=total 2=mcq
|
| 155 |
+
gt_score: Optional[torch.Tensor] = None, # (B,) float, original continuous score
|
| 156 |
+
vocab_size: int = None,
|
| 157 |
+
) -> torch.Tensor:
|
| 158 |
+
B, T, V = logits.shape
|
| 159 |
+
|
| 160 |
+
shift_logits = logits[:, :-1].contiguous() # (B, T-1, V)
|
| 161 |
+
shift_labels = labels[:, 1:].contiguous() # (B, T-1)
|
| 162 |
+
|
| 163 |
+
flat_logits = shift_logits.view(-1, V)
|
| 164 |
+
flat_labels = shift_labels.view(-1)
|
| 165 |
+
|
| 166 |
+
# ββ CE loss βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 167 |
+
if torch.isnan(flat_logits).any() or torch.isinf(flat_logits).any():
|
| 168 |
+
print(f"[ScoreLoss] WARNING: logits contain nan/inf BEFORE ce_loss, "
|
| 169 |
+
f"nan_count={torch.isnan(flat_logits).sum()}, inf_count={torch.isinf(flat_logits).sum()}")
|
| 170 |
+
return torch.tensor(0.0, device=logits.device, requires_grad=True)
|
| 171 |
+
|
| 172 |
+
ce_loss = F.cross_entropy(flat_logits, flat_labels, ignore_index=IGNORE_INDEX)
|
| 173 |
+
if torch.isnan(ce_loss):
|
| 174 |
+
ce_loss = torch.tensor(0.0, device=logits.device)
|
| 175 |
+
total_loss = self.ce_weight * ce_loss
|
| 176 |
+
|
| 177 |
+
# ββ Regression loss: route by sample_type ββββββββββββββββββββββββββββ
|
| 178 |
+
if sample_type is not None:
|
| 179 |
+
has_crit = (sample_type == SAMPLE_TYPE_CRIT).any()
|
| 180 |
+
has_total = (sample_type == SAMPLE_TYPE_TOTAL).any()
|
| 181 |
+
else:
|
| 182 |
+
has_crit = True
|
| 183 |
+
has_total = True
|
| 184 |
+
|
| 185 |
+
seq_len = T - 1 # after shift
|
| 186 |
+
|
| 187 |
+
if has_crit and self.crit_reg_weight > 0.0:
|
| 188 |
+
crit_gt = None
|
| 189 |
+
if gt_score is not None and sample_type is not None:
|
| 190 |
+
crit_mask = (sample_type == SAMPLE_TYPE_CRIT)
|
| 191 |
+
if crit_mask.any():
|
| 192 |
+
crit_gt = gt_score[crit_mask]
|
| 193 |
+
crit_logits = shift_logits[crit_mask].view(-1, V)
|
| 194 |
+
crit_labels = shift_labels[crit_mask].view(-1)
|
| 195 |
+
crit_B = crit_mask.sum().item()
|
| 196 |
+
else:
|
| 197 |
+
crit_gt = None
|
| 198 |
+
elif gt_score is not None:
|
| 199 |
+
crit_gt = gt_score
|
| 200 |
+
crit_logits = flat_logits
|
| 201 |
+
crit_labels = flat_labels
|
| 202 |
+
crit_B = B
|
| 203 |
+
else:
|
| 204 |
+
crit_gt = None
|
| 205 |
+
|
| 206 |
+
if crit_gt is not None:
|
| 207 |
+
reg = self._regression_for_head(
|
| 208 |
+
crit_logits, crit_labels,
|
| 209 |
+
self.crit_ids, self.crit_vals.to(logits.device),
|
| 210 |
+
self.crit_range, gt_scores=crit_gt,
|
| 211 |
+
placeholder_id=self.crit_ph_id,
|
| 212 |
+
batch_size=crit_B,
|
| 213 |
+
)
|
| 214 |
+
if reg is not None:
|
| 215 |
+
total_loss = total_loss + self.crit_reg_weight * reg
|
| 216 |
+
|
| 217 |
+
if has_total and self.total_reg_weight > 0.0:
|
| 218 |
+
total_gt = None
|
| 219 |
+
if gt_score is not None and sample_type is not None:
|
| 220 |
+
total_mask = (sample_type == SAMPLE_TYPE_TOTAL)
|
| 221 |
+
if total_mask.any():
|
| 222 |
+
total_gt = gt_score[total_mask]
|
| 223 |
+
total_logits = shift_logits[total_mask].view(-1, V)
|
| 224 |
+
total_labels = shift_labels[total_mask].view(-1)
|
| 225 |
+
total_B = total_mask.sum().item()
|
| 226 |
+
else:
|
| 227 |
+
total_gt = None
|
| 228 |
+
elif gt_score is not None:
|
| 229 |
+
total_gt = gt_score
|
| 230 |
+
total_logits = flat_logits
|
| 231 |
+
total_labels = flat_labels
|
| 232 |
+
total_B = B
|
| 233 |
+
else:
|
| 234 |
+
total_gt = None
|
| 235 |
+
|
| 236 |
+
if total_gt is not None:
|
| 237 |
+
reg = self._regression_for_head(
|
| 238 |
+
total_logits, total_labels,
|
| 239 |
+
self.total_ids, self.total_vals.to(logits.device),
|
| 240 |
+
self.total_range, gt_scores=total_gt,
|
| 241 |
+
placeholder_id=self.total_ph_id,
|
| 242 |
+
batch_size=total_B,
|
| 243 |
+
)
|
| 244 |
+
if reg is not None:
|
| 245 |
+
total_loss = total_loss + self.total_reg_weight * reg
|
| 246 |
+
|
| 247 |
+
return total_loss
|
qwenvl/train/score_tokens.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Generic integer-index score tokens.
|
| 3 |
+
|
| 4 |
+
A ScoreTokenRegistry holds N tokens <score1> ... <scoreN> that represent
|
| 5 |
+
N evenly-spaced bins covering [score_min, score_max].
|
| 6 |
+
|
| 7 |
+
The actual score value for bin k (1-indexed) is:
|
| 8 |
+
score_min + (k - 1) / (N - 1) * (score_max - score_min)
|
| 9 |
+
|
| 10 |
+
Usage
|
| 11 |
+
-----
|
| 12 |
+
# criteria: 1β10 range, 20 bins β <score1> β¦ <score20>
|
| 13 |
+
crit_reg = ScoreTokenRegistry(n_bins=20, score_min=1.0, score_max=10.0)
|
| 14 |
+
|
| 15 |
+
# total: 0β100 range, 101 bins β <total1> β¦ <total101>
|
| 16 |
+
total_reg = ScoreTokenRegistry(n_bins=101, score_min=0.0, score_max=100.0,
|
| 17 |
+
prefix="total")
|
| 18 |
+
|
| 19 |
+
all_tokens = crit_reg.tokens + total_reg.tokens
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import string
|
| 23 |
+
import torch
|
| 24 |
+
from typing import List, Optional
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def get_single_token_two_letter_combos(vocab: dict) -> List[str]:
|
| 28 |
+
"""Return ordered list of two-letter combos that are single tokens in vocab."""
|
| 29 |
+
combos = []
|
| 30 |
+
for c1 in string.ascii_lowercase:
|
| 31 |
+
for c2 in string.ascii_lowercase:
|
| 32 |
+
t = c1 + c2
|
| 33 |
+
if t in vocab:
|
| 34 |
+
combos.append(t)
|
| 35 |
+
return combos
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class ScoreTokenRegistry:
|
| 39 |
+
"""
|
| 40 |
+
Registry for a set of evenly-spaced score tokens.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
n_bins: Number of discrete bins / tokens.
|
| 44 |
+
score_min: Real value corresponding to bin 1.
|
| 45 |
+
score_max: Real value corresponding to bin n_bins.
|
| 46 |
+
prefix: Token prefix string (default "score").
|
| 47 |
+
Tokens are named <{prefix}1> β¦ <{prefix}{n_bins}>.
|
| 48 |
+
use_placeholder: If True, add a placeholder token <{prefix}_ph> that
|
| 49 |
+
appears in text instead of specific score tokens.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(
|
| 53 |
+
self,
|
| 54 |
+
n_bins: int,
|
| 55 |
+
score_min: float,
|
| 56 |
+
score_max: float,
|
| 57 |
+
prefix: str = "score",
|
| 58 |
+
use_placeholder: bool = False,
|
| 59 |
+
):
|
| 60 |
+
assert n_bins >= 2
|
| 61 |
+
self.n_bins = n_bins
|
| 62 |
+
self.score_min = score_min
|
| 63 |
+
self.score_max = score_max
|
| 64 |
+
self.prefix = prefix
|
| 65 |
+
self.use_placeholder = use_placeholder
|
| 66 |
+
|
| 67 |
+
self.tokens: list[str] = [f"<{prefix}{k}>" for k in range(1, n_bins + 1)]
|
| 68 |
+
self.placeholder_token: str = f"<{prefix}_ph>"
|
| 69 |
+
|
| 70 |
+
# Real value for each bin (linearly spaced)
|
| 71 |
+
self.values: list[float] = [
|
| 72 |
+
score_min + (k / (n_bins - 1)) * (score_max - score_min)
|
| 73 |
+
for k in range(n_bins)
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
# Pre-computed constants for score_to_token
|
| 77 |
+
self._scale = (n_bins - 1) / (score_max - score_min)
|
| 78 |
+
|
| 79 |
+
# O(1) lookup tables
|
| 80 |
+
self._token_to_idx: dict[str, int] = {t: i for i, t in enumerate(self.tokens)}
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def all_tokens(self) -> list[str]:
|
| 84 |
+
"""All tokens to register (score bins + placeholder if enabled)."""
|
| 85 |
+
if self.use_placeholder:
|
| 86 |
+
return self.tokens + [self.placeholder_token]
|
| 87 |
+
return self.tokens
|
| 88 |
+
|
| 89 |
+
def get_placeholder_id(self, tokenizer) -> int:
|
| 90 |
+
"""Return the token id of the placeholder token."""
|
| 91 |
+
return tokenizer.convert_tokens_to_ids(self.placeholder_token)
|
| 92 |
+
|
| 93 |
+
# ββ conversion helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
|
| 95 |
+
def score_to_token(self, score: float) -> str:
|
| 96 |
+
"""Map a real score to the nearest bin token (or placeholder if enabled)."""
|
| 97 |
+
if self.use_placeholder:
|
| 98 |
+
return self.placeholder_token
|
| 99 |
+
k = int(round((score - self.score_min) * self._scale))
|
| 100 |
+
k = max(0, min(self.n_bins - 1, k))
|
| 101 |
+
return self.tokens[k]
|
| 102 |
+
|
| 103 |
+
def token_to_score(self, token: str) -> float:
|
| 104 |
+
return self.values[self._token_to_idx[token]]
|
| 105 |
+
|
| 106 |
+
# ββ tensor helpers (used at inference / loss time) ββββββββββββββββββββββββ
|
| 107 |
+
|
| 108 |
+
def get_token_ids(self, tokenizer) -> list[int]:
|
| 109 |
+
"""Return token ids in token order (token1, token2, β¦, tokenN)."""
|
| 110 |
+
return [tokenizer.convert_tokens_to_ids(t) for t in self.tokens]
|
| 111 |
+
|
| 112 |
+
def get_value_tensor(
|
| 113 |
+
self, tokenizer=None, device: Optional[torch.device] = None
|
| 114 |
+
) -> torch.Tensor:
|
| 115 |
+
"""Return float tensor of real values aligned with token order."""
|
| 116 |
+
t = torch.tensor(self.values, dtype=torch.float32)
|
| 117 |
+
return t.to(device) if device else t
|
| 118 |
+
|
| 119 |
+
def expected_score(
|
| 120 |
+
self,
|
| 121 |
+
logits: torch.Tensor,
|
| 122 |
+
token_ids: list[int],
|
| 123 |
+
value_tensor: torch.Tensor,
|
| 124 |
+
) -> torch.Tensor:
|
| 125 |
+
"""
|
| 126 |
+
Compute expected score from a logits slice.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
logits: (vocab_size,) raw logits for one position
|
| 130 |
+
token_ids: ordered list of this registry's token ids
|
| 131 |
+
value_tensor: (n_bins,) real values matching token_ids order
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
scalar expected score
|
| 135 |
+
"""
|
| 136 |
+
ids_t = torch.tensor(token_ids, device=logits.device)
|
| 137 |
+
probs = torch.softmax(logits[ids_t], dim=-1)
|
| 138 |
+
return (probs * value_tensor.to(logits.device)).sum()
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class ExistingVocabScoreTokenRegistry:
|
| 142 |
+
"""
|
| 143 |
+
Registry that reuses existing vocabulary tokens (two-letter combos)
|
| 144 |
+
as score bins. Only the placeholder token (if enabled) is newly added.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
token_list: Pre-selected tokens from the existing vocabulary.
|
| 148 |
+
score_min: Real value corresponding to the first token.
|
| 149 |
+
score_max: Real value corresponding to the last token.
|
| 150 |
+
prefix: Used only for the placeholder token name <{prefix}_ph>.
|
| 151 |
+
use_placeholder: If True, add a placeholder token <{prefix}_ph>.
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
def __init__(
|
| 155 |
+
self,
|
| 156 |
+
token_list: List[str],
|
| 157 |
+
score_min: float,
|
| 158 |
+
score_max: float,
|
| 159 |
+
prefix: str = "score",
|
| 160 |
+
use_placeholder: bool = False,
|
| 161 |
+
):
|
| 162 |
+
assert len(token_list) >= 2
|
| 163 |
+
self.n_bins = len(token_list)
|
| 164 |
+
self.score_min = score_min
|
| 165 |
+
self.score_max = score_max
|
| 166 |
+
self.prefix = prefix
|
| 167 |
+
self.use_placeholder = use_placeholder
|
| 168 |
+
|
| 169 |
+
self.tokens: List[str] = list(token_list)
|
| 170 |
+
self.placeholder_token: str = f"<{prefix}_ph>"
|
| 171 |
+
|
| 172 |
+
self.values: List[float] = [
|
| 173 |
+
score_min + (k / (self.n_bins - 1)) * (score_max - score_min)
|
| 174 |
+
for k in range(self.n_bins)
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
+
self._scale = (self.n_bins - 1) / (score_max - score_min)
|
| 178 |
+
self._token_to_idx: dict[str, int] = {t: i for i, t in enumerate(self.tokens)}
|
| 179 |
+
|
| 180 |
+
@property
|
| 181 |
+
def all_tokens(self) -> List[str]:
|
| 182 |
+
"""Only the placeholder is a new token; bin tokens already exist."""
|
| 183 |
+
if self.use_placeholder:
|
| 184 |
+
return [self.placeholder_token]
|
| 185 |
+
return []
|
| 186 |
+
|
| 187 |
+
@property
|
| 188 |
+
def new_tokens(self) -> List[str]:
|
| 189 |
+
"""Tokens that need to be added to the tokenizer."""
|
| 190 |
+
return self.all_tokens
|
| 191 |
+
|
| 192 |
+
def get_placeholder_id(self, tokenizer) -> int:
|
| 193 |
+
return tokenizer.convert_tokens_to_ids(self.placeholder_token)
|
| 194 |
+
|
| 195 |
+
def score_to_token(self, score: float) -> str:
|
| 196 |
+
if self.use_placeholder:
|
| 197 |
+
return self.placeholder_token
|
| 198 |
+
k = int(round((score - self.score_min) * self._scale))
|
| 199 |
+
k = max(0, min(self.n_bins - 1, k))
|
| 200 |
+
return self.tokens[k]
|
| 201 |
+
|
| 202 |
+
def token_to_score(self, token: str) -> float:
|
| 203 |
+
return self.values[self._token_to_idx[token]]
|
| 204 |
+
|
| 205 |
+
def get_token_ids(self, tokenizer) -> List[int]:
|
| 206 |
+
return [tokenizer.convert_tokens_to_ids(t) for t in self.tokens]
|
| 207 |
+
|
| 208 |
+
def get_value_tensor(
|
| 209 |
+
self, tokenizer=None, device: Optional[torch.device] = None
|
| 210 |
+
) -> torch.Tensor:
|
| 211 |
+
t = torch.tensor(self.values, dtype=torch.float32)
|
| 212 |
+
return t.to(device) if device else t
|
| 213 |
+
|
| 214 |
+
def expected_score(
|
| 215 |
+
self,
|
| 216 |
+
logits: torch.Tensor,
|
| 217 |
+
token_ids: List[int],
|
| 218 |
+
value_tensor: torch.Tensor,
|
| 219 |
+
) -> torch.Tensor:
|
| 220 |
+
ids_t = torch.tensor(token_ids, device=logits.device)
|
| 221 |
+
probs = torch.softmax(logits[ids_t], dim=-1)
|
| 222 |
+
return (probs * value_tensor.to(logits.device)).sum()
|
qwenvl/train/train_qwen.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adopted from https://github.com/lm-sys/FastChat. Below is the original copyright:
|
| 2 |
+
# Adopted from tatsu-lab@stanford_alpaca. Below is the original copyright:
|
| 3 |
+
# Copyright 2023 Rohan Taori, Ishaan Gulrajani, Tianyi Zhang, Yann Dubois, Xuechen Li
|
| 4 |
+
#
|
| 5 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 6 |
+
# you may not use this file except in compliance with the License.
|
| 7 |
+
# You may obtain a copy of the License at
|
| 8 |
+
#
|
| 9 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
#
|
| 11 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 12 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
+
# See the License for the specific language governing permissions and
|
| 15 |
+
# limitations under the License.
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import logging
|
| 19 |
+
import pathlib
|
| 20 |
+
import torch
|
| 21 |
+
import transformers
|
| 22 |
+
import sys
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
from PIL import Image
|
| 25 |
+
Image.MAX_IMAGE_PIXELS = None # disable decompression bomb check for large training images
|
| 26 |
+
|
| 27 |
+
project_root = Path(__file__).parent.parent.parent
|
| 28 |
+
sys.path.append(str(project_root))
|
| 29 |
+
|
| 30 |
+
from trainer import replace_qwen2_vl_attention_class
|
| 31 |
+
|
| 32 |
+
from transformers import (
|
| 33 |
+
Qwen2VLForConditionalGeneration,
|
| 34 |
+
Qwen2_5_VLForConditionalGeneration,
|
| 35 |
+
Qwen3VLForConditionalGeneration,
|
| 36 |
+
Qwen3VLMoeForConditionalGeneration
|
| 37 |
+
)
|
| 38 |
+
from qwenvl.data.data_processor import make_supervised_data_module
|
| 39 |
+
from qwenvl.data.aesthetic_dataset import AestheticScoringDataset
|
| 40 |
+
from qwenvl.train.argument import (
|
| 41 |
+
ModelArguments,
|
| 42 |
+
DataArguments,
|
| 43 |
+
TrainingArguments,
|
| 44 |
+
)
|
| 45 |
+
from qwenvl.train.score_tokens import ScoreTokenRegistry, ExistingVocabScoreTokenRegistry, get_single_token_two_letter_combos
|
| 46 |
+
from qwenvl.train.score_loss import ScoreLoss
|
| 47 |
+
from qwenvl.train.logging_callback import LocalLoggingCallback
|
| 48 |
+
from transformers import AutoProcessor, Trainer
|
| 49 |
+
from deepspeed import zero
|
| 50 |
+
|
| 51 |
+
local_rank = None
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def rank0_print(*args):
|
| 55 |
+
if local_rank == 0:
|
| 56 |
+
print(*args)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def expand_vocab_for_scoring(model, tokenizer, registries):
|
| 60 |
+
"""Add custom score tokens to tokenizer and resize model embeddings.
|
| 61 |
+
|
| 62 |
+
Score tokens are initialized along a badβgood semantic axis;
|
| 63 |
+
placeholder tokens get the mean embedding.
|
| 64 |
+
"""
|
| 65 |
+
all_tokens = [t for reg in registries for t in reg.all_tokens]
|
| 66 |
+
num_added = tokenizer.add_tokens(all_tokens)
|
| 67 |
+
rank0_print(f"Added {num_added} score tokens to tokenizer (total vocab: {len(tokenizer)})")
|
| 68 |
+
model.resize_token_embeddings(len(tokenizer))
|
| 69 |
+
|
| 70 |
+
bad_words = ["bad", "terrible", "poor", "worst", "awful", "low", "weak"]
|
| 71 |
+
good_words = ["good", "excellent", "great", "best", "amazing", "high", "strong"]
|
| 72 |
+
|
| 73 |
+
with torch.no_grad():
|
| 74 |
+
w_in = model.get_input_embeddings().weight.data
|
| 75 |
+
w_out = model.get_output_embeddings().weight.data
|
| 76 |
+
base_vector = w_in[:-num_added].mean(dim=0, keepdim=True)
|
| 77 |
+
|
| 78 |
+
# Compute quality direction from semantic anchors
|
| 79 |
+
bad_ids = [tokenizer.convert_tokens_to_ids(w) for w in bad_words]
|
| 80 |
+
good_ids = [tokenizer.convert_tokens_to_ids(w) for w in good_words]
|
| 81 |
+
bad_vecs = torch.stack([w_in[i] for i in bad_ids if i != tokenizer.unk_token_id])
|
| 82 |
+
good_vecs = torch.stack([w_in[i] for i in good_ids if i != tokenizer.unk_token_id])
|
| 83 |
+
|
| 84 |
+
if len(bad_vecs) > 0 and len(good_vecs) > 0:
|
| 85 |
+
quality_dir = good_vecs.mean(0) - bad_vecs.mean(0)
|
| 86 |
+
q_norm = torch.norm(quality_dir)
|
| 87 |
+
if q_norm > 0:
|
| 88 |
+
quality_dir = quality_dir / q_norm
|
| 89 |
+
quality_scale = w_in[:-num_added].std() * 0.5
|
| 90 |
+
|
| 91 |
+
offset = 0
|
| 92 |
+
for reg in registries:
|
| 93 |
+
n_bins = reg.n_bins
|
| 94 |
+
for i in range(n_bins):
|
| 95 |
+
weight = 2.0 * i / (n_bins - 1) - 1.0 if n_bins > 1 else 0.0
|
| 96 |
+
vec = base_vector + quality_dir * weight * quality_scale
|
| 97 |
+
w_in[-num_added + offset + i] = vec
|
| 98 |
+
w_out[-num_added + offset + i] = vec
|
| 99 |
+
offset += n_bins
|
| 100 |
+
if reg.use_placeholder:
|
| 101 |
+
w_in[-num_added + offset] = base_vector
|
| 102 |
+
w_out[-num_added + offset] = base_vector
|
| 103 |
+
offset += 1
|
| 104 |
+
|
| 105 |
+
rank0_print(f"[init] Score tokens initialized with quality axis (badβgood), scale={quality_scale:.4f}")
|
| 106 |
+
else:
|
| 107 |
+
w_in[-num_added:] = base_vector
|
| 108 |
+
w_out[-num_added:] = base_vector
|
| 109 |
+
rank0_print("[init] Score tokens initialized with mean embedding (fallback)")
|
| 110 |
+
|
| 111 |
+
return model, tokenizer
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def expand_vocab_for_existing_tokens(model, tokenizer, registries):
|
| 115 |
+
"""Add only placeholder tokens when using existing vocab tokens as score bins.
|
| 116 |
+
|
| 117 |
+
Bin tokens (aa, ab, ...) already exist in the vocabulary β no embedding init needed.
|
| 118 |
+
Only placeholder tokens (if enabled) are newly added and initialized with mean embedding.
|
| 119 |
+
"""
|
| 120 |
+
new_tokens = [t for reg in registries for t in reg.new_tokens]
|
| 121 |
+
if new_tokens:
|
| 122 |
+
num_added = tokenizer.add_tokens(new_tokens)
|
| 123 |
+
rank0_print(f"[existing-vocab] Added {num_added} placeholder tokens (total vocab: {len(tokenizer)})")
|
| 124 |
+
model.resize_token_embeddings(len(tokenizer))
|
| 125 |
+
|
| 126 |
+
with torch.no_grad():
|
| 127 |
+
w_in = model.get_input_embeddings().weight.data
|
| 128 |
+
w_out = model.get_output_embeddings().weight.data
|
| 129 |
+
base_vector = w_in[:-num_added].mean(dim=0, keepdim=True)
|
| 130 |
+
w_in[-num_added:] = base_vector
|
| 131 |
+
w_out[-num_added:] = base_vector
|
| 132 |
+
rank0_print(f"[existing-vocab] Placeholder tokens initialized with mean embedding")
|
| 133 |
+
else:
|
| 134 |
+
rank0_print("[existing-vocab] No placeholder β using direct score bin tokens only")
|
| 135 |
+
|
| 136 |
+
for reg in registries:
|
| 137 |
+
bin_ids = reg.get_token_ids(tokenizer)
|
| 138 |
+
rank0_print(f"[existing-vocab] {reg.prefix}: {reg.n_bins} bins using tokens "
|
| 139 |
+
f"{reg.tokens[0]}..{reg.tokens[-1]} (ids {bin_ids[0]}..{bin_ids[-1]})")
|
| 140 |
+
|
| 141 |
+
return model, tokenizer
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class AestheticDataCollator:
|
| 145 |
+
"""Wraps DataCollatorForSupervisedDataset and additionally collects sample_type."""
|
| 146 |
+
|
| 147 |
+
def __init__(self, base_collator):
|
| 148 |
+
self.base = base_collator
|
| 149 |
+
|
| 150 |
+
def __call__(self, instances):
|
| 151 |
+
# collect sample_type and gt_score before passing to base collator (which ignores unknown keys)
|
| 152 |
+
sample_types = [inst.pop("sample_type", None) for inst in instances]
|
| 153 |
+
gt_scores = [inst.pop("gt_score", None) for inst in instances]
|
| 154 |
+
batch = self.base(instances)
|
| 155 |
+
if any(t is not None for t in sample_types):
|
| 156 |
+
batch["sample_type"] = torch.tensor(
|
| 157 |
+
[t if t is not None else -1 for t in sample_types],
|
| 158 |
+
dtype=torch.long,
|
| 159 |
+
)
|
| 160 |
+
if any(s is not None for s in gt_scores):
|
| 161 |
+
batch["gt_score"] = torch.tensor(
|
| 162 |
+
[s if s is not None else 0.0 for s in gt_scores],
|
| 163 |
+
dtype=torch.float32,
|
| 164 |
+
)
|
| 165 |
+
return batch
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
class AestheticTrainer(Trainer):
|
| 169 |
+
"""Trainer subclass that uses ScoreLoss when score tokens are enabled."""
|
| 170 |
+
|
| 171 |
+
def __init__(self, *args, score_loss: ScoreLoss = None, tokenizer=None, **kwargs):
|
| 172 |
+
super().__init__(*args, **kwargs)
|
| 173 |
+
self.score_loss = score_loss
|
| 174 |
+
self._decode_tokenizer = tokenizer # kept separately for decoding
|
| 175 |
+
|
| 176 |
+
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
|
| 177 |
+
if self.score_loss is None:
|
| 178 |
+
return super().compute_loss(model, inputs, return_outputs=return_outputs, **kwargs)
|
| 179 |
+
|
| 180 |
+
labels = inputs.pop("labels", None)
|
| 181 |
+
sample_type = inputs.pop("sample_type", None) # (B,) int tensor or None
|
| 182 |
+
gt_score = inputs.pop("gt_score", None) # (B,) float tensor or None
|
| 183 |
+
if gt_score is not None:
|
| 184 |
+
gt_score = gt_score.float()
|
| 185 |
+
outputs = model(**inputs)
|
| 186 |
+
logits = outputs.logits
|
| 187 |
+
|
| 188 |
+
loss = self.score_loss(logits, labels, sample_type=sample_type, gt_score=gt_score)
|
| 189 |
+
|
| 190 |
+
# ββ per-batch text monitor (rank-0 only, every N steps) βββββββββββββββ
|
| 191 |
+
step = self.state.global_step
|
| 192 |
+
if (step != getattr(self, '_last_print_step', -1)
|
| 193 |
+
and self.args.local_rank in (-1, 0)
|
| 194 |
+
and self._decode_tokenizer is not None):
|
| 195 |
+
self._last_print_step = step
|
| 196 |
+
tok = self._decode_tokenizer
|
| 197 |
+
input_text = tok.decode(
|
| 198 |
+
inputs["input_ids"][0], skip_special_tokens=False
|
| 199 |
+
)
|
| 200 |
+
pred_ids = logits[0].argmax(dim=-1)
|
| 201 |
+
label_row = labels[0]
|
| 202 |
+
mask = label_row != -100
|
| 203 |
+
if mask.any():
|
| 204 |
+
gt_ids = label_row[mask]
|
| 205 |
+
pred_at_label_pos = pred_ids[mask.roll(-1)][:-1] if mask.sum() > 1 else pred_ids[mask]
|
| 206 |
+
gt_text = tok.decode(gt_ids, skip_special_tokens=False)
|
| 207 |
+
pred_text = tok.decode(pred_at_label_pos, skip_special_tokens=False)
|
| 208 |
+
if torch.distributed.get_rank() == 0:
|
| 209 |
+
print(f"\n[step {step}] INPUT : {input_text[-200:]}")
|
| 210 |
+
print(f"[step {step}] GT : {gt_text[-300:]}")
|
| 211 |
+
print(f"[step {step}] PRED : {pred_text[-300:]}\n", flush=True)
|
| 212 |
+
|
| 213 |
+
return (loss, outputs) if return_outputs else loss
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def safe_save_model_for_hf_trainer(trainer: transformers.Trainer, output_dir: str):
|
| 218 |
+
"""Collects the state dict and dump to disk."""
|
| 219 |
+
|
| 220 |
+
if trainer.deepspeed:
|
| 221 |
+
torch.cuda.synchronize()
|
| 222 |
+
trainer.save_model(output_dir)
|
| 223 |
+
return
|
| 224 |
+
|
| 225 |
+
state_dict = trainer.model.state_dict()
|
| 226 |
+
if trainer.args.should_save:
|
| 227 |
+
cpu_state_dict = {key: value.cpu() for key, value in state_dict.items()}
|
| 228 |
+
del state_dict
|
| 229 |
+
trainer._save(output_dir, state_dict=cpu_state_dict) # noqa
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def set_model(model_args, model):
|
| 233 |
+
if model_args.tune_mm_vision:
|
| 234 |
+
for n, p in model.visual.named_parameters():
|
| 235 |
+
p.requires_grad = True
|
| 236 |
+
else:
|
| 237 |
+
for n, p in model.visual.named_parameters():
|
| 238 |
+
p.requires_grad = False
|
| 239 |
+
|
| 240 |
+
if model_args.tune_mm_mlp:
|
| 241 |
+
for n, p in model.visual.merger.named_parameters():
|
| 242 |
+
p.requires_grad = True
|
| 243 |
+
else:
|
| 244 |
+
for n, p in model.visual.merger.named_parameters():
|
| 245 |
+
p.requires_grad = False
|
| 246 |
+
|
| 247 |
+
if model_args.tune_mm_llm:
|
| 248 |
+
for n, p in model.language_model.named_parameters():
|
| 249 |
+
p.requires_grad = True
|
| 250 |
+
model.lm_head.requires_grad = True
|
| 251 |
+
else:
|
| 252 |
+
for n, p in model.language_model.named_parameters():
|
| 253 |
+
p.requires_grad = False
|
| 254 |
+
model.lm_head.requires_grad = False
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def train(attn_implementation="flash_attention_2"):
|
| 258 |
+
global local_rank
|
| 259 |
+
|
| 260 |
+
parser = transformers.HfArgumentParser(
|
| 261 |
+
(ModelArguments, DataArguments, TrainingArguments)
|
| 262 |
+
)
|
| 263 |
+
model_args, data_args, training_args = parser.parse_args_into_dataclasses()
|
| 264 |
+
|
| 265 |
+
local_rank = training_args.local_rank
|
| 266 |
+
os.makedirs(training_args.output_dir, exist_ok=True)
|
| 267 |
+
|
| 268 |
+
# if "qwen3" in model_args.model_name_or_path.lower() and "a" in Path(model_args.model_name_or_path.rstrip("/")).name.lower():
|
| 269 |
+
# model = Qwen3VLMoeForConditionalGeneration.from_pretrained(
|
| 270 |
+
# model_args.model_name_or_path,
|
| 271 |
+
# cache_dir=training_args.cache_dir,
|
| 272 |
+
# attn_implementation=attn_implementation,
|
| 273 |
+
# dtype=(torch.bfloat16 if training_args.bf16 else None),
|
| 274 |
+
# )
|
| 275 |
+
# data_args.model_type = "qwen3vl"
|
| 276 |
+
if "qwen3" in model_args.model_name_or_path.lower():
|
| 277 |
+
model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 278 |
+
model_args.model_name_or_path,
|
| 279 |
+
cache_dir=training_args.cache_dir,
|
| 280 |
+
attn_implementation=attn_implementation,
|
| 281 |
+
dtype=(torch.bfloat16 if training_args.bf16 else None),
|
| 282 |
+
)
|
| 283 |
+
data_args.model_type = "qwen3vl"
|
| 284 |
+
elif "qwen2.5" in model_args.model_name_or_path.lower():
|
| 285 |
+
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
| 286 |
+
model_args.model_name_or_path,
|
| 287 |
+
cache_dir=training_args.cache_dir,
|
| 288 |
+
attn_implementation=attn_implementation,
|
| 289 |
+
dtype=(torch.bfloat16 if training_args.bf16 else None),
|
| 290 |
+
)
|
| 291 |
+
data_args.model_type = "qwen2.5vl"
|
| 292 |
+
else:
|
| 293 |
+
model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 294 |
+
model_args.model_name_or_path,
|
| 295 |
+
cache_dir=training_args.cache_dir,
|
| 296 |
+
attn_implementation=attn_implementation,
|
| 297 |
+
dtype=(torch.bfloat16 if training_args.bf16 else None),
|
| 298 |
+
)
|
| 299 |
+
data_args.model_type = "qwen3vl"
|
| 300 |
+
|
| 301 |
+
print(f'the initlized model is {model_args.model_name_or_path} the class is {model.__class__.__name__}')
|
| 302 |
+
print(f'attn_implementation: {model.config._attn_implementation}')
|
| 303 |
+
processor = AutoProcessor.from_pretrained(
|
| 304 |
+
model_args.model_name_or_path,
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
# Expand vocabulary with score tokens
|
| 308 |
+
score_loss_fn = None
|
| 309 |
+
crit_registry = None
|
| 310 |
+
total_registry = None
|
| 311 |
+
if data_args.use_score_tokens:
|
| 312 |
+
if data_args.use_existing_vocab_tokens:
|
| 313 |
+
import json as _json_vocab
|
| 314 |
+
vocab_path = os.path.join(model_args.model_name_or_path, "vocab.json")
|
| 315 |
+
with open(vocab_path, "r") as f:
|
| 316 |
+
vocab = _json_vocab.load(f)
|
| 317 |
+
valid_combos = get_single_token_two_letter_combos(vocab)
|
| 318 |
+
n_crit = data_args.crit_score_bins
|
| 319 |
+
n_total = data_args.total_score_bins
|
| 320 |
+
assert len(valid_combos) >= n_crit + n_total, (
|
| 321 |
+
f"Need {n_crit + n_total} single-token combos but only {len(valid_combos)} available"
|
| 322 |
+
)
|
| 323 |
+
crit_tokens = valid_combos[:n_crit]
|
| 324 |
+
total_tokens = valid_combos[n_crit:n_crit + n_total]
|
| 325 |
+
|
| 326 |
+
crit_registry = ExistingVocabScoreTokenRegistry(
|
| 327 |
+
token_list=crit_tokens,
|
| 328 |
+
score_min=1.0,
|
| 329 |
+
score_max=10.0,
|
| 330 |
+
prefix="crit",
|
| 331 |
+
use_placeholder=data_args.use_placeholder,
|
| 332 |
+
)
|
| 333 |
+
total_registry = ExistingVocabScoreTokenRegistry(
|
| 334 |
+
token_list=total_tokens,
|
| 335 |
+
score_min=0.0,
|
| 336 |
+
score_max=100.0,
|
| 337 |
+
prefix="total",
|
| 338 |
+
use_placeholder=data_args.use_placeholder,
|
| 339 |
+
)
|
| 340 |
+
model, processor.tokenizer = expand_vocab_for_existing_tokens(
|
| 341 |
+
model, processor.tokenizer, [crit_registry, total_registry]
|
| 342 |
+
)
|
| 343 |
+
else:
|
| 344 |
+
crit_registry = ScoreTokenRegistry(
|
| 345 |
+
n_bins=data_args.crit_score_bins,
|
| 346 |
+
score_min=1.0,
|
| 347 |
+
score_max=10.0,
|
| 348 |
+
prefix="score",
|
| 349 |
+
use_placeholder=data_args.use_placeholder,
|
| 350 |
+
)
|
| 351 |
+
total_registry = ScoreTokenRegistry(
|
| 352 |
+
n_bins=data_args.total_score_bins,
|
| 353 |
+
score_min=0.0,
|
| 354 |
+
score_max=100.0,
|
| 355 |
+
prefix="total",
|
| 356 |
+
use_placeholder=data_args.use_placeholder,
|
| 357 |
+
)
|
| 358 |
+
model, processor.tokenizer = expand_vocab_for_scoring(
|
| 359 |
+
model, processor.tokenizer, [crit_registry, total_registry]
|
| 360 |
+
)
|
| 361 |
+
score_loss_fn = ScoreLoss(
|
| 362 |
+
crit_registry=crit_registry,
|
| 363 |
+
total_registry=total_registry,
|
| 364 |
+
tokenizer=processor.tokenizer,
|
| 365 |
+
ce_weight=training_args.score_ce_weight,
|
| 366 |
+
crit_reg_weight=training_args.crit_reg_weight,
|
| 367 |
+
total_reg_weight=training_args.total_reg_weight,
|
| 368 |
+
regression_loss=training_args.score_regression_loss,
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
if data_args.data_flatten or data_args.data_packing:
|
| 372 |
+
replace_qwen2_vl_attention_class()
|
| 373 |
+
model.config.use_cache = False
|
| 374 |
+
|
| 375 |
+
if training_args.gradient_checkpointing:
|
| 376 |
+
if hasattr(model, "enable_input_require_grads"):
|
| 377 |
+
model.enable_input_require_grads()
|
| 378 |
+
else:
|
| 379 |
+
|
| 380 |
+
def make_inputs_require_grad(module, input, output):
|
| 381 |
+
output.requires_grad_(True)
|
| 382 |
+
|
| 383 |
+
model.get_input_embeddings().register_forward_hook(make_inputs_require_grad)
|
| 384 |
+
|
| 385 |
+
tokenizer = transformers.AutoTokenizer.from_pretrained(
|
| 386 |
+
model_args.model_name_or_path,
|
| 387 |
+
cache_dir=training_args.cache_dir,
|
| 388 |
+
model_max_length=training_args.model_max_length,
|
| 389 |
+
padding_side="right",
|
| 390 |
+
use_fast=False,
|
| 391 |
+
)
|
| 392 |
+
if data_args.use_score_tokens:
|
| 393 |
+
tokenizer = processor.tokenizer
|
| 394 |
+
|
| 395 |
+
if training_args.lora_enable:
|
| 396 |
+
from peft import LoraConfig, get_peft_model, TaskType
|
| 397 |
+
print("LoRA enabled")
|
| 398 |
+
|
| 399 |
+
for p in model.parameters():
|
| 400 |
+
p.requires_grad = False
|
| 401 |
+
|
| 402 |
+
lora_config = LoraConfig(
|
| 403 |
+
r=training_args.lora_r or 64,
|
| 404 |
+
lora_alpha=training_args.lora_alpha or 128,
|
| 405 |
+
lora_dropout=training_args.lora_dropout or 0.05,
|
| 406 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # Qwen η attention ηΊΏζ§ε±
|
| 407 |
+
bias="none",
|
| 408 |
+
task_type=TaskType.CAUSAL_LM,
|
| 409 |
+
)
|
| 410 |
+
model = get_peft_model(model, lora_config)
|
| 411 |
+
else:
|
| 412 |
+
set_model(model_args, model)
|
| 413 |
+
|
| 414 |
+
if torch.distributed.get_rank() == 0:
|
| 415 |
+
model.visual.print_trainable_parameters()
|
| 416 |
+
model.model.print_trainable_parameters()
|
| 417 |
+
if torch.distributed.get_rank() == 0:
|
| 418 |
+
for name, param in model.named_parameters():
|
| 419 |
+
print(f"{name}:{param.requires_grad}")
|
| 420 |
+
|
| 421 |
+
if data_args.use_score_tokens:
|
| 422 |
+
from qwenvl.data.data_processor import update_processor_pixels
|
| 423 |
+
processor = update_processor_pixels(processor, data_args)
|
| 424 |
+
from qwenvl.data import data_dict as _data_dict
|
| 425 |
+
_ds_cfg = _data_dict.get("aesthetic_track1", {})
|
| 426 |
+
annotation_path = os.environ.get("AESTHETIC_ANNOTATION", _ds_cfg["annotation_path"])
|
| 427 |
+
image_dir = os.environ.get("AESTHETIC_IMAGE_DIR", _ds_cfg["data_path"])
|
| 428 |
+
train_dataset = AestheticScoringDataset(
|
| 429 |
+
processor=processor,
|
| 430 |
+
annotation_path=annotation_path,
|
| 431 |
+
image_dir=image_dir,
|
| 432 |
+
data_args=data_args,
|
| 433 |
+
crit_registry=crit_registry,
|
| 434 |
+
total_registry=total_registry,
|
| 435 |
+
)
|
| 436 |
+
from qwenvl.data.data_processor import DataCollatorForSupervisedDataset
|
| 437 |
+
base_collator = DataCollatorForSupervisedDataset(processor.tokenizer)
|
| 438 |
+
data_collator = AestheticDataCollator(base_collator)
|
| 439 |
+
data_module = dict(train_dataset=train_dataset, eval_dataset=None, data_collator=data_collator)
|
| 440 |
+
else:
|
| 441 |
+
data_module = make_supervised_data_module(processor, data_args=data_args)
|
| 442 |
+
|
| 443 |
+
# Build a small fixed sample set for generation logging (rank-0 only)
|
| 444 |
+
_sample_items = []
|
| 445 |
+
if data_args.use_score_tokens and local_rank in (-1, 0):
|
| 446 |
+
import json as _json
|
| 447 |
+
_ann_path = os.environ.get("AESTHETIC_ANNOTATION", "")
|
| 448 |
+
_img_root = os.environ.get("AESTHETIC_IMAGE_DIR", "")
|
| 449 |
+
try:
|
| 450 |
+
_all_raw = _json.load(open(_ann_path))
|
| 451 |
+
_raw = [x for x in _all_raw if "question" not in x][:4]
|
| 452 |
+
from qwenvl.data.aesthetic_dataset import CRITERIA_ORDER, CRITERIA_QUESTIONS
|
| 453 |
+
for _item in _raw:
|
| 454 |
+
_img = str(Path(_img_root) / _item["image_path"])
|
| 455 |
+
_crit = CRITERIA_ORDER[0]
|
| 456 |
+
_sample_items.append({
|
| 457 |
+
"image_path": _img,
|
| 458 |
+
"question": CRITERIA_QUESTIONS[_crit],
|
| 459 |
+
"gt_score": _item["criteria"].get(_crit, {}).get("score", "?"),
|
| 460 |
+
})
|
| 461 |
+
except Exception as _e:
|
| 462 |
+
rank0_print(f"[LocalLoggingCallback] Could not build sample items: {_e}")
|
| 463 |
+
|
| 464 |
+
_logging_cb = LocalLoggingCallback(
|
| 465 |
+
processor=processor,
|
| 466 |
+
sample_dataset=_sample_items,
|
| 467 |
+
sample_every_steps=training_args.sample_every_steps,
|
| 468 |
+
output_dir=training_args.output_dir,
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
trainer = AestheticTrainer(
|
| 472 |
+
model=model,
|
| 473 |
+
processing_class=tokenizer,
|
| 474 |
+
args=training_args,
|
| 475 |
+
score_loss=score_loss_fn,
|
| 476 |
+
tokenizer=tokenizer,
|
| 477 |
+
callbacks=[_logging_cb],
|
| 478 |
+
**data_module,
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
if list(pathlib.Path(training_args.output_dir).glob("checkpoint-*")):
|
| 482 |
+
logging.info("checkpoint found, resume training")
|
| 483 |
+
trainer.train(resume_from_checkpoint=True)
|
| 484 |
+
else:
|
| 485 |
+
trainer.train()
|
| 486 |
+
trainer.save_state()
|
| 487 |
+
|
| 488 |
+
model.config.use_cache = True
|
| 489 |
+
|
| 490 |
+
safe_save_model_for_hf_trainer(trainer=trainer, output_dir=training_args.output_dir)
|
| 491 |
+
|
| 492 |
+
processor.save_pretrained(training_args.output_dir)
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
if __name__ == "__main__":
|
| 496 |
+
train(attn_implementation="flash_attention_2")
|
scripts/sft_aesthetic.sh
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Training script for aesthetic scoring with custom score tokens.
|
| 4 |
+
#
|
| 5 |
+
# Each image is expanded into 14 independent samples:
|
| 6 |
+
# - 13 per-criterion samples (score range 1β10, bins=crit_score_bins)
|
| 7 |
+
# - 1 total-score sample (score range 0β100, bins=total_score_bins)
|
| 8 |
+
#
|
| 9 |
+
# Assistant output format (per sample):
|
| 10 |
+
# <think>{reason}</think>
|
| 11 |
+
# The score is <scoreK> β criteria sample
|
| 12 |
+
#
|
| 13 |
+
# The score is <totalK> β total sample
|
| 14 |
+
#
|
| 15 |
+
# Loss = CE_loss + crit_reg_weight * Huber(criteria) + total_reg_weight * Huber(total)
|
| 16 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
+
# export CUDA_VISIBLE_DEVICES=0
|
| 18 |
+
# ββ Distributed configuration (auto-detect) ββββββββββββββββββββββββββββββββββ
|
| 19 |
+
MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"}
|
| 20 |
+
MASTER_PORT=${MASTER_PORT:-$(shuf -i 20001-29999 -n 1)}
|
| 21 |
+
NNODES=${WORLD_SIZE:-1}
|
| 22 |
+
|
| 23 |
+
if [ -z "$NPROC_PER_NODE" ]; then
|
| 24 |
+
if command -v nvidia-smi &> /dev/null; then
|
| 25 |
+
NPROC_PER_NODE=$(nvidia-smi --list-gpus | wc -l)
|
| 26 |
+
else
|
| 27 |
+
NPROC_PER_NODE=1
|
| 28 |
+
fi
|
| 29 |
+
fi
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
export DS_BUILD_SHM_COMM=0 # Disable building shared memory communication ops
|
| 33 |
+
export DS_BUILD_OPS=0 # Disable building ops to avoid compilation errors
|
| 34 |
+
# ββ Paths βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
+
# Base model: Qwen3-VL-8B or a previously fine-tuned checkpoint
|
| 36 |
+
# LLM=/mnt/hdfs/zw04mlnn01/checkpoint/llm_platform/hldy/model/Qwen/Qwen3-VL-8B-Instruct/main
|
| 37 |
+
LLM=
|
| 38 |
+
# Image root directory (must contain all image_path entries in track_1_train.json)
|
| 39 |
+
IMAGE_DIR=zijielou/PortraitCraft/main
|
| 40 |
+
|
| 41 |
+
# Annotation file (image_path uses subdir format: images_00/xxx.jpg)
|
| 42 |
+
ANNOTATION=PortraitCraft/track_1_train_subdir.json
|
| 43 |
+
|
| 44 |
+
# Output
|
| 45 |
+
RUN_NAME="qwen3vl_aesthetic_score_tokens_0509"
|
| 46 |
+
OUTPUT_DIR=result/portrait/${RUN_NAME}
|
| 47 |
+
|
| 48 |
+
# DeepSpeed config
|
| 49 |
+
DEEPSPEED=./scripts/zero2.json
|
| 50 |
+
|
| 51 |
+
# ββ Hyper-parameters ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
+
LR=2e-5
|
| 53 |
+
BATCH_SIZE=2 # per GPU
|
| 54 |
+
GRAD_ACCUM=16 # effective batch = NPROC_PER_NODE * BATCH_SIZE * GRAD_ACCUM
|
| 55 |
+
NUM_EPOCHS=3
|
| 56 |
+
|
| 57 |
+
# ββ Score token settings ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
CRIT_BINS=20 # <score1>β¦<score20> covering 1.0β10.0
|
| 59 |
+
TOTAL_BINS=101 # <total1>β¦<total101> covering 0β100
|
| 60 |
+
|
| 61 |
+
# ββ Loss weights ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 62 |
+
CE_WEIGHT=1.0
|
| 63 |
+
CRIT_REG_WEIGHT=0.5 # regression loss weight for criteria samples
|
| 64 |
+
TOTAL_REG_WEIGHT=0.5 # regression loss weight for total-score samples
|
| 65 |
+
REG_LOSS_TYPE=huber # huber | mse
|
| 66 |
+
|
| 67 |
+
# ββ Image resolution ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 68 |
+
MAX_PIXELS=$((32 * 32 * 1024)) # ~451k pixels (~672px side)
|
| 69 |
+
MIN_PIXELS=$((32 * 32 * 224)) # ~12k pixels
|
| 70 |
+
IMAGE_MAX_TOKEN_NUM=1024
|
| 71 |
+
# ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 72 |
+
ENTRY=qwenvl/train/train_qwen.py
|
| 73 |
+
|
| 74 |
+
# ββ Update aesthetic data path in data registry βββββββββββββββββββββββββββββββ
|
| 75 |
+
# Patch the image_dir at runtime via environment variable so we don't hard-code
|
| 76 |
+
# it inside __init__.py. The AestheticScoringDataset reads IMAGE_DIR if set.
|
| 77 |
+
export AESTHETIC_IMAGE_DIR=${IMAGE_DIR}
|
| 78 |
+
export AESTHETIC_ANNOTATION=${ANNOTATION}
|
| 79 |
+
export WANDB_DISABLED=true
|
| 80 |
+
|
| 81 |
+
# ββ Auto-compute save_steps = half epoch ββββββββββββββββββββββββββββββββββββββ
|
| 82 |
+
NUM_IMAGES=$(python -c "import json; print(len(json.load(open('${ANNOTATION}'))))")
|
| 83 |
+
SAMPLES_PER_IMAGE=14 # 13 criteria + 1 total
|
| 84 |
+
TOTAL_SAMPLES=$((NUM_IMAGES * SAMPLES_PER_IMAGE))
|
| 85 |
+
EFFECTIVE_BATCH=$((NPROC_PER_NODE * NNODES * BATCH_SIZE * GRAD_ACCUM))
|
| 86 |
+
STEPS_PER_EPOCH=$(( (TOTAL_SAMPLES + EFFECTIVE_BATCH - 1) / EFFECTIVE_BATCH ))
|
| 87 |
+
SAVE_STEPS=$(( STEPS_PER_EPOCH / 2 ))
|
| 88 |
+
[ ${SAVE_STEPS} -lt 1 ] && SAVE_STEPS=1
|
| 89 |
+
echo "[auto] ${NUM_IMAGES} images Γ ${SAMPLES_PER_IMAGE} = ${TOTAL_SAMPLES} samples"
|
| 90 |
+
echo "[auto] effective_batch=${EFFECTIVE_BATCH}, steps/epoch=${STEPS_PER_EPOCH}, save_steps=${SAVE_STEPS} (half epoch)"
|
| 91 |
+
|
| 92 |
+
# ββ Training arguments ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 93 |
+
ARGS="
|
| 94 |
+
--model_name_or_path ${LLM} \
|
| 95 |
+
--use_score_tokens True \
|
| 96 |
+
--crit_score_bins ${CRIT_BINS} \
|
| 97 |
+
--total_score_bins ${TOTAL_BINS} \
|
| 98 |
+
--score_ce_weight ${CE_WEIGHT} \
|
| 99 |
+
--crit_reg_weight ${CRIT_REG_WEIGHT} \
|
| 100 |
+
--total_reg_weight ${TOTAL_REG_WEIGHT} \
|
| 101 |
+
--tune_mm_vision True \
|
| 102 |
+
--tune_mm_mlp True \
|
| 103 |
+
--tune_mm_llm True \
|
| 104 |
+
--bf16 True \
|
| 105 |
+
--output_dir ${OUTPUT_DIR} \
|
| 106 |
+
--num_train_epochs ${NUM_EPOCHS} \
|
| 107 |
+
--per_device_train_batch_size ${BATCH_SIZE} \
|
| 108 |
+
--per_device_eval_batch_size $((BATCH_SIZE * 2)) \
|
| 109 |
+
--gradient_accumulation_steps ${GRAD_ACCUM} \
|
| 110 |
+
--max_pixels ${MAX_PIXELS} \
|
| 111 |
+
--min_pixels ${MIN_PIXELS} \
|
| 112 |
+
--eval_strategy no \
|
| 113 |
+
--save_strategy steps \
|
| 114 |
+
--save_steps ${SAVE_STEPS} \
|
| 115 |
+
--save_total_limit 6 \
|
| 116 |
+
--learning_rate ${LR} \
|
| 117 |
+
--weight_decay 0.0 \
|
| 118 |
+
--warmup_ratio 0.03 \
|
| 119 |
+
--max_grad_norm 2.0 \
|
| 120 |
+
--lr_scheduler_type cosine \
|
| 121 |
+
--logging_steps 1 \
|
| 122 |
+
--model_max_length 4096 \
|
| 123 |
+
--gradient_checkpointing False \
|
| 124 |
+
--dataloader_num_workers 4 \
|
| 125 |
+
--run_name ${RUN_NAME} \
|
| 126 |
+
--sample_every_steps 500 \
|
| 127 |
+
--report_to none \
|
| 128 |
+
--deepspeed ${DEEPSPEED}
|
| 129 |
+
"
|
| 130 |
+
|
| 131 |
+
# ββ Launch ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 132 |
+
mkdir -p ${OUTPUT_DIR}
|
| 133 |
+
|
| 134 |
+
echo "============================================================"
|
| 135 |
+
echo " GPUs: ${NPROC_PER_NODE}, Nodes: ${NNODES}"
|
| 136 |
+
echo " Master: ${MASTER_ADDR}:${MASTER_PORT}"
|
| 137 |
+
echo " Output: ${OUTPUT_DIR}"
|
| 138 |
+
echo "============================================================"
|
| 139 |
+
|
| 140 |
+
torchrun \
|
| 141 |
+
--nproc_per_node=${NPROC_PER_NODE} \
|
| 142 |
+
--nnodes=${NNODES} \
|
| 143 |
+
--master_addr=${MASTER_ADDR} \
|
| 144 |
+
--master_port=${MASTER_PORT} \
|
| 145 |
+
${ENTRY} ${ARGS} 2>&1 | tee ${OUTPUT_DIR}/train.log
|
| 146 |
+
|
scripts/zero2.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fp16": {
|
| 3 |
+
"enabled": "auto",
|
| 4 |
+
"loss_scale": 0,
|
| 5 |
+
"loss_scale_window": 1000,
|
| 6 |
+
"initial_scale_power": 16,
|
| 7 |
+
"hysteresis": 2,
|
| 8 |
+
"min_loss_scale": 1
|
| 9 |
+
},
|
| 10 |
+
"bf16": {
|
| 11 |
+
"enabled": "auto"
|
| 12 |
+
},
|
| 13 |
+
"train_micro_batch_size_per_gpu": "auto",
|
| 14 |
+
"train_batch_size": "auto",
|
| 15 |
+
"gradient_accumulation_steps": "auto",
|
| 16 |
+
"zero_optimization": {
|
| 17 |
+
"stage": 2,
|
| 18 |
+
"overlap_comm": true,
|
| 19 |
+
"contiguous_gradients": true,
|
| 20 |
+
"sub_group_size": 1e9,
|
| 21 |
+
"reduce_bucket_size": "auto"
|
| 22 |
+
}
|
| 23 |
+
}
|