File size: 6,799 Bytes
ee65391 9bb66a6 ee65391 ade41fa ee65391 9bb66a6 ee65391 9bb66a6 ee65391 ade41fa ee65391 ade41fa ee65391 ade41fa ee65391 ade41fa ee65391 9bb66a6 26a35d0 9bb66a6 | 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 156 157 158 159 | ---
pretty_name: MTG Commander Deck Completion
tags:
- magic-the-gathering
- mtg
- commander
- edh
- pytorch
- recommender-system
license: mit
datasets:
- nsaroiu/moxfield-dump
---
# MTG Commander Deck Completion
Given a Commander (and optionally a partial decklist), suggests which other
cards belong in the deck. Trained on 189,108 real public Commander decklists
from [Moxfield](https://moxfield.com) ([dataset](https://huggingface.co/datasets/nsaroiu/moxfield-dump)).
Training/evaluation code: [github.com/nsaroiu/mtg-deck-completion](https://github.com/nsaroiu/mtg-deck-completion)
(the tokenizer + model + training/eval pipeline that produced these weights β not the
Moxfield data-collection pipeline or the web UI built on top of this model, which live
elsewhere and aren't public).
## Architecture
Every card gets a hybrid embedding: a learned per-card vector, summed with a
small MLP over structured features (color identity, mana cost, type,
keywords) and a semantic sentence embedding of its oracle text β so even a
rarely-seen card starts from a meaningful representation instead of a
near-random one.
A deck is treated as an unordered *set*: a permutation-invariant encoder
(mean-pool or self-attention) pools the known cards + commander(s) into one
vector, scored against the full card vocabulary via a tied dot product
(word2vec-style).
Production inference is an **ensemble of two specialized checkpoints**, not
one model β a single model/loss was tried and found to trade off staple
recognition against synergy ranking:
1. **Synergy phase** (`set_transformer_softmax_checkpoint.pt`, self-attention
encoder): generates iteratively, a few picks at a time, re-conditioning on
its own prior picks β this is what surfaces specific combo/synergy pieces
rather than a generic "good stuff" pile.
2. **Staple backfill** (`deepsets_softmax_checkpoint.pt`, mean-pool encoder):
tops up near-universal staples (Sol Ring, Command Tower, ...) to a
realistic target count, since the synergy phase alone under-recommends
them.
3. **Optional pruning pass** (`prune_checkpoint_inject_lowdensity.pt`): a
separate discriminator model that re-scores the assembled deck and cuts
the weakest fit, the one step no generation-only model can do.
## Files
| file | role |
|---|---|
| `deepsets_softmax_checkpoint.pt` | staple-recognition specialist |
| `set_transformer_softmax_checkpoint.pt` | synergy-ranking specialist |
| `prune_checkpoint_inject_lowdensity.pt` | optional deck-pruning pass |
| `tokenizer.json` | card vocabulary + structured features |
| `tokenizer_text_embeddings.npy` | cached oracle-text sentence embeddings |
| `card_tiers.json` | staple / mid-tier / long-tail classification per card |
All three checkpoints share the same card vocabulary and expect
`tokenizer.json` / `tokenizer_text_embeddings.npy` alongside them.
## Evaluation
Recall@50 / Precision@50 / MRR on held-out real decks, by how much of the
deck is already known (mask ratio β low = mostly complete, high = mostly
empty), staple-recognition checkpoint, validation split:
| mask ratio | Recall@50 | Precision@50 | MRR |
|---|---|---|---|
| 0.1 (deck mostly complete) | 0.57 | 0.10 | 0.121 |
| 0.5 | 0.48 | 0.40 | 0.066 |
| 0.9 (deck mostly empty) | 0.36 | 0.54 | 0.045 |
Held-out-commander split (commanders never seen in training, testing
generalization via card content rather than memorized co-occurrence):
Recall@50 0.29β0.44 across the same ratio range β meaningfully above chance,
confirming the hybrid content embeddings carry real signal for unfamiliar
commanders. Full breakdown by card-popularity tier, plus the synergy/pruning
checkpoints' own numbers, in the GitHub repo's eval output.
## Usage
There's no standalone pip package yet β loading these checkpoints requires
the tokenizer + model code from the GitHub repo above (`tokenizer/` and
`training/`; no other part of that repo is needed to run inference):
```bash
git clone https://github.com/nsaroiu/mtg-deck-completion
cd mtg-deck-completion
pip install -r requirements.txt
```
```python
from huggingface_hub import hf_hub_download
repo = "nsaroiu/mtg-deck-completion"
for f in ["deepsets_softmax_checkpoint.pt", "set_transformer_softmax_checkpoint.pt",
"prune_checkpoint_inject_lowdensity.pt", "tokenizer.json",
"tokenizer_text_embeddings.npy", "card_tiers.json"]:
hf_hub_download(repo_id=repo, filename=f, local_dir=".")
from training.evaluate import load_checkpoint
from training.ensemble import ensemble_complete_deck, load_tiers
from training.train import pick_device
device = pick_device()
staple_model, tok, _ = load_checkpoint("deepsets_softmax_checkpoint.pt", device)
synergy_model, _, _ = load_checkpoint("set_transformer_softmax_checkpoint.pt", device)
tiers = load_tiers(tok, path="card_tiers.json")
# (name, score, source) triples, source in {"synergy", "staple"}
results = ensemble_complete_deck(
["Atraxa, Praetors' Voice"], [], 20, tok, device,
staple_model, synergy_model, tiers,
)
for name, score, source in results:
print(f"{score:.3f} [{source:7}] {name}")
```
`training/ensemble.py` is also a CLI covering the same flow shown above
(`--commander` β repeatable, for partner commanders β `--card`,
`--chunk-size`/`--temperature` for the iterative synergy phase,
`--prune-checkpoint` to enable the pruning pass). `training/complete_deck.py`
exposes just one specialist checkpoint at a time, without the ensemble
fusion, if that's all you need.
## Limitations
- **Staple-tier ranking is imprecise in a near-empty context.** The model
confidently recognizes *that* a staple belongs (~90% precision when it
predicts one) well before it reliably ranks *which specific* staple is the
true target β the ensemble's backfill phase exists specifically to work
around this.
- **Color-identity legality is not learned** β it's enforced as a
deterministic post-filter (see `legal_mask` in `training/complete_deck.py`).
Using the raw logits without that filter can suggest illegal cards.
- **Iterative generation (the synergy phase's default mode) is an
experimental setting**, tuned on a sample of 11 commanders, not
exhaustively validated.
- Trained only on real, currently-popular Commander decks β a Commander
format power-level/meta shift (new sets, ban changes) isn't reflected
until retrained on fresher data.
## License
The weights and code in this repository are released under the MIT license.
The training data itself is a scrape of public Moxfield decklists (see the
[dataset repo](https://huggingface.co/datasets/nsaroiu/moxfield-dump), used
to train this model) with no asserted license β if that matters for your
use case, that's Moxfield's terms to check, not a constraint this repo
imposes.
|