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.