CoolFace
Modelpublic

pblrvo/Qwen3-4B-Game-semantic-IDs-v2

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes12downloads
Model Card

Qwen3-4B QLoRA -- Generative Recommender over Semantic IDs (v2)

QLoRA fine-tune of Qwen/Qwen3-4B to reason over semantic IDs -- short discrete codes produced by an RQ-VAE trained on game-catalog item embeddings -- instead of ever seeing raw item IDs or embeddings directly. Trained on the paired dataset pblrvo/steam-games-semanticIds-instructions.

This is a second iteration over pblrvo/Qwen3-4B-Game-semantic-IDs (v1), kept as a separate model rather than replacing it -- the two make different tradeoffs (see Evaluation below) and v1 is still the better choice for some use cases.

What changed from v1

  • —Grounding train/val split fixed. v1 held out entire items from grounding's val set, so grounding was tested on items whose name<->SID mapping was structurally never in training -- not a fair test. v2 splits within each item's repeated examples instead, so every item is trained on and val tests recall under an unseen phrasing.
  • —Two new task types: nl_preference (open-ended natural-language queries, e.g. "I want a racing game") and nl_similar_item (natural-language "recommend something like X" queries), both grounded in the same catalog/co-occurrence data as the existing tasks.
  • —Cross-task exposure cap. An item could independently hit the ceiling in several recommendation tasks at once (sequential + asy + similaritem + nlsimilar_item), compounding to ~140 total exposures for popular items vs. a dozen for a typical one -- this is what was driving the v1 model's tendency to over-recommend a handful of popular titles regardless of input. v2 caps combined exposure at 40, deliberately excluding grounding (which stays exactly uniform per item).
  • —Stage 1 (embedding warmup) redesigned per STAR (arXiv, "Semantic-ID Token-Embedding Alignment for Generative Recommenders"): gradient-masked so only the new SID tokens update (the pretrained vocabulary stays frozen, previously it was inadvertently trainable too), and restricted to grounding-only text<->SID pairs instead of a mix of all task types.
  • —grounding_id2name enriched with a short "About the game" snippet alongside name+genres, so the model grounds SIDs against real content, not just a name/genre tag.

Pipeline

Game catalog items -> embedded (Qwen3-0.6B) -> compressed into 4-level semantic IDs via RQ-VAE -> those ID tokens become new vocabulary the LLM is fine-tuned to reason over. Two-stage fine-tuning:

  1. 1.Embedding warmup: only the new semantic-ID token embeddings are trained (codebook-grounded initialization + gradient-masked, grounding-only high-LR run) so the new tokens carry meaningful, linguistically grounded structure before task-specific training begins.
  2. 2.QLoRA (this checkpoint): a real LoRA adapter (rank 8) across all attention/MLP projections, trained on top of stage 1's warmed-up embeddings, via Unsloth.

Both stages run through 4-bit quantization -- full-parameter fine-tuning of a ~4B model doesn't fit a single 12GB consumer GPU alongside gradients/optimizer state.

Tasks

  • —Sequential recommendation: predict the next item's semantic ID from a user's play history
  • —Grounding: map a semantic ID <-> item name/genres(+short description), both directions
  • —Similar item: given an item, suggest another one real users also engaged with
  • —NL preference: open-ended natural-language preference queries ("I want a racing game") -> a matching item's semantic ID
  • —NL similar item: natural-language "recommend something like X" queries, same ground truth as Similar item
  • —ASY (asymmetric item prediction, LC-Rec, arXiv 2311.09049): same history/target pairs as sequential, rendered as the target's name instead of its semantic ID

Evaluation

