Spaces:
Running
Running
Download training/evaluate.py from nsaroiu/mtg-deck-completion: direct link, hf CLI and curl.
- Browser
- Download file 6.64 kB
-
https://huggingface.co/spaces/nsaroiu/mtg-deck-completion/resolve/main/training/evaluate.py
- Command line
-
hf download hf://spaces/nsaroiu/mtg-deck-completion/training/evaluate.py
-
curl -L -o evaluate.py https://huggingface.co/spaces/nsaroiu/mtg-deck-completion/resolve/main/training/evaluate.py
6.64 kB
| """ | |
| evaluate.py | |
| Evaluation for the deck-completion model: Recall@K on held-out (masked) cards across fixed | |
| mask ratios, broken out by card popularity tier (since staples like Sol Ring | |
| appear in almost every deck -- a model that just always predicts staples | |
| regardless of context can otherwise look deceptively good on an aggregate | |
| Recall@K number), plus a nearest-neighbor qualitative check on the learned | |
| embeddings. | |
| Usage: | |
| python3 -m training.evaluate | |
| python3 -m training.evaluate --split test --checkpoint ./data/deepsets_checkpoint.pt | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import torch | |
| from tokenizer.mtg_tokenizer import CardTokenizer, PAD | |
| from training.deck_dataset import load_decks_auto, split_decks | |
| from training.metrics import ( | |
| STAPLE_COLOR_TIER_NAMES, | |
| card_tiers, | |
| nearest_neighbors, | |
| recall_at_k, | |
| staple_color_tiers, | |
| ) | |
| from training.model import DeckCompletionModel, HybridCardEmbedding, TwoHeadDeckCompletionModel, build_encoder | |
| from training.train import TEXT_EMBEDDING_CACHE, TOKENIZER_PATH, pick_device | |
| DEFAULT_CHECKPOINT = "./data/deepsets_checkpoint.pt" | |
| def load_checkpoint(checkpoint_path: str, device: torch.device): | |
| tok = CardTokenizer.load_pretrained(TOKENIZER_PATH) | |
| struct_feats = tok.feature_matrix() | |
| text_feats = tok.text_embeddings(cache_path=TEXT_EMBEDDING_CACHE) | |
| ckpt = torch.load(checkpoint_path, map_location=device, weights_only=False) | |
| pad_id = tok.token_to_id[PAD] | |
| card_embedding = HybridCardEmbedding( | |
| ckpt["vocab_size"], ckpt["embed_dim"], struct_feats, text_feats, pad_id | |
| ) | |
| # ckpt.get(...) defaults: checkpoints saved before the Set Transformer | |
| # addition have no encoder_type key at all and are all DeepSets. | |
| encoder_type = ckpt.get("encoder_type", "deepsets") | |
| encoder_kwargs = ckpt.get("encoder_kwargs", {}) or {} | |
| encoder = build_encoder(encoder_type, ckpt["embed_dim"], ckpt["hidden_dim"], **encoder_kwargs) | |
| # ckpt.get(...) default: checkpoints saved before the deck-completeness | |
| # feature was added have no such key and were all trained without it. | |
| model = DeckCompletionModel( | |
| card_embedding, encoder, ckpt["hidden_dim"], | |
| use_completeness_feature=ckpt.get("use_completeness_feature", False), | |
| ) | |
| model.load_state_dict(ckpt["model_state_dict"]) | |
| model.to(device) | |
| model.eval() | |
| return model, tok, ckpt | |
| def load_two_head_checkpoint(checkpoint_path: str, device: torch.device): | |
| """Sibling of load_checkpoint for a TwoHeadDeckCompletionModel checkpoint | |
| (training/train_two_head.py, ckpt["architecture"] == "two_head") -- | |
| same tokenizer/feature-loading and encoder reconstruction, just a | |
| different model class. Kept separate rather than branched inside | |
| load_checkpoint since the two model classes have different forward | |
| signatures (one (B,V) logits tensor vs. a {"staple","synergy"} dict) -- | |
| callers need to know which one they're getting.""" | |
| tok = CardTokenizer.load_pretrained(TOKENIZER_PATH) | |
| struct_feats = tok.feature_matrix() | |
| text_feats = tok.text_embeddings(cache_path=TEXT_EMBEDDING_CACHE) | |
| ckpt = torch.load(checkpoint_path, map_location=device, weights_only=False) | |
| pad_id = tok.token_to_id[PAD] | |
| card_embedding = HybridCardEmbedding( | |
| ckpt["vocab_size"], ckpt["embed_dim"], struct_feats, text_feats, pad_id | |
| ) | |
| encoder = build_encoder( | |
| ckpt.get("encoder_type", "deepsets"), ckpt["embed_dim"], ckpt["hidden_dim"], | |
| **(ckpt.get("encoder_kwargs", {}) or {}), | |
| ) | |
| model = TwoHeadDeckCompletionModel(card_embedding, encoder, ckpt["hidden_dim"]) | |
| model.load_state_dict(ckpt["model_state_dict"]) | |
| model.to(device) | |
| model.eval() | |
| return model, tok, ckpt | |
| def run( | |
| checkpoint_path: str = DEFAULT_CHECKPOINT, | |
| split: str = "val", | |
| max_decks: int | None = None, | |
| tier_mode: str = "default", | |
| ): | |
| device = pick_device() | |
| model, tok, ckpt = load_checkpoint(checkpoint_path, device) | |
| print(f"Loaded checkpoint from epoch {ckpt['epoch']} (val_loss={ckpt['val_loss']:.4f})") | |
| decks = load_decks_auto(tok) | |
| splits = split_decks(decks) | |
| if tier_mode == "color_split": | |
| tiers = staple_color_tiers(splits["train"], tok) | |
| tier_names = STAPLE_COLOR_TIER_NAMES | |
| else: | |
| tiers = card_tiers(splits["train"]) | |
| tier_names = ("staple", "mid", "long_tail") | |
| for name in ("val", "test", "held_out_commander") if split == "all" else (split,): | |
| result = recall_at_k( | |
| model, splits[name], tiers, | |
| device=device, max_decks=max_decks, tier_names=tier_names, | |
| ) | |
| print(f"\n=== {name} ({result['n_decks']} decks) ===") | |
| print("Recall@K by mask ratio:") | |
| for k, v in result["by_ratio"].items(): | |
| print(f" {k}: {v:.3f}") | |
| print("Recall@K by popularity tier (of true targets, how many found):") | |
| for k, v in result["by_tier"].items(): | |
| print(f" {k}: {v:.3f}") | |
| print("Precision@K by mask ratio:") | |
| for k, v in result["precision_by_ratio"].items(): | |
| print(f" {k}: {v:.3f}") | |
| print("Precision@K by popularity tier (of predictions in this tier, how many correct):") | |
| for k, v in result["precision_by_tier"].items(): | |
| print(f" {k}: {v:.3f}") | |
| print("MRR by mask ratio:") | |
| for k, v in result["mrr_by_ratio"].items(): | |
| print(f" ratio={k}: {v:.4f}") | |
| print("MRR by popularity tier:") | |
| for k, v in result["mrr_by_tier"].items(): | |
| print(f" {k}: {v:.4f}") | |
| print("\n=== Nearest-neighbor sanity check (learned embeddings) ===") | |
| for name in ("Sol Ring", "Swords to Plowshares", "Cyclonic Rift"): | |
| neighbors = nearest_neighbors(model, tok, name, device=device) | |
| print(f"{name}:") | |
| for n, sim in neighbors: | |
| print(f" {n} (sim={sim:.3f})") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--checkpoint", default=DEFAULT_CHECKPOINT) | |
| parser.add_argument("--split", default="val", choices=["train", "val", "test", "held_out_commander", "all"]) | |
| parser.add_argument("--max-decks", type=int, default=None) | |
| parser.add_argument("--tier-mode", default="default", choices=["default", "color_split"], | |
| help="'color_split' further splits 'staple' into 'universal' (colorless) " | |
| "vs 'color_popular' (has a color identity) -- see staple_color_tiers().") | |
| args = parser.parse_args() | |
| run(checkpoint_path=args.checkpoint, split=args.split, max_decks=args.max_decks, tier_mode=args.tier_mode) | |