Spaces:
Running
Running
File size: 6,149 Bytes
f655146 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | # finetune/eval_ft.py
"""Stage 5 gate — side-by-side Bengali eval: fine-tuned MiniCPM-V (vLLM) vs the
native base model (the current shipping "Lever #1" path).
CLAUDE.md / finetune/README are explicit: ship the fine-tune ONLY if it clearly
beats the native path in a human read. The train metrics (loss/token_acc) say the
LoRA fit the distilled targets — they do NOT say Bengali quality improved. This
script produces the artifact a Bengali speaker needs to make that call before
FINETUNED_VISION_MODEL is ever set.
Fairness: BOTH paths get the EXACT same app-built Bengali prompt (build_story_prompt)
and the same image — exactly what each would receive in production. The only
difference under test is the model weights.
native: core.modal_infra.generate_story_remote → base openbmb/MiniCPM-V-4_5 (Ollama)
FT: finetune.serve_vllm.generate_story_ft_remote → merged LoRA (vLLM)
Held-out set: the 61 labelset images that the purity gate rejected, so they were
NEVER trained on, yet are on-distribution. (Override with --images for your own.)
Run:
uv run modal deploy finetune/serve_vllm.py # FT server must be live
uv run python finetune/eval_ft.py --n 10 # 10 held-out images
uv run python finetune/eval_ft.py --images path/to/dir --n 8 --style রূপকথা
Out:
finetune/eval_results/ft_vs_native_YYYYMMDD_HHMM.md
"""
import argparse
import base64
import json
import os
import sys
import time
from datetime import datetime
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.model_config import ACTIVE_STACK, get_vision_options
from core.modal_infra import generate_story_remote
from core.prompts import STYLES, build_story_prompt
from finetune.serve_vllm import generate_story_ft_remote
TRAIN_JSON = Path("finetune/data/train.json")
LABELSET = Path("finetune/data/labelset")
def held_out_images() -> list[Path]:
"""Labelset images that are NOT in train.json — unseen but on-distribution."""
trained = {x["image"].split("/")[-1] for x in json.loads(TRAIN_JSON.read_text())}
imgs = [
p for p in sorted(LABELSET.glob("*"))
if p.suffix.lower() in (".jpg", ".jpeg", ".png") and p.name not in trained
]
return imgs
def _encode(path: Path) -> str:
return base64.b64encode(path.read_bytes()).decode()
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--images", default=None, help="dir of images (default: held-out labelset)")
ap.add_argument("--n", type=int, default=10, help="number of images to evaluate")
ap.add_argument("--style", default="রূপকথা", choices=list(STYLES["bn"].keys()))
ap.add_argument("--instruction", default="একটা গল্প বলো")
args = ap.parse_args()
if args.images:
imgs = [
p for p in sorted(Path(args.images).glob("*"))
if p.suffix.lower() in (".jpg", ".jpeg", ".png")
]
else:
imgs = held_out_images()
imgs = imgs[: args.n]
if not imgs:
sys.exit("No images found to evaluate.")
print(f"Evaluating {len(imgs)} images · style={args.style} · stack={ACTIVE_STACK}", flush=True)
options = get_vision_options("bn")
# Precompute the (identical) prompt + encoded bytes per image.
items = []
for img in imgs:
prompt = build_story_prompt(
instruction=args.instruction,
language="bn",
style=args.style,
child_name="",
stack_key=ACTIVE_STACK,
num_images=1,
)
items.append((img, prompt, [_encode(img)]))
# Two phases so each serverless model cold-starts ONCE, not per image (the
# alternating native→FT loop kept scaling the other model back to zero).
print("Phase 1/2: native (base) ...", flush=True)
natives = []
for i, (img, prompt, b64) in enumerate(items, 1):
t0 = time.time()
story = (generate_story_remote(b64, prompt, options) or "").strip()
dt = round(time.time() - t0, 1)
print(f" native [{i}/{len(items)}] {img.name} {dt}s", flush=True)
natives.append((story, dt))
print("Phase 2/2: fine-tuned (vLLM) ...", flush=True)
fts = []
for i, (img, prompt, b64) in enumerate(items, 1):
t0 = time.time()
story = (generate_story_ft_remote(b64, prompt) or "").strip()
dt = round(time.time() - t0, 1)
print(f" ft [{i}/{len(items)}] {img.name} {dt}s", flush=True)
fts.append((story, dt))
rows = [
(items[i][0], natives[i][0], natives[i][1], fts[i][0], fts[i][1])
for i in range(len(items))
]
out_dir = Path("finetune/eval_results")
out_dir.mkdir(exist_ok=True)
fname = out_dir / f"ft_vs_native_{datetime.now():%Y%m%d_%H%M}.md"
lines = [
f"# FT vs Native — Bengali story quality ({args.style})",
f"Generated: {datetime.now():%Y-%m-%d %H:%M} · stack {ACTIVE_STACK} · {len(rows)} held-out images\n",
"**Native** = base openbmb/MiniCPM-V-4_5 (current shipping Lever #1). ",
"**FT** = merged Bengali LoRA via vLLM. Same prompt + image for both.\n",
"> For the Bengali reviewer: for each image, which story reads more like a real "
"grandmother's bedtime tale (natural words, রূপকথা imagery, no English/garbled "
"words, calm sleepy ending)? Mark a winner per row.\n",
"---\n",
]
for img, native, tn, ft, tf in rows:
lines += [
f"## {img.name}",
f"})\n",
f"### Native (base) — {tn}s",
native or "_(empty)_", "",
f"### FT (LoRA) — {tf}s",
ft or "_(empty)_", "",
"**Winner (reviewer):** ☐ Native ☐ FT ☐ Tie · notes: ____",
"\n---\n",
]
fname.write_text("\n".join(lines))
print(f"\nReport written to {fname}")
print("Open it, have a Bengali speaker mark winners, and only set "
"FINETUNED_VISION_MODEL if FT clearly wins.")
if __name__ == "__main__":
main()
|