Spaces:
Running
Running
| """Export the serving artifact: best.pt (training checkpoint) -> capit-sat.pt (inference contract).""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import shutil | |
| from pathlib import Path | |
| import torch | |
| from capit.checkpoint import load | |
| from capit.config import config | |
| from capit.data.vocab import Vocab | |
| from capit.models.decoder import Decoder | |
| from capit.models.encoder import Encoder | |
| from capit.serving import build_artifact | |
| def _split_counts(karpathy_json: Path) -> dict[str, int]: | |
| records = json.loads(karpathy_json.read_text())["images"] | |
| counts: dict[str, int] = {} | |
| for r in records: | |
| counts[r["split"]] = counts.get(r["split"], 0) + 1 | |
| return counts | |
| def _scores_table(metrics: dict[str, dict[str, float]]) -> str: | |
| cols = ["BLEU-1", "BLEU-2", "BLEU-3", "BLEU-4", "CIDEr"] | |
| lines = ["| beam | " + " | ".join(cols) + " |", "|-----:|" + "|".join(["-------:"] * len(cols)) + "|"] | |
| for beam in sorted(metrics, key=int): | |
| s = metrics[beam] | |
| lines.append(f"| {beam} | " + " | ".join(f"{s[c]:.2f}" for c in cols) + " |") | |
| return "\n".join(lines) | |
| def _model_card( | |
| repo_id: str, splits: dict[str, int], best_bleu4: float, best_epoch: int, metrics: dict[str, dict[str, float]] | |
| ) -> str: | |
| return f"""--- | |
| license: mit | |
| language: | |
| - en | |
| library_name: pytorch | |
| pipeline_tag: image-to-text | |
| tags: | |
| - image-captioning | |
| - show-attend-and-tell | |
| - visual-attention | |
| datasets: | |
| - flickr8k | |
| metrics: | |
| - bleu | |
| - cider | |
| --- | |
| # capit-sat | |
| Show, Attend and Tell image captioner, trained from scratch on Flickr8k (Karpathy split). | |
| The glass-box half of [capit](https://github.com/Bukunmi2108/capit) — exposes per-word | |
| attention, beam candidates, and word-by-word playback. | |
| ## Test-set scores (pycocoevalcap, Karpathy test = {splits.get('test', '?')} images) | |
| {_scores_table(metrics)} | |
| ## Training | |
| - Backbone: frozen ResNet-50 (ImageNet). Decoder trained from scratch. | |
| - Best val BLEU-4 {best_bleu4:.2f} at epoch {best_epoch} (early-stopped); Colab T4. | |
| - Splits: train {splits.get('train', '?')}, val {splits.get('val', '?')}, test {splits.get('test', '?')}. | |
| ## Known limitation | |
| Attention is effectively 7x7: ResNet-50 at 224px is natively 7x7 and the encoder upsamples | |
| to 14x14, so heatmaps are coarse (~32px blocks). Captions are grounded; the spots are | |
| region-level, not pixel-level. | |
| ## Use | |
| `huggingface_hub.hf_hub_download("{repo_id}", "capit-sat.pt")` + `vocab.json`, then | |
| `capit.serving.load_artifact(...)`. | |
| """ | |
| def export(ckpt_path: Path, vocab_path: Path, out_dir: Path, repo_id: str, metrics_json: Path) -> Path: | |
| if not metrics_json.exists(): | |
| raise FileNotFoundError( | |
| f"metrics file {metrics_json} not found — generate it first:\n" | |
| f" uv run python -m capit.evaluate --out-json {metrics_json}" | |
| ) | |
| metrics = json.loads(metrics_json.read_text()) | |
| vocab = Vocab.load(vocab_path) | |
| state = load(ckpt_path) | |
| if state.vocab_sha256 != vocab.sha256(): | |
| raise ValueError(f"vocab mismatch: ckpt {state.vocab_sha256[:8]} != vocab {vocab.sha256()[:8]}") | |
| encoder = Encoder(pretrained=True) | |
| decoder = Decoder(vocab_size=len(vocab)) | |
| decoder.load_state_dict(state.model_state) | |
| blob = build_artifact(encoder, decoder, vocab) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| artifact_path = out_dir / "capit-sat.pt" | |
| tmp = artifact_path.with_name(artifact_path.name + ".tmp") | |
| torch.save(blob, tmp) | |
| os.replace(tmp, artifact_path) | |
| shutil.copyfile(vocab_path, out_dir / "vocab.json") | |
| splits = _split_counts(config.karpathy_json) | |
| (out_dir / "README.md").write_text(_model_card(repo_id, splits, state.best_bleu4, state.best_epoch, metrics)) | |
| return artifact_path | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--ckpt", default=str(config.ckpt_dir / "best.pt")) | |
| parser.add_argument("--vocab", default=str(config.vocab_path)) | |
| parser.add_argument("--out-dir", default=str(config.data_root / "artifact")) | |
| parser.add_argument("--repo-id", default="Bukunmi2108/capit-sat") | |
| parser.add_argument("--metrics-json", default=str(config.data_root / "eval_results.json")) | |
| args = parser.parse_args() | |
| path = export(Path(args.ckpt), Path(args.vocab), Path(args.out_dir), args.repo_id, Path(args.metrics_json)) | |
| size_mb = path.stat().st_size / 1e6 | |
| print(f"wrote {path} ({size_mb:.1f} MB), vocab.json, README.md to {args.out_dir}") | |
| print(f"push: hf upload {args.repo_id} {args.out_dir} . --repo-type model") | |
| if __name__ == "__main__": | |
| main() | |