Recall@K / NDCG@K via constrained beam search (candidates restricted to real catalog items through a trie over valid semantic IDs / descriptions), the same methodology TIGER (Rajput et al. 2023) and LC-Rec use for semantic-ID generative recommenders. n=500 per task (n=71-277 where a task's val split has fewer examples than that), temperature=0.8, beam=10; catalog size ~8,563 items, so random-chance Recall@10 is ~0.12%. grounding_id2name and asy (both name+genres(+blurb)-output tasks) are scored by collapsing each beam candidate and the target down to just the item's plain Name before scoring, so the metric asks "predicted the correct game" rather than requiring an exact match on genre/blurb text too.

Taskv1 Recall@5v2 Recall@5v1 Recall@10v2 Recall@10v1 NDCG@10v2 NDCG@10
grounding_name2id2%32.4%3%41.2%0.0120.275
grounding_id2name0%0.2%0%0.2%0.0000.002
asy-- (not evaluated)0%--0%--0.000
sequential12%2.6%13%3.0%0.0940.023
similar_item10%3.8%13%5.6%0.0820.035
nlsimilaritem-- (new)2.9%--5.1%--0.027
nl_preference-- (new)77.5%--87.3%--0.662

This is a real tradeoff, not a clean win. Grounding (name<->SID) improved dramatically. But sequential/similar_item regressed substantially under strict exact-match Recall@K -- most likely because the cross-task exposure cap cut raw training volume for those tasks by ~40%, trading some ability to memorize the specific recorded co-occurrence pairing for eliminating the popularity- collapse shortcut (v1's tendency to recommend the same few popular items regardless of input).

Qualitative spot-checks (10-15 examples each, decoded to game names) support that read only partly. v2's predictions for sequential/similar_item are often genre/franchise-coherent and fully diverse (no repeated defaults) even when they miss the specific recorded target -- e.g. a racing-game seed predicts another racing game, a Saints Row 2 history predicts Saints Row: The Third. Recall@K can't credit a plausible-but-different answer, so the metric understates v2's real recommendation quality on these tasks to some degree. But a second check complicates the "just needs more training data" story: for a batch of similar_item misses, the specific recorded target's own training exposure was solidly mid-range (6-22 repeats, vs. a dataset median of 12 post-cap) -- not the low-exposure outliers a pure data-starvation explanation would predict. And not every miss is genre-coherent either: a Time Mysteries 3 seed (same series as its recorded Time Mysteries 2 target) predicted an unrelated Medieval: Total War, no genre or franchise logic behind it. The more accurate picture: similar_item's ground truth itself is inherently ambiguous (a seed item can have up to 10 valid co-occurring partners in the raw data, e.g. from bundles or sales rather than genre; only one is credited per eval example), so some of the gap is metric-can't-credit-a-different-valid-answer, but some is a genuine specific-recall gap unrelated to how often that particular item was seen. Checkpoint-by-checkpoint Recall@K was flat across the entire back half of training (no late-training improvement), consistent with the model having already reached whatever ceiling this recipe supports rather than needing more steps.

grounding_id2name/asy are the one place a real evaluation bug was found and fixed mid-analysis: constrained beam search defaulted to max_new_tokens=32, plenty for the 6-token semantic-ID outputs but far too small once grounding_id2name's target could include a ~90-token blurb -- generation was being hard-cut before reaching any valid stopping point, so candidates never matched anything, correct or not. Fixing that (raised to 96 for these two tasks) moved grounding_id2name from a measured 0.00% to 0.20% -- genuinely small, not an artifact. The likely real explanation: a semantic ID's coarser RQ-VAE codes are deliberately shared across many similar items (that's what makes the hierarchy useful for generalization elsewhere), so recovering the one specific name behind a shared/coarse code is close to an arbitrary lookup problem, and a rank-8 LoRA adapter over ~8,500 items may not have the capacity to memorize it precisely.

Update (2026-08-10): fair comparison against v3, and a metric correction

Qwen3-8B-Game-semantic-IDs-v3 (bigger model, full-parameter fine-tune, redesigned dataset) initially looked like it regressed on sequential/asy relative to this model's numbers above -- but those numbers were measured on a different, pre-redesign val set with temperature=0.8 sampling, not v3's deterministic eval on the redesigned data. That was never a controlled comparison. Two things were checked to make it one:

Is the semantic-ID vocabulary even comparable? If the RQ-VAE had been retrained for v3, this model's learned token embeddings would encode a different item mapping than v3's, making any comparison invalid outright. Checked directly by joining this model's own training data (8,093 grounding_name2id name->SID pairs) against the current codebook by item name: 8,011/8,093 (99.0%) identical assignments -- the 82 mismatches are ambiguous duplicate catalog names (e.g. "Portal", "Contract"), not a re-embedding. The codebook is unchanged, so a fair comparison is valid.

Re-run under identical conditions. This model was re-evaluated with v3's exact eval code, val set, and settings (n=500/task, seed=0, deterministic beam search):

TaskThis model (v2), fair comparisonv3
grounding_name2id37.00%96.80%
sequential3.60%1.00%
similar_item1.00%1.00%
nlsimilaritem4.40%2.00%
grounding_id2name0.20%7.20%
asy0.00%0.00%
nl_preference84.86%94.95%

The sequential edge over v3 holds up under fair conditions -- not a dataset artifact. But re-scored under a metric that asks "did the model recommend any game this user actually played" instead of the one exact next-lowest-playtime item (justification: these sequences are ordered by playtime, not real chronology -- see v3's README for the full argument), the picture changes substantially:

TaskMetricThis model (v2)v3
sequentialAny-played Hit@1059.20%58.00%
sequentialSmall-library Hit@10 (lift over random)49.03% (23.03x)43.24% (20.31x)
asyAny-played Hit@1021.80%36.20%
asySmall-library Hit@10 (lift over random)7.54% (2.98x)21.83% (8.62x)

sequential is a near-tie once measured this way (the apparent 3.6x gap was mostly an artifact of exact-match Recall@K's sensitivity to an arbitrary playtime-rank position, not a real capability gap). asy is not a tie -- v3 wins clearly (1.7x pooled, 2.9x on the harder small-library stratum), a real improvement the exact-match metric couldn't see because both models scored a flat 0.00% under it. Full writeup, including the codebook-compatibility check and multi-target methodology: `SEQUENTIAL_ASY_REGRESSION_ANALYSIS.md` in the project repository.

Known limitations

  • —Choosing between v1 and v2 depends on the task you care about. v1 is meaningfully better at sequential/similar_item's strict exact-match recall; v2 is dramatically better at grounding and adds two new natural-language query tasks.
  • —grounding_id2name/asy (SID/history -> name) are weak in both versions, most likely because recovering one specific name from a semantic ID's shared/coarse codes is close to an arbitrary lookup problem -- see above.
  • —Compute-constrained training. QLoRA rank 8, ~0.9 epochs of the fine-tuning stage (wall-clock capped on a single 12GB GPU, not run to convergence).
  • —No classical-recommender baseline (e.g. SASRec) has been run against the same data -- these numbers show the model beats random chance substantially, not that the semantic-ID + LLM approach outperforms a much simpler sequential recommender.
  • —Single training run, single seed, throughout.

Usage

python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel

ADAPTER = "pblrvo/Qwen3-4B-Game-semantic-IDs-v2"
BASE_MODEL = "Qwen/Qwen3-4B"

tokenizer = AutoTokenizer.from_pretrained(ADAPTER)
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True, llm_int8_skip_modules=["lm_head"],
)
base_model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, dtype=torch.bfloat16, quantization_config=quantization_config)
base_model.resize_token_embeddings(len(tokenizer))
model = PeftModel.from_pretrained(base_model, ADAPTER)

The semantic-ID vocabulary (<|sid_start|>, <|sid_L{level}_{code}|>, <|sid_end|>) is only meaningful relative to the specific RQ-VAE codebook trained in the source project -- this model isn't usable standalone without that catalog/codebook context.