AnhDau's picture
Export best bbox-indicator visual baseline Vietnamese | best BLEU=12.3542
9391100 verified
Raw
History Blame Contribute Delete
11.9 kB
import json
import random
from pathlib import Path
from typing import Dict, Optional
import pandas as pd
import sacrebleu
import torch
import torch.nn as nn
from torch.optim import AdamW
from tqdm.auto import tqdm
from .data import BBoxAwareImageCaptioningCollator
from .utils import cleanup_memory
def move_batch_to_device(batch, device):
output = {}
for key, value in batch.items():
if isinstance(value, torch.Tensor):
output[key] = value.to(device, non_blocking=True)
else:
output[key] = value
return output
def compute_loss_from_logits(logits, labels, label_smoothing=0.0):
vocab_size = logits.size(-1)
loss_fct = nn.CrossEntropyLoss(
ignore_index=-100,
label_smoothing=label_smoothing,
)
return loss_fct(
logits.reshape(-1, vocab_size),
labels.reshape(-1),
)
def make_optimizer(model, learning_rate=3e-5, weight_decay=0.01):
decay_params = []
no_decay_params = []
for name, param in model.named_parameters():
if not param.requires_grad:
continue
if (
name.endswith(".bias")
or "layernorm" in name.lower()
or "layer_norm" in name.lower()
or "norm" in name.lower()
):
no_decay_params.append(param)
else:
decay_params.append(param)
optimizer_grouped_parameters = [
{"params": decay_params, "weight_decay": weight_decay},
{"params": no_decay_params, "weight_decay": 0.0},
]
use_fused = torch.cuda.is_available()
try:
optimizer = AdamW(
optimizer_grouped_parameters,
lr=learning_rate,
betas=(0.9, 0.999),
eps=1e-8,
fused=use_fused,
)
except TypeError:
optimizer = AdamW(
optimizer_grouped_parameters,
lr=learning_rate,
betas=(0.9, 0.999),
eps=1e-8,
)
return optimizer
def select_eval_indices(dataset_size, max_samples=None, seed=42):
if max_samples is None or int(max_samples) >= dataset_size:
return list(range(dataset_size))
rng = random.Random(int(seed))
return rng.sample(range(dataset_size), int(max_samples))
def pick_diverse_examples(rows, num_examples=5, seed=42):
rng = random.Random(seed)
shuffled = list(rows)
rng.shuffle(shuffled)
selected = []
seen_images = set()
for row in shuffled:
if row["image_id"] in seen_images:
continue
selected.append(row)
seen_images.add(row["image_id"])
if len(selected) >= num_examples:
break
if len(selected) < num_examples:
for row in shuffled:
if row not in selected:
selected.append(row)
if len(selected) >= num_examples:
break
return selected
@torch.no_grad()
def evaluate_loss(
model,
dataloader,
device,
label_smoothing=0.0,
use_bf16=False,
use_fp16=False,
):
model.eval()
autocast_dtype = torch.bfloat16 if use_bf16 else torch.float16
total_loss = 0.0
total_batches = 0
for batch in tqdm(dataloader, desc="eval loss", leave=False):
batch = move_batch_to_device(batch, device)
with torch.amp.autocast(
device_type="cuda",
dtype=autocast_dtype,
enabled=(use_bf16 or use_fp16),
):
outputs = model(
pixel_values=batch["pixel_values"],
bbox_features=batch["bbox_features"],
labels=batch["labels"],
)
loss = compute_loss_from_logits(
logits=outputs.logits,
labels=batch["labels"],
label_smoothing=label_smoothing,
)
total_loss += float(loss.detach().cpu())
total_batches += 1
cleanup_memory()
return total_loss / max(1, total_batches)
@torch.no_grad()
def evaluate_generation(
model,
dataset,
tokenizer,
image_processor,
device,
tgt_lang_code="en_XX",
max_target_length=64,
eval_batch_size=128,
num_workers=4,
pin_memory=True,
max_samples=None,
num_examples=5,
sample_seed=42,
output_path=None,
split_name="val",
num_beams=4,
max_new_tokens=30,
repetition_penalty=1.1,
no_repeat_ngram_size=3,
length_penalty=1.0,
use_bf16=False,
use_fp16=False,
):
model.eval()
autocast_dtype = torch.bfloat16 if use_bf16 else torch.float16
indices = select_eval_indices(
dataset_size=len(dataset),
max_samples=max_samples,
seed=sample_seed,
)
eval_subset = torch.utils.data.Subset(dataset, indices)
eval_collator = BBoxAwareImageCaptioningCollator(
image_processor=image_processor,
tokenizer=tokenizer,
tgt_lang_code=tgt_lang_code,
max_target_length=max_target_length,
)
eval_loader = torch.utils.data.DataLoader(
eval_subset,
batch_size=eval_batch_size,
shuffle=False,
collate_fn=eval_collator,
num_workers=num_workers,
pin_memory=pin_memory,
)
predictions = []
references = []
rows = []
forced_bos_token_id = tokenizer.convert_tokens_to_ids(tgt_lang_code)
for batch in tqdm(eval_loader, desc=f"generation eval ({split_name})", leave=False):
batch = move_batch_to_device(batch, device)
with torch.amp.autocast(
device_type="cuda",
dtype=autocast_dtype,
enabled=(use_bf16 or use_fp16),
):
generated_ids = model.generate(
pixel_values=batch["pixel_values"],
bbox_features=batch["bbox_features"],
tokenizer=tokenizer,
num_beams=num_beams,
max_new_tokens=max_new_tokens,
forced_bos_token_id=forced_bos_token_id,
repetition_penalty=repetition_penalty,
no_repeat_ngram_size=no_repeat_ngram_size,
length_penalty=length_penalty,
)
batch_predictions = tokenizer.batch_decode(
generated_ids,
skip_special_tokens=True,
)
for image_id, region_id, raw_bbox, reference, prediction in zip(
batch["image_id"],
batch["region_id"],
batch["raw_bbox"],
batch["caption"],
batch_predictions,
):
pred = str(prediction).strip()
ref = str(reference).strip()
predictions.append(pred)
references.append(ref)
rows.append({
"split": split_name,
"image_id": image_id,
"region_id": region_id,
"raw_bbox": raw_bbox,
"ground_truth": ref,
"prediction": pred,
"is_empty": pred == "",
"same_as_ground_truth": pred.lower() == ref.lower(),
})
bleu = sacrebleu.corpus_bleu(predictions, [references]).score if predictions else float("nan")
chrf = sacrebleu.corpus_chrf(predictions, [references]).score if predictions else float("nan")
predictions_df = pd.DataFrame(rows)
if output_path is not None:
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
predictions_df.to_csv(output_path, index=False, encoding="utf-8-sig")
print(f"Full {split_name} predictions saved to: {output_path}")
print(f"Saved rows: {len(predictions_df):,}")
example_rows = pick_diverse_examples(
rows,
num_examples=num_examples,
seed=sample_seed,
)
cleanup_memory()
return {
"bleu": float(bleu),
"chrf": float(chrf),
"num_samples": len(predictions),
"examples": rows,
"qualitative_examples": example_rows,
"predictions_path": str(output_path) if output_path is not None else None,
}
def show_and_save_qualitative_examples(gen_metrics, epoch, output_dir):
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
qualitative_rows = gen_metrics.get("qualitative_examples", [])
qualitative_df = pd.DataFrame(qualitative_rows)
qualitative_path = output_dir / f"epoch_{epoch + 1:03d}_qualitative_predictions.csv"
qualitative_df.to_csv(qualitative_path, index=False, encoding="utf-8-sig")
print("Qualitative predictions saved to:", qualitative_path)
display_cols = [
"image_id",
"region_id",
"raw_bbox",
"ground_truth",
"prediction",
"is_empty",
]
existing_cols = [col for col in display_cols if col in qualitative_df.columns]
if len(qualitative_df) > 0:
return qualitative_df[existing_cols]
return qualitative_df
def summarize_prediction_diversity(predictions_df):
return (
predictions_df
.groupby("image_id")
.agg(
num_regions=("region_id", "count"),
unique_ground_truth=("ground_truth", "nunique"),
unique_predictions=("prediction", "nunique"),
first_prediction=("prediction", "first"),
)
.reset_index()
.sort_values(["num_regions", "unique_ground_truth"], ascending=False)
)
def train_one_epoch(
model,
train_loader,
optimizer,
scheduler,
scaler,
device,
epoch,
global_step,
grad_accum_steps=1,
max_grad_norm=1.0,
label_smoothing=0.0,
use_bf16=False,
use_fp16=False,
start_batch_idx=0,
):
model.train()
autocast_dtype = torch.bfloat16 if use_bf16 else torch.float16
running_loss = 0.0
processed_batches = 0
progress_bar = tqdm(
enumerate(train_loader),
total=len(train_loader),
desc=f"epoch {epoch + 1}",
)
optimizer.zero_grad(set_to_none=True)
for batch_idx, batch in progress_bar:
if batch_idx < start_batch_idx:
continue
batch = move_batch_to_device(batch, device)
with torch.amp.autocast(
device_type="cuda",
dtype=autocast_dtype,
enabled=(use_bf16 or use_fp16),
):
outputs = model(
pixel_values=batch["pixel_values"],
bbox_features=batch["bbox_features"],
labels=batch["labels"],
)
loss = compute_loss_from_logits(
logits=outputs.logits,
labels=batch["labels"],
label_smoothing=label_smoothing,
)
loss_for_backward = loss / grad_accum_steps
if use_fp16:
scaler.scale(loss_for_backward).backward()
else:
loss_for_backward.backward()
running_loss += float(loss.detach().cpu())
processed_batches += 1
should_step = (
((batch_idx + 1) % grad_accum_steps == 0)
or ((batch_idx + 1) == len(train_loader))
)
if should_step:
if use_fp16:
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(
[p for p in model.parameters() if p.requires_grad],
max_grad_norm,
)
if use_fp16:
scaler.step(optimizer)
scaler.update()
else:
optimizer.step()
scheduler.step()
optimizer.zero_grad(set_to_none=True)
global_step += 1
progress_bar.set_postfix(
{
"loss": f"{running_loss / max(1, processed_batches):.4f}",
"lr": f"{optimizer.param_groups[0]['lr']:.2e}",
"step": global_step,
}
)
avg_loss = running_loss / max(1, processed_batches)
return avg_loss, global_step