Spaces:
Configuration error
Configuration error
Commit ·
302e907
1
Parent(s): 7c00779
docs(plan): add Stage 0 eval-methodology gate to Option B plan
Browse files- Full_Step-By-Step_Plan__Option_B_.txt +323 -0
- Makefile +21 -0
- scripts/categorize_predictions.py +278 -0
- scripts/rescore_nltk_bleu.py +334 -0
Full_Step-By-Step_Plan__Option_B_.txt
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Full Step-By-Step Plan (Option B)
|
| 2 |
+
|
| 3 |
+
================================================================================
|
| 4 |
+
STAGE 0 — EVALUATION-METHODOLOGY GATE (run BEFORE Stage 1; may make Stage 1
|
| 5 |
+
unnecessary)
|
| 6 |
+
================================================================================
|
| 7 |
+
|
| 8 |
+
Purpose: before spending 3.5 GPU-hours retraining, test whether the apparent
|
| 9 |
+
gap to the IEEE BLEU-4 ~24 baseline is an evaluation-methodology artefact
|
| 10 |
+
rather than a genuine quality deficit. Two pre-registered, falsifiable tests:
|
| 11 |
+
|
| 12 |
+
Part A (BLEU) — rescore the EXISTING v2.0.0 predictions against all 5
|
| 13 |
+
COCO references (the committed predictions.jsonl has
|
| 14 |
+
only ~1.46 refs/image). Emits a pre-registered band:
|
| 15 |
+
DOMINANT / MAJOR-BUT-PARTIAL / MINOR.
|
| 16 |
+
Part B (qualitative) — blinded categorization of 30 predictions into
|
| 17 |
+
SPECIFIC-CORRECT / GENERIC-CORRECT / PARTIALLY-CORRECT
|
| 18 |
+
/ INCORRECT, run WITHOUT seeing Part A's number.
|
| 19 |
+
|
| 20 |
+
Combined verdict (see scripts/categorize_predictions.py docstring) decides
|
| 21 |
+
whether Stage 1 is REQUIRED, OPTIONAL, or UNNECESSARY. If "don't retrain" or
|
| 22 |
+
"ship without retraining", skip Stages 1-2 and go to Stage 7 (reframe).
|
| 23 |
+
|
| 24 |
+
Pre-registration discipline: both scripts embed their predictions/rubric in
|
| 25 |
+
their module docstrings and must be COMMITTED before being run. Part A is run
|
| 26 |
+
and committed first; Part B runs afterward so the BLEU number cannot bias the
|
| 27 |
+
qualitative read.
|
| 28 |
+
|
| 29 |
+
--------------------------------------------------------------------------------
|
| 30 |
+
Cell G0 — locate the COCO annotations file (path varies by dataset mount)
|
| 31 |
+
|
| 32 |
+
ANN=$(find /kaggle/input -maxdepth 6 -name "captions_train2017.json" 2>/dev/null | head -1)
|
| 33 |
+
echo "Annotations: $ANN"
|
| 34 |
+
# If empty, the coco-2017-dataset is not attached — add it in the right sidebar.
|
| 35 |
+
|
| 36 |
+
--------------------------------------------------------------------------------
|
| 37 |
+
Cell G1 — PART A: 5-ref BLEU rescore + pre-registered band
|
| 38 |
+
|
| 39 |
+
# Assumes the repo is already cloned + installed (see Stage 1 Cell 2/4). If
|
| 40 |
+
# running Stage 0 standalone, clone first:
|
| 41 |
+
# !git clone https://github.com/apoorvrajdev/image-captioning-system.git
|
| 42 |
+
# %cd image-captioning-system
|
| 43 |
+
!pip install -q nltk sacrebleu
|
| 44 |
+
|
| 45 |
+
import subprocess
|
| 46 |
+
ANN = subprocess.check_output(
|
| 47 |
+
"find /kaggle/input -maxdepth 6 -name captions_train2017.json 2>/dev/null | head -1",
|
| 48 |
+
shell=True).decode().strip()
|
| 49 |
+
print("Annotations:", ANN)
|
| 50 |
+
|
| 51 |
+
!python -m scripts.rescore_nltk_bleu \
|
| 52 |
+
--predictions-path results/stabilized-beam-w4-lp07-rp12/predictions.jsonl \
|
| 53 |
+
--coco-annotations "$ANN"
|
| 54 |
+
|
| 55 |
+
# Watch the line: "PRE-REGISTERED BAND (...): <DOMINANT|MAJOR-BUT-PARTIAL|MINOR>".
|
| 56 |
+
# The full table is also written to
|
| 57 |
+
# results/stabilized-beam-w4-lp07-rp12/metrics_5ref.json
|
| 58 |
+
# Commit metrics_5ref.json before running Part B.
|
| 59 |
+
|
| 60 |
+
--------------------------------------------------------------------------------
|
| 61 |
+
Cell G2 — PART B: blinded qualitative worklist (run AFTER Part A is committed)
|
| 62 |
+
|
| 63 |
+
# Blinding: do NOT open metrics_5ref.json before categorizing. This cell only
|
| 64 |
+
# PREPARES the sample (prints 30 predictions + their 5 refs, no metrics). The
|
| 65 |
+
# categorization itself is a judgment step done against the rubric in
|
| 66 |
+
# scripts/categorize_predictions.py — either by you, or by handing the printed
|
| 67 |
+
# worklist to a separate Claude Code turn. It never reads the BLEU output.
|
| 68 |
+
|
| 69 |
+
import subprocess
|
| 70 |
+
ANN = subprocess.check_output(
|
| 71 |
+
"find /kaggle/input -maxdepth 6 -name captions_train2017.json 2>/dev/null | head -1",
|
| 72 |
+
shell=True).decode().strip()
|
| 73 |
+
|
| 74 |
+
!python -m scripts.categorize_predictions \
|
| 75 |
+
--predictions-path results/stabilized-beam-w4-lp07-rp12/predictions.jsonl \
|
| 76 |
+
--coco-annotations "$ANN"
|
| 77 |
+
|
| 78 |
+
# After judging each sample, write a categories JSONL
|
| 79 |
+
# ({sample_id, category, justification}) and finalize:
|
| 80 |
+
# !python -m scripts.categorize_predictions \
|
| 81 |
+
# --coco-annotations "$ANN" \
|
| 82 |
+
# --categories results/stabilized-beam-w4-lp07-rp12/categories.jsonl
|
| 83 |
+
# Then apply the COMBINED DECISION RULE (categorize docstring) with the Part A
|
| 84 |
+
# band to decide whether to proceed to Stage 1.
|
| 85 |
+
|
| 86 |
+
--------------------------------------------------------------------------------
|
| 87 |
+
Stage 0 decision gate:
|
| 88 |
+
- Combined verdict "don't retrain" / "ship without retraining" -> SKIP Stage 1
|
| 89 |
+
and Stage 2; go straight to Stage 7 (README reframe).
|
| 90 |
+
- Combined verdict "retrain" -> proceed to Stage 1 below.
|
| 91 |
+
- "Flag for human review" -> stop and decide manually before any GPU spend.
|
| 92 |
+
|
| 93 |
+
================================================================================
|
| 94 |
+
|
| 95 |
+
Stage 1 - Kaggle Training (you do this in a browser):-
|
| 96 |
+
|
| 97 |
+
Step 1.1 - Create the Kaggle notebook
|
| 98 |
+
Go to https://www.kaggle.com -> + Create -> New Notebook
|
| 99 |
+
Right sidebar -> Settings:
|
| 100 |
+
Accelerator: GPU T4 x2
|
| 101 |
+
Internet: ON
|
| 102 |
+
Persistence: Files only
|
| 103 |
+
Add Data -> search awsaf49/coco-2017-dataset -> click Add
|
| 104 |
+
Rename notebook to: image-captioning-baseline-recipe-v3
|
| 105 |
+
|
| 106 |
+
Step 1.2 - Paste these cells in order
|
| 107 |
+
Cell 1 - Confirm dataset path
|
| 108 |
+
|
| 109 |
+
!ls /kaggle/input/datasets/awsaf49/
|
| 110 |
+
!find /kaggle/input -maxdepth 6 -name "captions_train2017.json" 2>/dev/null
|
| 111 |
+
|
| 112 |
+
Cell 2 - Clone repo (already pushed, current main has the right code)
|
| 113 |
+
|
| 114 |
+
!git clone https://github.com/apoorvrajdev/image-captioning-system.git
|
| 115 |
+
%cd image-captioning-system
|
| 116 |
+
|
| 117 |
+
Cell 3 - Install tf-keras legacy shim
|
| 118 |
+
|
| 119 |
+
!pip install -q tf-keras
|
| 120 |
+
import os
|
| 121 |
+
os.environ["TF_USE_LEGACY_KERAS"] = "1"
|
| 122 |
+
|
| 123 |
+
Cell 4 - Install project deps
|
| 124 |
+
|
| 125 |
+
import os
|
| 126 |
+
os.environ["TF_USE_LEGACY_KERAS"] = "1"
|
| 127 |
+
|
| 128 |
+
!sed -i '/^tensorflow/d' requirements.txt
|
| 129 |
+
!pip install -q -r requirements.txt -r requirements-dev.txt -r requirements-eval.txt
|
| 130 |
+
!pip install -q --no-deps -e .
|
| 131 |
+
|
| 132 |
+
import tensorflow as tf
|
| 133 |
+
import tf_keras
|
| 134 |
+
print("TF:", tf.__version__, "| tf_keras:", tf_keras.__version__, "| GPUs:", tf.config.list_physical_devices("GPU"))
|
| 135 |
+
|
| 136 |
+
# Expect: TF 2.19, tf_keras 2.x, 2 GPUs.
|
| 137 |
+
|
| 138 |
+
Cell 5 - Patch base.yaml with Kaggle dataset path
|
| 139 |
+
# NOTE: confirm the real mount from Cell 1's find output; the awsaf49 dataset
|
| 140 |
+
# commonly mounts at /kaggle/input/coco-2017-dataset/coco2017 (no datasets/awsaf49 segment).
|
| 141 |
+
|
| 142 |
+
!sed -i 's|base_path: data/coco2017|base_path: /kaggle/input/datasets/awsaf49/coco-2017-dataset/coco2017|' configs/base.yaml
|
| 143 |
+
!grep base_path configs/base.yaml
|
| 144 |
+
|
| 145 |
+
Cell 6 - TRAIN with base.yaml (the actual training, ~3.3h)
|
| 146 |
+
|
| 147 |
+
import os
|
| 148 |
+
os.environ["CAPTIONING__DATA__BASE_PATH"] = "/kaggle/input/datasets/awsaf49/coco-2017-dataset/coco2017"
|
| 149 |
+
|
| 150 |
+
!python -m scripts.train \
|
| 151 |
+
--config configs/base.yaml \
|
| 152 |
+
--output-dir outputs/runs/baseline
|
| 153 |
+
|
| 154 |
+
# Watch for: Epoch 1 loss ~3.6. EarlyStopping likely fires ~epoch 5-7 with restore_best_weights=True.
|
| 155 |
+
|
| 156 |
+
Cell 7 - Copy artefacts to versioned dir
|
| 157 |
+
|
| 158 |
+
!mkdir -p models/v3.0.0
|
| 159 |
+
!cp outputs/runs/baseline/best.h5 models/v3.0.0/model.h5
|
| 160 |
+
!cp outputs/runs/baseline/vocab.pkl models/v3.0.0/vocab.pkl
|
| 161 |
+
!cp outputs/runs/baseline/vocab.json models/v3.0.0/vocab.json
|
| 162 |
+
!ls -la models/v3.0.0/
|
| 163 |
+
|
| 164 |
+
Cell 8 - Greedy evaluation
|
| 165 |
+
|
| 166 |
+
import os
|
| 167 |
+
os.environ["CAPTIONING__DATA__BASE_PATH"] = "/kaggle/input/datasets/awsaf49/coco-2017-dataset/coco2017"
|
| 168 |
+
|
| 169 |
+
!python -m scripts.evaluate \
|
| 170 |
+
--config configs/base.yaml \
|
| 171 |
+
--weights models/v3.0.0/model.h5 \
|
| 172 |
+
--tokenizer-dir models/v3.0.0 \
|
| 173 |
+
--results-root results \
|
| 174 |
+
--run-id baseline-greedy \
|
| 175 |
+
--model-id inceptionv3-transformer-baseline \
|
| 176 |
+
--decode-strategy greedy \
|
| 177 |
+
--max-samples 500
|
| 178 |
+
|
| 179 |
+
Cell 9 - Beam evaluation (same params as v2.0.0 for apples-to-apples)
|
| 180 |
+
|
| 181 |
+
import os
|
| 182 |
+
os.environ["CAPTIONING__DATA__BASE_PATH"] = "/kaggle/input/datasets/awsaf49/coco-2017-dataset/coco2017"
|
| 183 |
+
|
| 184 |
+
!python -m scripts.evaluate \
|
| 185 |
+
--config configs/base.yaml \
|
| 186 |
+
--weights models/v3.0.0/model.h5 \
|
| 187 |
+
--tokenizer-dir models/v3.0.0 \
|
| 188 |
+
--results-root results \
|
| 189 |
+
--run-id baseline-beam-w4-lp07-rp12 \
|
| 190 |
+
--model-id inceptionv3-transformer-baseline \
|
| 191 |
+
--decode-strategy beam \
|
| 192 |
+
--beam-width 4 \
|
| 193 |
+
--length-penalty 0.7 \
|
| 194 |
+
--repetition-penalty 1.2 \
|
| 195 |
+
--max-samples 500
|
| 196 |
+
|
| 197 |
+
Cell 10 - Qualitative samples (30 random predictions)
|
| 198 |
+
|
| 199 |
+
!python -m scripts.inspect_predictions \
|
| 200 |
+
--config configs/base.yaml \
|
| 201 |
+
--weights models/v3.0.0/model.h5 \
|
| 202 |
+
--tokenizer-dir models/v3.0.0 \
|
| 203 |
+
--decode-strategy beam \
|
| 204 |
+
--beam-width 4 \
|
| 205 |
+
--n-samples 30 \
|
| 206 |
+
--output results/baseline-beam-w4-lp07-rp12/qualitative.jsonl
|
| 207 |
+
|
| 208 |
+
Cell 11 - Print metrics summary + side-by-side vs v2.0.0
|
| 209 |
+
|
| 210 |
+
import json
|
| 211 |
+
|
| 212 |
+
print("=" * 70)
|
| 213 |
+
print("BASELINE RECIPE (v3.0.0) - configs/base.yaml")
|
| 214 |
+
print("=" * 70)
|
| 215 |
+
for run in ("baseline-greedy", "baseline-beam-w4-lp07-rp12"):
|
| 216 |
+
m = json.load(open(f"results/{run}/metrics.json"))
|
| 217 |
+
print(f"\n{run}:")
|
| 218 |
+
for k in ("bleu1", "bleu2", "bleu3", "bleu4", "rouge_l", "meteor", "cider"):
|
| 219 |
+
print(f" {k:10s} = {m[k]:.4f}")
|
| 220 |
+
|
| 221 |
+
print("\n" + "=" * 70)
|
| 222 |
+
print("STABILIZED RECIPE (v2.0.0) - for comparison")
|
| 223 |
+
print("=" * 70)
|
| 224 |
+
print("""
|
| 225 |
+
stabilized-greedy:
|
| 226 |
+
bleu1 = 42.20, bleu4 = 10.57, rouge_l = 37.57, meteor = 15.45, cider = 0.789
|
| 227 |
+
stabilized-beam-w4-lp07-rp12:
|
| 228 |
+
bleu1 = 41.93, bleu4 = 10.39, rouge_l = 36.84, meteor = 15.56, cider = 0.826
|
| 229 |
+
""")
|
| 230 |
+
|
| 231 |
+
Cell 12 - Package handoff zip
|
| 232 |
+
|
| 233 |
+
!mkdir -p /kaggle/working/handoff
|
| 234 |
+
!cp -r results /kaggle/working/handoff/
|
| 235 |
+
!cp -r models /kaggle/working/handoff/
|
| 236 |
+
!cp outputs/runs/baseline/history.json /kaggle/working/handoff/ 2>/dev/null || true
|
| 237 |
+
!cd /kaggle/working && zip -r /kaggle/working/handoff-baseline.zip handoff/
|
| 238 |
+
!ls -la /kaggle/working/handoff-baseline.zip
|
| 239 |
+
|
| 240 |
+
Step 1.3 - Run the notebook
|
| 241 |
+
Hit Run All -> wait ~3.5 hours
|
| 242 |
+
Save Version -> "Save & Run All (Commit)" so the outputs persist
|
| 243 |
+
After completion, Output tab -> download handoff-baseline.zip
|
| 244 |
+
|
| 245 |
+
Stage 2 - Decision Gate (1 minute, you do this looking at Cell 11 output)
|
| 246 |
+
Look at the CIDEr numbers in Cell 11:
|
| 247 |
+
|
| 248 |
+
Outcome / What to do
|
| 249 |
+
base.yaml CIDEr >= 0.88 (significantly better than 0.826) -> Parity validated.
|
| 250 |
+
Proceed to Stage 3. Reframe as ablation in README.
|
| 251 |
+
base.yaml CIDEr 0.83-0.87 (similar to stabilized) -> Mixed signal. Check Cell 10
|
| 252 |
+
qualitative captions -> if they look more specific, still ship. Otherwise
|
| 253 |
+
pause and investigate.
|
| 254 |
+
base.yaml CIDEr < 0.82 (worse than stabilized) -> Surprise. Don't ship. There's
|
| 255 |
+
a hidden divergence we missed.
|
| 256 |
+
Also check Cell 10 qualitative samples - look for confident, specific captions
|
| 257 |
+
("a woman riding a brown horse on a beach") vs generic ones ("a person on a
|
| 258 |
+
beach"). This matters more than the metric delta for the live demo.
|
| 259 |
+
|
| 260 |
+
Stage 3 - Local Setup (5 min, you do this on your machine)
|
| 261 |
+
|
| 262 |
+
Step 3.1 - Download + extract handoff zip
|
| 263 |
+
|
| 264 |
+
mkdir -p "/d/PROJECT/New folder/handoff-baseline"
|
| 265 |
+
cd "/d/PROJECT/New folder/handoff-baseline"
|
| 266 |
+
unzip -o handoff-baseline.zip
|
| 267 |
+
ls -la handoff/
|
| 268 |
+
ls -la handoff/models/v3.0.0/
|
| 269 |
+
ls -la handoff/results/
|
| 270 |
+
|
| 271 |
+
# Expect: model.h5 (~227MB), vocab.json, vocab.pkl, two baseline-* results dirs, history.json.
|
| 272 |
+
|
| 273 |
+
Step 3.2 - Sanity check the metrics
|
| 274 |
+
|
| 275 |
+
cat handoff/results/baseline-greedy/metrics.json | python -m json.tool
|
| 276 |
+
cat handoff/results/baseline-beam-w4-lp07-rp12/metrics.json | python -m json.tool
|
| 277 |
+
|
| 278 |
+
Stage 4 - Upload to HF Hub (5 min)
|
| 279 |
+
|
| 280 |
+
Step 4.1 - Upload v3.0.0 weights
|
| 281 |
+
|
| 282 |
+
cd "/d/PROJECT/New folder/handoff-baseline/handoff"
|
| 283 |
+
hf auth whoami || hf auth login
|
| 284 |
+
hf upload apoorvrajdev/captioning-inceptionv3-transformer models/v3.0.0/model.h5 model.h5
|
| 285 |
+
hf upload apoorvrajdev/captioning-inceptionv3-transformer models/v3.0.0/vocab.json vocab.json
|
| 286 |
+
hf upload apoorvrajdev/captioning-inceptionv3-transformer models/v3.0.0/vocab.pkl vocab.pkl
|
| 287 |
+
|
| 288 |
+
Step 4.2 - Tag as v3.0.0
|
| 289 |
+
|
| 290 |
+
python -c "from huggingface_hub import HfApi; HfApi().create_tag('apoorvrajdev/captioning-inceptionv3-transformer', tag='v3.0.0')"
|
| 291 |
+
|
| 292 |
+
# Verify at https://huggingface.co/apoorvrajdev/captioning-inceptionv3-transformer/tags
|
| 293 |
+
|
| 294 |
+
Stage 5 - Flip Live Demo to v3.0.0 (2 min, browser)
|
| 295 |
+
Go to https://huggingface.co/spaces/apoorvrajdev/image-captioning-api -> Settings
|
| 296 |
+
Variables and secrets -> find BACKEND_WEIGHTS_HUB_REVISION
|
| 297 |
+
Click edit -> change from v2.0.0 -> v3.0.0 -> Save
|
| 298 |
+
Space auto-rebuilds (~2 min). Watch the Logs tab -> snapshot_download pulling
|
| 299 |
+
v3.0.0 then model_loaded: true.
|
| 300 |
+
Test: https://image-captioning-system.vercel.app -> drop an image -> check quality.
|
| 301 |
+
|
| 302 |
+
Stage 6 - Commit Results to Repo (5 min)
|
| 303 |
+
|
| 304 |
+
cd /d/PROJECT/image-captioning-system-main
|
| 305 |
+
cp -r "/d/PROJECT/New folder/handoff-baseline/handoff/results/baseline-greedy" results/
|
| 306 |
+
cp -r "/d/PROJECT/New folder/handoff-baseline/handoff/results/baseline-beam-w4-lp07-rp12" results/
|
| 307 |
+
git add results/baseline-greedy results/baseline-beam-w4-lp07-rp12
|
| 308 |
+
git status
|
| 309 |
+
|
| 310 |
+
# Suggested commit (you run it):
|
| 311 |
+
# git commit -m "feat(results): add baseline-recipe COCO eval (v3.0.0 checkpoint)"
|
| 312 |
+
# git push origin main
|
| 313 |
+
|
| 314 |
+
Stage 7 - Reframe README as Ablation (no Kaggle needed)
|
| 315 |
+
Once Stage 6 is done (or once the Stage 0 gate says "don't retrain"), rewrite
|
| 316 |
+
the README's "Model Quality" section as a recipe ablation:
|
| 317 |
+
Original recipe (v3.0.0) - constant Adam(1e-3), no label smoothing, EarlyStopping
|
| 318 |
+
Stabilized recipe (v2.0.0) - cosine LR + warmup + label smoothing 0.1 + dropout-off val
|
| 319 |
+
Side-by-side table of BLEU-1..4 / CIDEr / METEOR / ROUGE-L (greedy + beam) for both
|
| 320 |
+
Honest explanation: stabilization tricks designed for large-data regimes hurt
|
| 321 |
+
small-data captioning. The portfolio story becomes "I ran an ablation and
|
| 322 |
+
learned when modern recipes don't help" — and, from Stage 0, "I separated a
|
| 323 |
+
metric-methodology artefact from a genuine quality gap before spending GPU time."
|
Makefile
CHANGED
|
@@ -115,6 +115,27 @@ eval: ## Evaluate the latest model on COCO val (BLEU, CIDEr, METEOR, ROUGE)
|
|
| 115 |
predict: ## CLI single-image inference (usage: make predict IMAGE=path/to/img.jpg)
|
| 116 |
$(PYTHON) -m scripts.predict --image $(IMAGE)
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
# =============================================================================
|
| 119 |
# Backend (FastAPI)
|
| 120 |
# =============================================================================
|
|
|
|
| 115 |
predict: ## CLI single-image inference (usage: make predict IMAGE=path/to/img.jpg)
|
| 116 |
$(PYTHON) -m scripts.predict --image $(IMAGE)
|
| 117 |
|
| 118 |
+
# =============================================================================
|
| 119 |
+
# Evaluation-methodology gate (Phase 1b — run BEFORE Kaggle Stage 1 retrain)
|
| 120 |
+
# =============================================================================
|
| 121 |
+
# Both targets need the official COCO 2017 captions file. Download + extract:
|
| 122 |
+
# curl -L -o ann.zip https://images.cocodataset.org/annotations/annotations_trainval2017.zip
|
| 123 |
+
# unzip ann.zip # -> annotations/captions_train2017.json
|
| 124 |
+
# Then pass its path via COCO_ANNOTATIONS, e.g.:
|
| 125 |
+
# make rescore-5ref COCO_ANNOTATIONS=annotations/captions_train2017.json
|
| 126 |
+
|
| 127 |
+
.PHONY: rescore-5ref
|
| 128 |
+
rescore-5ref: ## Part A: 5-ref BLEU rescore + pre-registered band (needs COCO_ANNOTATIONS=...)
|
| 129 |
+
$(PYTHON) -m scripts.rescore_nltk_bleu \
|
| 130 |
+
--predictions-path results/stabilized-beam-w4-lp07-rp12/predictions.jsonl \
|
| 131 |
+
--coco-annotations $(COCO_ANNOTATIONS)
|
| 132 |
+
|
| 133 |
+
.PHONY: categorize-30
|
| 134 |
+
categorize-30: ## Part B: blinded 30-sample categorization worklist (needs COCO_ANNOTATIONS=...)
|
| 135 |
+
$(PYTHON) -m scripts.categorize_predictions \
|
| 136 |
+
--predictions-path results/stabilized-beam-w4-lp07-rp12/predictions.jsonl \
|
| 137 |
+
--coco-annotations $(COCO_ANNOTATIONS)
|
| 138 |
+
|
| 139 |
# =============================================================================
|
| 140 |
# Backend (FastAPI)
|
| 141 |
# =============================================================================
|
scripts/categorize_predictions.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Blinded qualitative categorization of caption predictions (Part B).
|
| 2 |
+
|
| 3 |
+
This is the qualitative half of the two-part evaluation-methodology gate. It
|
| 4 |
+
tests CIDEr-style image-specificity: does the checkpoint produce captions that
|
| 5 |
+
meaningfully constrain the image, or generic category labels? It runs in a
|
| 6 |
+
SEPARATE Claude Code turn from the BLEU rescore so the BLEU number cannot bias
|
| 7 |
+
the categorization. By construction this script:
|
| 8 |
+
|
| 9 |
+
* does NOT import scripts/rescore_nltk_bleu.py,
|
| 10 |
+
* does NOT read results/.../metrics_5ref.json (or any BLEU output),
|
| 11 |
+
* shares no state with Part A.
|
| 12 |
+
|
| 13 |
+
The blinding is structural: when the sample is drawn from qualitative.jsonl,
|
| 14 |
+
the per-sample metric fields (sentence_bleu4, sentence_rouge_l, flags, ...) are
|
| 15 |
+
dropped; only ``image`` and ``prediction`` are carried forward, then the full
|
| 16 |
+
5-reference set is joined from the official COCO annotations.
|
| 17 |
+
|
| 18 |
+
Workflow (two modes)
|
| 19 |
+
--------------------
|
| 20 |
+
PREPARE (default, no --categories):
|
| 21 |
+
Select the blinded sample, join 5 COCO refs, PRINT each sample as a
|
| 22 |
+
numbered block, and write a pending --output (category/justification
|
| 23 |
+
= null). The operator/agent then categorizes each sample BY JUDGMENT
|
| 24 |
+
using the rubric below.
|
| 25 |
+
FINALIZE (--categories PATH):
|
| 26 |
+
Read a JSONL of {sample_id, category, justification}, validate every
|
| 27 |
+
category against the four allowed values (invented categories raise),
|
| 28 |
+
enforce the <=25-word justification limit, merge, write the final
|
| 29 |
+
--output, and print category COUNTS (never naked percentages).
|
| 30 |
+
|
| 31 |
+
CATEGORIZATION RUBRIC
|
| 32 |
+
---------------------
|
| 33 |
+
Each prediction is assigned EXACTLY ONE category:
|
| 34 |
+
|
| 35 |
+
SPECIFIC-CORRECT
|
| 36 |
+
Caption identifies the main subject correctly AND includes at least one
|
| 37 |
+
distinguishing attribute that meaningfully constrains the image — color,
|
| 38 |
+
count, named action, spatial relation, or a named secondary object.
|
| 39 |
+
Example: "a woman riding a brown horse on a beach."
|
| 40 |
+
|
| 41 |
+
GENERIC-CORRECT
|
| 42 |
+
Caption identifies the main subject correctly but lacks any distinguishing
|
| 43 |
+
attribute — the same caption could describe many photos of the category.
|
| 44 |
+
Example: "a person on a beach."
|
| 45 |
+
|
| 46 |
+
PARTIALLY-CORRECT
|
| 47 |
+
Identifies at least one element correctly and gets at least one other
|
| 48 |
+
element wrong (wrong color, wrong count, wrong action, wrong secondary
|
| 49 |
+
object). Example for an image of two skiers: "a man riding skis."
|
| 50 |
+
|
| 51 |
+
INCORRECT
|
| 52 |
+
Misidentifies the main subject, is incoherent, or describes a scene
|
| 53 |
+
clearly absent from all 5 references.
|
| 54 |
+
|
| 55 |
+
COMBINED DECISION RULE (BLEU verdict from Part A + SPECIFIC-CORRECT count from
|
| 56 |
+
Part B):
|
| 57 |
+
|
| 58 |
+
BLEU DOMINANT (>=18) AND SPECIFIC-CORRECT >= 12/30
|
| 59 |
+
-> Strong evidence for the metric-parity reframe. Don't retrain.
|
| 60 |
+
Pivot to Stage 7 reframe in the plan.
|
| 61 |
+
|
| 62 |
+
BLEU MAJOR-BUT-PARTIAL (14-18) AND SPECIFIC-CORRECT >= 15/30
|
| 63 |
+
-> Qualitative is strong enough to ship without retraining.
|
| 64 |
+
Reframe with a "BLEU underestimates this checkpoint" note.
|
| 65 |
+
|
| 66 |
+
BLEU MAJOR-BUT-PARTIAL (14-18) AND SPECIFIC-CORRECT < 10/30
|
| 67 |
+
-> Qualitative confirms BLEU. Retrain (Kaggle Stage 1).
|
| 68 |
+
|
| 69 |
+
BLEU MINOR (<=13)
|
| 70 |
+
-> Retrain regardless of qualitative. Metric gap is too large to argue
|
| 71 |
+
around with 30 samples.
|
| 72 |
+
|
| 73 |
+
Any combination not covered above (e.g. DOMINANT with SPECIFIC-CORRECT <12,
|
| 74 |
+
or MAJOR-BUT-PARTIAL with SPECIFIC-CORRECT 10-14)
|
| 75 |
+
-> Flag for human review. Do not auto-decide.
|
| 76 |
+
|
| 77 |
+
Usage
|
| 78 |
+
-----
|
| 79 |
+
# PREPARE the blinded worklist:
|
| 80 |
+
python -m scripts.categorize_predictions \
|
| 81 |
+
--coco-annotations /path/to/captions_train2017.json
|
| 82 |
+
|
| 83 |
+
# FINALIZE after judgment:
|
| 84 |
+
python -m scripts.categorize_predictions \
|
| 85 |
+
--coco-annotations /path/to/captions_train2017.json \
|
| 86 |
+
--categories results/stabilized-beam-w4-lp07-rp12/categories.jsonl
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
from __future__ import annotations
|
| 90 |
+
|
| 91 |
+
import json
|
| 92 |
+
import random
|
| 93 |
+
from collections import Counter
|
| 94 |
+
from pathlib import Path
|
| 95 |
+
|
| 96 |
+
import click
|
| 97 |
+
|
| 98 |
+
# The four categories are fixed. Renaming or adding to this set (e.g.
|
| 99 |
+
# "ALMOST-SPECIFIC") is forbidden — finalize mode raises on any other value.
|
| 100 |
+
ALLOWED_CATEGORIES = ("SPECIFIC-CORRECT", "GENERIC-CORRECT", "PARTIALLY-CORRECT", "INCORRECT")
|
| 101 |
+
MAX_JUSTIFICATION_WORDS = 25
|
| 102 |
+
|
| 103 |
+
_RESULTS_DIR = Path("results/stabilized-beam-w4-lp07-rp12")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _image_id(image_path: str) -> int:
|
| 107 |
+
return int(Path(image_path).stem)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _load_coco_refs(path: Path) -> dict[int, list[str]]:
|
| 111 |
+
data = json.loads(path.read_text(encoding="utf-8"))
|
| 112 |
+
refs: dict[int, list[str]] = {}
|
| 113 |
+
for ann in data["annotations"]:
|
| 114 |
+
refs.setdefault(int(ann["image_id"]), []).append(ann["caption"])
|
| 115 |
+
return refs
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _read_jsonl(path: Path) -> list[dict]:
|
| 119 |
+
rows: list[dict] = []
|
| 120 |
+
with path.open(encoding="utf-8") as f:
|
| 121 |
+
for line in f:
|
| 122 |
+
line = line.strip()
|
| 123 |
+
if line:
|
| 124 |
+
rows.append(json.loads(line))
|
| 125 |
+
return rows
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _select_sample(predictions_path: Path, sample_size: int, seed: int) -> list[dict]:
|
| 129 |
+
"""Return [{image, prediction}] for the blinded sample (metrics dropped)."""
|
| 130 |
+
qualitative = _RESULTS_DIR / "qualitative.jsonl"
|
| 131 |
+
if qualitative.exists():
|
| 132 |
+
rows = _read_jsonl(qualitative)
|
| 133 |
+
if len(rows) >= sample_size:
|
| 134 |
+
# Blinding: carry ONLY image + prediction; drop sentence_bleu4,
|
| 135 |
+
# sentence_rouge_l, flags, length_tokens, etc.
|
| 136 |
+
return [
|
| 137 |
+
{"image": r["image"], "prediction": r["prediction"]} for r in rows[:sample_size]
|
| 138 |
+
]
|
| 139 |
+
rows = _read_jsonl(predictions_path)
|
| 140 |
+
picks = random.Random(seed).sample(rows, k=min(sample_size, len(rows)))
|
| 141 |
+
return [{"image": r["image"], "prediction": r["prediction"]} for r in picks]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _join_refs(sample: list[dict], coco: dict[int, list[str]]) -> list[dict]:
|
| 145 |
+
"""Attach the full COCO ref list; raise (no fallback) on missing ids."""
|
| 146 |
+
missing = [_image_id(s["image"]) for s in sample if _image_id(s["image"]) not in coco]
|
| 147 |
+
if missing:
|
| 148 |
+
raise click.ClickException(
|
| 149 |
+
f"{len(missing)} sample image_id(s) absent from the annotations file. "
|
| 150 |
+
f"First 5: {missing[:5]}. Refusing to proceed with partial references."
|
| 151 |
+
)
|
| 152 |
+
out: list[dict] = []
|
| 153 |
+
for s in sample:
|
| 154 |
+
iid = _image_id(s["image"])
|
| 155 |
+
out.append({"sample_id": str(iid), "prediction": s["prediction"], "refs": coco[iid]})
|
| 156 |
+
return out
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@click.command()
|
| 160 |
+
@click.option(
|
| 161 |
+
"--predictions-path",
|
| 162 |
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
| 163 |
+
default=_RESULTS_DIR / "predictions.jsonl",
|
| 164 |
+
help="predictions.jsonl (fallback sample source if qualitative.jsonl is absent).",
|
| 165 |
+
)
|
| 166 |
+
@click.option(
|
| 167 |
+
"--coco-annotations",
|
| 168 |
+
required=True,
|
| 169 |
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
| 170 |
+
help="captions_train2017.json — full 5-reference set per image.",
|
| 171 |
+
)
|
| 172 |
+
@click.option("--sample-size", type=int, default=30, help="Number of samples to categorize.")
|
| 173 |
+
@click.option("--seed", type=int, default=42, help="RNG seed (only used for the fallback sampler).")
|
| 174 |
+
@click.option(
|
| 175 |
+
"--output",
|
| 176 |
+
"output_path",
|
| 177 |
+
type=click.Path(path_type=Path),
|
| 178 |
+
default=_RESULTS_DIR / "qualitative_categorized.jsonl",
|
| 179 |
+
help="Where the categorized rows are written.",
|
| 180 |
+
)
|
| 181 |
+
@click.option(
|
| 182 |
+
"--categories",
|
| 183 |
+
"categories_path",
|
| 184 |
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
| 185 |
+
default=None,
|
| 186 |
+
help="JSONL of {sample_id, category, justification}. Omit for PREPARE mode.",
|
| 187 |
+
)
|
| 188 |
+
def main(
|
| 189 |
+
predictions_path: Path,
|
| 190 |
+
coco_annotations: Path,
|
| 191 |
+
sample_size: int,
|
| 192 |
+
seed: int,
|
| 193 |
+
output_path: Path,
|
| 194 |
+
categories_path: Path | None,
|
| 195 |
+
) -> None:
|
| 196 |
+
"""Prepare a blinded sample (default) or finalize a categorized sample."""
|
| 197 |
+
coco = _load_coco_refs(coco_annotations)
|
| 198 |
+
sample = _select_sample(predictions_path, sample_size, seed)
|
| 199 |
+
joined = _join_refs(sample, coco)
|
| 200 |
+
n = len(joined)
|
| 201 |
+
|
| 202 |
+
if categories_path is None:
|
| 203 |
+
# ---- PREPARE: print blinded worklist, write pending output ---------
|
| 204 |
+
click.echo(f"Blinded worklist: {n} samples (predictions + 5 refs only; NO metrics shown).")
|
| 205 |
+
click.echo("Categorize each per the rubric in this script's docstring, then re-run")
|
| 206 |
+
click.echo("with --categories pointing at a JSONL of {sample_id, category, justification}.")
|
| 207 |
+
click.echo("=" * 80)
|
| 208 |
+
for i, row in enumerate(joined, start=1):
|
| 209 |
+
click.echo(f"[{i:>2}] sample_id={row['sample_id']}")
|
| 210 |
+
click.echo(f" PRED: {row['prediction']}")
|
| 211 |
+
for j, ref in enumerate(row["refs"], start=1):
|
| 212 |
+
click.echo(f" ref{j}: {ref}")
|
| 213 |
+
click.echo("-" * 80)
|
| 214 |
+
pending = [
|
| 215 |
+
{
|
| 216 |
+
"sample_id": r["sample_id"],
|
| 217 |
+
"prediction": r["prediction"],
|
| 218 |
+
"refs": r["refs"],
|
| 219 |
+
"category": None,
|
| 220 |
+
"justification": None,
|
| 221 |
+
}
|
| 222 |
+
for r in joined
|
| 223 |
+
]
|
| 224 |
+
with output_path.open("w", encoding="utf-8") as f:
|
| 225 |
+
for row in pending:
|
| 226 |
+
f.write(json.dumps(row) + "\n")
|
| 227 |
+
click.echo(f"Wrote pending worklist ({n} rows, categories null): {output_path}")
|
| 228 |
+
return
|
| 229 |
+
|
| 230 |
+
# ---- FINALIZE: validate, merge, write, count ---------------------------
|
| 231 |
+
cat_rows = {r["sample_id"]: r for r in _read_jsonl(categories_path)}
|
| 232 |
+
final: list[dict] = []
|
| 233 |
+
for row in joined:
|
| 234 |
+
sid = row["sample_id"]
|
| 235 |
+
if sid not in cat_rows:
|
| 236 |
+
raise click.ClickException(f"sample_id {sid} missing from {categories_path}.")
|
| 237 |
+
category = cat_rows[sid]["category"]
|
| 238 |
+
justification = cat_rows[sid].get("justification", "")
|
| 239 |
+
if category not in ALLOWED_CATEGORIES:
|
| 240 |
+
raise click.ClickException(
|
| 241 |
+
f"sample_id {sid}: category {category!r} is not one of {ALLOWED_CATEGORIES}. "
|
| 242 |
+
"The rubric's four categories may not be renamed or expanded."
|
| 243 |
+
)
|
| 244 |
+
if len(str(justification).split()) > MAX_JUSTIFICATION_WORDS:
|
| 245 |
+
raise click.ClickException(
|
| 246 |
+
f"sample_id {sid}: justification exceeds {MAX_JUSTIFICATION_WORDS} words."
|
| 247 |
+
)
|
| 248 |
+
final.append(
|
| 249 |
+
{
|
| 250 |
+
"sample_id": sid,
|
| 251 |
+
"prediction": row["prediction"],
|
| 252 |
+
"refs": row["refs"],
|
| 253 |
+
"category": category,
|
| 254 |
+
"justification": justification,
|
| 255 |
+
}
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
with output_path.open("w", encoding="utf-8") as f:
|
| 259 |
+
for row in final:
|
| 260 |
+
f.write(json.dumps(row) + "\n")
|
| 261 |
+
|
| 262 |
+
counts = Counter(r["category"] for r in final)
|
| 263 |
+
click.echo(f"Categorized {n} samples -> {output_path}")
|
| 264 |
+
click.echo("Counts:")
|
| 265 |
+
click.echo(" " + ", ".join(f"{counts.get(c, 0)}/{n} {c}" for c in ALLOWED_CATEGORIES))
|
| 266 |
+
specific = counts.get("SPECIFIC-CORRECT", 0)
|
| 267 |
+
click.echo("")
|
| 268 |
+
click.echo(f"SPECIFIC-CORRECT = {specific}/{n} (input to the COMBINED DECISION RULE)")
|
| 269 |
+
click.echo("Apply the COMBINED DECISION RULE in this script's docstring together with the")
|
| 270 |
+
click.echo("Part A band. This script does NOT read the BLEU output — do not auto-decide here.")
|
| 271 |
+
click.echo(
|
| 272 |
+
f"Note: with N={n}, a proportion carries roughly +/-18% sampling margin; "
|
| 273 |
+
"report counts, not point-estimate percentages."
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
if __name__ == "__main__":
|
| 278 |
+
main()
|
scripts/rescore_nltk_bleu.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""One-off diagnostic: rescore committed predictions under varying BLEU regimes.
|
| 2 |
+
|
| 3 |
+
This script isolates the two evaluation-methodology axes that separate this
|
| 4 |
+
repo's metrics from the IEEE notebook's reported BLEU-4 ~24:
|
| 5 |
+
|
| 6 |
+
* Axis A — aggregation + smoothing (sacrebleu corpus vs NLTK smoothed
|
| 7 |
+
sentence-BLEU). A previous rescore showed this is a wash under
|
| 8 |
+
defensible smoothers when scored against the same references.
|
| 9 |
+
* Axis B — reference count. The committed predictions.jsonl carries only
|
| 10 |
+
~1.46 references/image (most have a single reference), not COCO's
|
| 11 |
+
canonical 5. This script joins the full 5-reference set from the
|
| 12 |
+
official annotations file and rescores against it.
|
| 13 |
+
|
| 14 |
+
When ``--coco-annotations`` is omitted the script reproduces the original
|
| 15 |
+
~1.46-ref behaviour, so the two reference-count regimes can be compared
|
| 16 |
+
side-by-side in the same session.
|
| 17 |
+
|
| 18 |
+
The scripts that gate the Kaggle retraining run are split in two — this one
|
| 19 |
+
(BLEU) and ``scripts/categorize_predictions.py`` (blinded qualitative read) —
|
| 20 |
+
and are run in SEPARATE turns so the BLEU number cannot bias the qualitative
|
| 21 |
+
categorization. The two scripts share no code and no state.
|
| 22 |
+
|
| 23 |
+
Usage
|
| 24 |
+
-----
|
| 25 |
+
# 5-ref gating test (the real test):
|
| 26 |
+
python -m scripts.rescore_nltk_bleu \
|
| 27 |
+
--predictions-path results/stabilized-beam-w4-lp07-rp12/predictions.jsonl \
|
| 28 |
+
--coco-annotations /path/to/captions_train2017.json
|
| 29 |
+
|
| 30 |
+
# 1.46-ref reproduction (omit --coco-annotations):
|
| 31 |
+
python -m scripts.rescore_nltk_bleu \
|
| 32 |
+
--predictions-path results/stabilized-beam-w4-lp07-rp12/predictions.jsonl
|
| 33 |
+
|
| 34 |
+
PRE-REGISTERED BLEU PREDICTION
|
| 35 |
+
------------------------------
|
| 36 |
+
HYPOTHESIS: Reference-count is the dominant remaining axis of the IEEE eval
|
| 37 |
+
methodology gap (Axis A — aggregation+smoothing — was already shown to be a
|
| 38 |
+
wash by the previous rescore under defensible smoothers).
|
| 39 |
+
|
| 40 |
+
PREDICTION: 5-ref sacrebleu corpus BLEU-4 will land >= 18.
|
| 41 |
+
|
| 42 |
+
DECISION RULE (BLEU-only):
|
| 43 |
+
>= 18 -> DOMINANT. Methodology (reference count) dominates the IEEE gap.
|
| 44 |
+
14-18 -> MAJOR-BUT-PARTIAL. Methodology is a major but partial factor.
|
| 45 |
+
<= 13 -> MINOR. Methodology contributes ~3 points at most. Checkpoint
|
| 46 |
+
genuinely underperforms IEEE.
|
| 47 |
+
|
| 48 |
+
SECONDARY: 5-ref NLTK method1 should land within ~1 point of 5-ref sacrebleu
|
| 49 |
+
corpus. If it diverges by >3 points, Axis A is not a wash under 5 refs after
|
| 50 |
+
all and the multi-axis analysis above needs revision.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
from __future__ import annotations
|
| 54 |
+
|
| 55 |
+
import json
|
| 56 |
+
from collections.abc import Sequence
|
| 57 |
+
from pathlib import Path
|
| 58 |
+
|
| 59 |
+
import click
|
| 60 |
+
from nltk.translate.bleu_score import SmoothingFunction, sentence_bleu
|
| 61 |
+
|
| 62 |
+
# Sentinels the training pipeline wraps captions in (mirrors
|
| 63 |
+
# captioning.preprocessing.caption). Inlined so this one-off stays standalone
|
| 64 |
+
# and TF-free (fast to run as a Kaggle cell).
|
| 65 |
+
START_TOKEN = "[start]"
|
| 66 |
+
END_TOKEN = "[end]"
|
| 67 |
+
|
| 68 |
+
# Caption normalisation identical to captioning.preprocessing.caption
|
| 69 |
+
# .preprocess_caption MINUS the sentinel wrap: lowercase, strip punctuation,
|
| 70 |
+
# collapse whitespace. Applied to raw COCO captions so the 5-ref set lands in
|
| 71 |
+
# the SAME token space as the predictions and the stored 1.46-ref set —
|
| 72 |
+
# otherwise the reference-count axis would be contaminated by a tokenisation
|
| 73 |
+
# mismatch.
|
| 74 |
+
import re # noqa: E402 (kept next to the patterns it owns)
|
| 75 |
+
|
| 76 |
+
_PUNCTUATION_RE = re.compile(r"[^\w\s]")
|
| 77 |
+
_WHITESPACE_RE = re.compile(r"\s+")
|
| 78 |
+
|
| 79 |
+
# Cumulative BLEU-n weight vectors (uniform over the first n orders).
|
| 80 |
+
_CUMULATIVE_WEIGHTS = {
|
| 81 |
+
1: (1.0, 0.0, 0.0, 0.0),
|
| 82 |
+
2: (0.5, 0.5, 0.0, 0.0),
|
| 83 |
+
3: (1 / 3, 1 / 3, 1 / 3, 0.0),
|
| 84 |
+
4: (0.25, 0.25, 0.25, 0.25),
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
_SMOOTHERS = {
|
| 88 |
+
"method0": SmoothingFunction().method0,
|
| 89 |
+
"method1": SmoothingFunction().method1,
|
| 90 |
+
"method4": SmoothingFunction().method4,
|
| 91 |
+
"method7": SmoothingFunction().method7,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _normalize(text: str) -> str:
|
| 96 |
+
"""Lowercase, strip punctuation, collapse whitespace (no sentinels)."""
|
| 97 |
+
if not text:
|
| 98 |
+
return ""
|
| 99 |
+
text = text.lower()
|
| 100 |
+
text = _PUNCTUATION_RE.sub("", text)
|
| 101 |
+
text = _WHITESPACE_RE.sub(" ", text)
|
| 102 |
+
return text.strip()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _strip_sentinels(caption: str) -> str:
|
| 106 |
+
"""Remove [start]/[end], lowercase, collapse whitespace."""
|
| 107 |
+
if not caption:
|
| 108 |
+
return ""
|
| 109 |
+
cleaned = caption.replace(START_TOKEN, " ").replace(END_TOKEN, " ")
|
| 110 |
+
return _normalize(cleaned)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _image_id(image_path: str) -> int:
|
| 114 |
+
"""COCO image_id from a .../train2017/000000530117.jpg path."""
|
| 115 |
+
stem = Path(image_path).stem
|
| 116 |
+
return int(stem) # raises ValueError on a non-numeric stem (malformed path)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _load_predictions(path: Path) -> list[dict]:
|
| 120 |
+
rows: list[dict] = []
|
| 121 |
+
with path.open(encoding="utf-8") as f:
|
| 122 |
+
for line in f:
|
| 123 |
+
line = line.strip()
|
| 124 |
+
if line:
|
| 125 |
+
rows.append(json.loads(line))
|
| 126 |
+
return rows
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _load_coco_refs(path: Path) -> dict[int, list[str]]:
|
| 130 |
+
"""Build {image_id: [raw captions...]} from captions_train2017.json."""
|
| 131 |
+
data = json.loads(path.read_text(encoding="utf-8"))
|
| 132 |
+
refs: dict[int, list[str]] = {}
|
| 133 |
+
for ann in data["annotations"]:
|
| 134 |
+
refs.setdefault(int(ann["image_id"]), []).append(ann["caption"])
|
| 135 |
+
return refs
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def _refs_by_slot(references: Sequence[Sequence[str]]) -> list[list[str]]:
|
| 139 |
+
"""Ragged per-example references -> sacrebleu per-slot layout."""
|
| 140 |
+
max_refs = max((len(r) for r in references), default=0)
|
| 141 |
+
return [[refs[i] if i < len(refs) else "" for refs in references] for i in range(max_refs)]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _sacrebleu_breakdown(preds: list[str], references: list[list[str]]) -> dict[int, float]:
|
| 145 |
+
"""sacrebleu corpus BLEU-1..4, same config as captioning.evaluation.bleu."""
|
| 146 |
+
import sacrebleu
|
| 147 |
+
|
| 148 |
+
refs_by_slot = _refs_by_slot(references)
|
| 149 |
+
out: dict[int, float] = {}
|
| 150 |
+
for n in (1, 2, 3, 4):
|
| 151 |
+
scorer = sacrebleu.metrics.BLEU(max_ngram_order=n, effective_order=True)
|
| 152 |
+
out[n] = float(scorer.corpus_score(preds, refs_by_slot).score)
|
| 153 |
+
return out
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def _nltk_macro_breakdown(
|
| 157 |
+
hyps: list[list[str]], refs: list[list[list[str]]], smoother
|
| 158 |
+
) -> dict[int, float]:
|
| 159 |
+
"""Macro-averaged sentence BLEU-1..4 (0-100) for a given smoother."""
|
| 160 |
+
sums = {n: 0.0 for n in (1, 2, 3, 4)}
|
| 161 |
+
for hyp, ref_list in zip(hyps, refs, strict=True):
|
| 162 |
+
if not hyp:
|
| 163 |
+
continue
|
| 164 |
+
for n in (1, 2, 3, 4):
|
| 165 |
+
sums[n] += sentence_bleu(
|
| 166 |
+
ref_list, hyp, weights=_CUMULATIVE_WEIGHTS[n], smoothing_function=smoother
|
| 167 |
+
)
|
| 168 |
+
count = len(hyps) or 1
|
| 169 |
+
return {n: 100.0 * sums[n] / count for n in (1, 2, 3, 4)}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _band(bleu4: float) -> str:
|
| 173 |
+
"""Map 5-ref sacrebleu corpus BLEU-4 to the pre-registered band."""
|
| 174 |
+
if bleu4 >= 18.0:
|
| 175 |
+
return "DOMINANT"
|
| 176 |
+
if bleu4 >= 14.0:
|
| 177 |
+
return "MAJOR-BUT-PARTIAL"
|
| 178 |
+
if bleu4 <= 13.0:
|
| 179 |
+
return "MINOR"
|
| 180 |
+
return "BOUNDARY-13-14-REVIEW" # 13 < x < 14 — left undefined by the spec
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@click.command()
|
| 184 |
+
@click.option(
|
| 185 |
+
"--predictions-path",
|
| 186 |
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
| 187 |
+
default=Path("results/stabilized-beam-w4-lp07-rp12/predictions.jsonl"),
|
| 188 |
+
help="predictions.jsonl from a scripts.evaluate run.",
|
| 189 |
+
)
|
| 190 |
+
@click.option(
|
| 191 |
+
"--coco-annotations",
|
| 192 |
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
| 193 |
+
default=None,
|
| 194 |
+
help="captions_train2017.json. When given, scores against all 5 COCO refs; "
|
| 195 |
+
"when omitted, reproduces the ~1.46-ref behaviour.",
|
| 196 |
+
)
|
| 197 |
+
@click.option(
|
| 198 |
+
"--smoother",
|
| 199 |
+
type=click.Choice(list(_SMOOTHERS)),
|
| 200 |
+
default="method1",
|
| 201 |
+
help="Primary NLTK smoother for the headline table (method4 always also shown).",
|
| 202 |
+
)
|
| 203 |
+
def main(predictions_path: Path, coco_annotations: Path | None, smoother: str) -> None:
|
| 204 |
+
"""Rescore predictions; in 5-ref mode emit the pre-registered band."""
|
| 205 |
+
rows = _load_predictions(predictions_path)
|
| 206 |
+
five_ref = coco_annotations is not None
|
| 207 |
+
|
| 208 |
+
# ---- Build the active reference set ------------------------------------
|
| 209 |
+
preds: list[str] = []
|
| 210 |
+
refs_sacre: list[list[str]] = [] # normalised strings, per example
|
| 211 |
+
if coco_annotations is not None:
|
| 212 |
+
coco = _load_coco_refs(coco_annotations)
|
| 213 |
+
missing: list[int] = []
|
| 214 |
+
for rec in rows:
|
| 215 |
+
iid = _image_id(rec["image"])
|
| 216 |
+
if iid not in coco:
|
| 217 |
+
missing.append(iid)
|
| 218 |
+
if missing:
|
| 219 |
+
raise click.ClickException(
|
| 220 |
+
f"{len(missing)} prediction image_id(s) absent from "
|
| 221 |
+
f"{coco_annotations}. First 5: {missing[:5]}. "
|
| 222 |
+
"Refusing to fall back to single-ref scoring."
|
| 223 |
+
)
|
| 224 |
+
for rec in rows:
|
| 225 |
+
iid = _image_id(rec["image"])
|
| 226 |
+
preds.append(_normalize(rec["prediction"]))
|
| 227 |
+
refs_sacre.append([_normalize(c) for c in coco[iid]])
|
| 228 |
+
else:
|
| 229 |
+
for rec in rows:
|
| 230 |
+
preds.append(_normalize(rec["prediction"]))
|
| 231 |
+
refs_sacre.append([_strip_sentinels(r) for r in rec["references"]])
|
| 232 |
+
|
| 233 |
+
# NLTK works on token lists.
|
| 234 |
+
hyps_tok = [p.split() for p in preds]
|
| 235 |
+
refs_tok = [[r.split() for r in ref_list if r] for ref_list in refs_sacre]
|
| 236 |
+
|
| 237 |
+
# ---- Reference-count stats (A2) ----------------------------------------
|
| 238 |
+
counts = [len(r) for r in refs_sacre]
|
| 239 |
+
n = len(counts)
|
| 240 |
+
ref_stats = {
|
| 241 |
+
"n_examples": n,
|
| 242 |
+
"mean": round(sum(counts) / n, 4) if n else 0.0,
|
| 243 |
+
"min": min(counts) if counts else 0,
|
| 244 |
+
"max": max(counts) if counts else 0,
|
| 245 |
+
"n_lt_5": sum(1 for c in counts if c < 5),
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
# ---- Metrics -----------------------------------------------------------
|
| 249 |
+
new_sacre = _sacrebleu_breakdown(preds, refs_sacre)
|
| 250 |
+
new_primary = _nltk_macro_breakdown(hyps_tok, refs_tok, _SMOOTHERS[smoother])
|
| 251 |
+
new_method4 = _nltk_macro_breakdown(hyps_tok, refs_tok, _SMOOTHERS["method4"])
|
| 252 |
+
|
| 253 |
+
metrics_path = predictions_path.parent / "metrics.json"
|
| 254 |
+
committed = (
|
| 255 |
+
json.loads(metrics_path.read_text(encoding="utf-8")) if metrics_path.exists() else {}
|
| 256 |
+
)
|
| 257 |
+
committed_bleu = {n_: committed.get(f"bleu{n_}", float("nan")) for n_ in (1, 2, 3, 4)}
|
| 258 |
+
|
| 259 |
+
regime = "5-ref" if five_ref else "1.46-ref"
|
| 260 |
+
|
| 261 |
+
# ---- Headline four-column table (NO method7) ---------------------------
|
| 262 |
+
click.echo(f"Predictions : {predictions_path}")
|
| 263 |
+
click.echo(
|
| 264 |
+
f"Reference set : {regime} "
|
| 265 |
+
f"(mean {ref_stats['mean']}/image, min {ref_stats['min']}, "
|
| 266 |
+
f"max {ref_stats['max']}, {ref_stats['n_lt_5']}/{n} have <5 refs)"
|
| 267 |
+
)
|
| 268 |
+
click.echo("")
|
| 269 |
+
header = (
|
| 270 |
+
f" {'metric':<8}{'committed sacre':>16}{f'new sacre ({regime})':>20}"
|
| 271 |
+
f"{f'NLTK {smoother}':>16}{'NLTK method4':>16}"
|
| 272 |
+
)
|
| 273 |
+
click.echo(header)
|
| 274 |
+
for n_ in (1, 2, 3, 4):
|
| 275 |
+
click.echo(
|
| 276 |
+
f" BLEU-{n_:<3}{committed_bleu[n_]:>16.2f}{new_sacre[n_]:>20.2f}"
|
| 277 |
+
f"{new_primary[n_]:>16.2f}{new_method4[n_]:>16.2f}"
|
| 278 |
+
)
|
| 279 |
+
click.echo("")
|
| 280 |
+
|
| 281 |
+
# ---- Secondary check + band (5-ref only) -------------------------------
|
| 282 |
+
if five_ref:
|
| 283 |
+
delta = new_primary[4] - new_sacre[4]
|
| 284 |
+
axis_a_wash = abs(delta) <= 3.0
|
| 285 |
+
band = _band(new_sacre[4])
|
| 286 |
+
click.echo(f"SECONDARY CHECK: NLTK method1 BLEU-4 - sacrebleu corpus BLEU-4 = {delta:+.2f}")
|
| 287 |
+
click.echo(
|
| 288 |
+
" -> Axis A is a wash under 5 refs (|delta| <= 3)."
|
| 289 |
+
if axis_a_wash
|
| 290 |
+
else " -> Axis A is NOT a wash under 5 refs (|delta| > 3); revise the multi-axis analysis."
|
| 291 |
+
)
|
| 292 |
+
click.echo("")
|
| 293 |
+
click.echo(
|
| 294 |
+
f"PRE-REGISTERED BAND (5-ref sacrebleu corpus BLEU-4 = {new_sacre[4]:.2f}): {band}"
|
| 295 |
+
)
|
| 296 |
+
click.echo("")
|
| 297 |
+
|
| 298 |
+
out = {
|
| 299 |
+
"predictions_path": str(predictions_path),
|
| 300 |
+
"coco_annotations": str(coco_annotations),
|
| 301 |
+
"ref_stats": ref_stats,
|
| 302 |
+
"committed_1p46ref_sacrebleu": {f"bleu{k}": committed_bleu[k] for k in (1, 2, 3, 4)},
|
| 303 |
+
"new_5ref": {
|
| 304 |
+
"sacrebleu_corpus": {f"bleu{k}": new_sacre[k] for k in (1, 2, 3, 4)},
|
| 305 |
+
f"nltk_{smoother}": {f"bleu{k}": new_primary[k] for k in (1, 2, 3, 4)},
|
| 306 |
+
"nltk_method4": {f"bleu{k}": new_method4[k] for k in (1, 2, 3, 4)},
|
| 307 |
+
},
|
| 308 |
+
"band": band,
|
| 309 |
+
"band_basis": "5ref_sacrebleu_corpus_bleu4",
|
| 310 |
+
"secondary_check": {
|
| 311 |
+
"method1_vs_corpus_bleu4_delta": round(delta, 4),
|
| 312 |
+
"axis_a_wash_under_5ref": axis_a_wash,
|
| 313 |
+
},
|
| 314 |
+
}
|
| 315 |
+
out_path = predictions_path.parent / "metrics_5ref.json"
|
| 316 |
+
out_path.write_text(json.dumps(out, indent=2), encoding="utf-8")
|
| 317 |
+
click.echo(f"Wrote: {out_path}")
|
| 318 |
+
else:
|
| 319 |
+
click.echo(
|
| 320 |
+
"1.46-ref mode: band + metrics_5ref.json skipped "
|
| 321 |
+
"(pass --coco-annotations to run the gating test)."
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
# ---- Diagnostic: smoother sensitivity (NOT headline) -------------------
|
| 325 |
+
click.echo("")
|
| 326 |
+
click.echo("DIAGNOSTIC (smoother sensitivity — NOT the headline; method7 is known to inflate):")
|
| 327 |
+
click.echo(f" {'smoother':<10}{'BLEU-4':>10}")
|
| 328 |
+
for name in ("method0", "method1", "method4", "method7"):
|
| 329 |
+
b4 = _nltk_macro_breakdown(hyps_tok, refs_tok, _SMOOTHERS[name])[4]
|
| 330 |
+
click.echo(f" {name:<10}{b4:>10.2f}")
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
if __name__ == "__main__":
|
| 334 |
+
main()
|