{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/eugeneyan-com/2025-09-14-training-an-llm-recsys-hybrid-for-steerable-recs-with/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "I got nerdsniped when I first heard about Semantic IDs. The idea is simple: Instead of using random hash IDs for videos or songs or products, we can use semantically meaningful tokens that an LLM can natively understand. I wondered, could we train an LLM-recommender hybrid on the rich behavioral data that makes today’s recommender systems so effective?\n\nTo my surprise, we can! The result is a language model that can converse in both English and item IDs, not with retrieval or other tools, but as a single, “bilingual” model where items (i.e., semantic IDs) are part of its vocabulary. Like a recommender model, it can recommend items given historical interactions. But the big surprise—and capability unlock—was when I found that **I could simply chat with the model to steer its recommendations, and it could reason about its choices, offer explanations, and creatively name product bundles**.\n\nHere’s a demo video. And here’s the [code](https://github.com/eugeneyan/semantic-ids-llm) to prep the data, train the model, and chat with it. (Note: This is a small model with very basic finetuning, so prompting matters a lot. Also, it’s not as general purpose and robust as most LLMs because of the limited finetuning.)\n\n[YouTube video player](https://www.youtube.com/watch?v=_0n4QS--3V8)\n\n## Wait, why is this useful?\n\nThis marries the best of recsys and language modeling. On one hand, language models have world knowledge and can eloquently talk about products, but are unaware of our catalog. Also, their recommendations are generic and suffer from popularity bias. Conversely, search and recommendation models are trained on our catalog and billions of user interactions. They excel at predicting what a user will click or buy next, but can’t be steered via natural language or reason on their choices. (What they have in common though, is learning on sequences.)\n\nTo do this, we first extend the language model’s vocabulary with semantic ID tokens like `<|sid_0|>`, `<|sid_1|>`, `<|sid_2|>`, etc. These tokens are used to represent the catalog. Then, we apply continued pretraining to teach the model the relationships between semantic IDs and the catalog, and then further finetune it on sequences of user behavior. Together, this teaches the model to make recommendations—from our catalog—based on user preferences.\n\nWhile this LLM-recommender hybrid may not match the raw precision of a specialized multi-stage recsys, it offers a new capability: **steerability and reasoning on recommendations**. The result is a unified experience across search, recommendations, and chat, giving users the ability to find what they need in our catalog by simply asking for it.\n\n> **User:** I like animal and cute games. `<|rec|>`\n>\n> > *Model interprets the natural language query (“animal and cute”) and generates semantic IDs for relevant games.*\n> >\n> > ```text\n> > <|sid_start|><|sid_173|><|sid_324|><|sid_764|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_201|><|sid_397|><|sid_701|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_173|><|sid_305|><|sid_670|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Animal Crossing: New Leaf”, “DISNEY INFINITY Starter Pack 3DS”, “Nintendogs + Cats: Golden Retriever and New Friends”\n\n## Data and models\n\n### Finding training data with both item metadata and user sequences\n\nWe’ll use data from the Video Games category of [Amazon Reviews 2023 (Hou et al., 2024)](https://amazon-reviews-2023.github.io). The data has rich product metadata and we can create user interaction sequences from it.\n\nThe dataset contains 137k products, each with fields for title, description, features, category, store information, ratings, prices, etc. After keeping only products with titles longer than 20 characters and descriptions longer than 100 characters, we’re left with 66k products. The behavioral data contains 737k records. From this, I built 79k user purchase sequences, each containing at least three valid items. These sequences had an average length of 6.5 items.\n\nI also considered the [Amazon KDD Cup 2023 data (Amazon, 2023)](https://www.aicrowd.com/challenges/amazon-kdd-cup-23-multilingual-recommendation-challenge). It had 500k items and a focus on sequential behavior data. However, its multilingual nature added complexity, and the lack of a product category field made it difficult to work with. In the end, I went with the Amazon Reviews dataset to start simple and be frugal with my compute budget.\n\n### Semantic IDs from RQ-VAEs\n\n**Semantic IDs** ([Rajput et al., 2023](https://arxiv.org/abs/2305.05065); [Singh et al., 2023](https://arxiv.org/abs/2306.08121)) are hierarchical representations that **encode items into a sequence of tokens, replacing embeddings or hash-based IDs**. Unlike a conventional item ID (`B0040JHNQG`) that has no inherent meaning, a semantic ID (`<|sid_0|><|sid_256|><|sid_512|><|sid_768|>`) encodes item information. As a result of the training process, similar items naturally share common prefixes, forming a tree-like structure where each level of the ID represents increasingly fine-grained information about the item.\n\n![Image](https://eugeneyan.com/assets/semantic-id.webp \"Image\")\n\nThe hierarchical structure of semantic IDs for food videos ([source](https://arxiv.org/abs/2306.08121))\n\n**Residual Quantized Variational Autoencoders** (RQ-VAEs; [Zeghidour et al., 2021](https://arxiv.org/abs/2107.03312), [Lee et al., 2022](https://arxiv.org/abs/2203.01941)) is what we use to convert continuous embeddings into discrete semantic IDs. We start by encoding an item’s metadata (e.g., title, description) into an embedding. The RQ-VAE then uses hierarchical quantization to convert this embedding into a sequence of discrete tokens.\n\n![Image](https://eugeneyan.com/assets/rqvae.webp \"Image\")\n\nHow RQVAEs convert embeddings to semantic IDs ([source](https://arxiv.org/abs/2305.05065))\n\nThis is an iterative process. For the first level, the model finds the closest vector in the first codebook to the input embedding; this vector becomes the first token of the Semantic ID. The model then calculates the quantization error, or *residual*, by subtracting the chosen codebook vector from the input embedding. For the second level, it finds the closest vector in the second codebook to this residual, which gives us the second token. This process repeats for each level, with each step capturing progressively finer details that the previous levels missed.\n\nThe loss function of the RQ-VAE is worth discussing, as understanding it is key to generating high-quality semantic IDs. The overall loss has two main components:\n\n\\\\\\[L(x) = L\\_\\\\text{recon} + L\\_\\\\text{rqvae}\\\\\\]\n\nThe first component, the **reconstruction loss**, ensures the decoder can accurately reconstruct the original item embedding ($x$) from the final quantized representation ($\\\\hat{x}$). It’s a standard squared error loss:\n\n\\\\\\[L\\_{recon} = ||x-\\\\hat{x}||^2\\\\\\]\n\nThe second component, the **quantization loss**, measures how well the codebook vectors match the residuals generated by the encoder. It contains two terms:\n\n\\\\\\[L\\_{\\\\text{rqvae}} := \\\\sum\\_{i=0}^{m-1} \\\\left\\[ \\\\|\\\\text{sg}\\[r\\_i\\] - e\\_{c\\_i}\\\\|^2 + \\\\beta\\\\|r\\_i - \\\\text{sg}\\[e\\_{c\\_i}\\]\\\\|^2 \\\\right\\]\\\\\\]\n\nThe first term ($|\\\\text{sg}\\[r\\_i\\] - e\\_{c\\_i}|^2)$, the **codebook loss**, is responsible for training the codebook embeddings. It measures the distance between the encoder’s residual ($r\\_i$) and the chosen codebook vector ($e\\_{c\\_i}$). The stop-gradient is applied to the encoder’s output ($\\\\text{sg}\\[r\\_i\\]$) to treat the residuals as the fixed target. Thus, the gradients only flow to the codebook vector, pulling it closer to the encoder’s output.\n\nThe second term ($\\\\beta|r\\_i - \\\\text{sg}\\[e\\_{c\\_i}\\]|^2$), the **commitment loss**, is responsible for training the encoder. It measures the same distance, but the stop-gradient is applied to the codebook vector ($\\\\text{sg}\\[e\\_{c\\_i}\\]$) instead. This stops updates to the codebook and forces the encoder to produce outputs, or *commit* to, vectors that are already in the codebook. The hyperparameter $\\\\beta$ controls the strength of this commitment penalty.\n\n```python\n# Pytorch code for loss function (without recursive loop)\nreconstruction_loss = F.mse_loss(x, x_reconstructed)\n\ncodebook_loss = F.mse_loss(residual.detach(), codebook_vector)\ncommitment_loss = F.mse_loss(residual, codebook_vector.detach())\nquantization_loss = codebook_loss + commitment_weight * commitment_loss\n\ntotal_loss = recon_loss + quantization_loss\n```\n\nThrough this process, the RQ-VAE produces a semantic ID as a sequence of tokens, one from each quantization level. Because similar items share common prefixes, language models can better understand product relationships, which is also useful for tree-based retrieval.\n\n```python\n# How to hierarchically encode embeddings to semantic IDs\ndef encode_to_semantic_ids(self, x: Tensor) -> Tensor:\n\n    with torch.no_grad():\n        residual = self.encode(x)\n        indices_list = []\n\n        for vq_layer in self.vq_layers:\n            vq_output = vq_layer(residual)\n            indices_list.append(vq_output.indices)\n            residual = residual - vq_output.quantized\n        \n    return torch.stack(indices_list, dim=-1)\n```\n\nNonetheless, one practical challenge is that this doesn’t guarantee a unique ID for every item. In my experiments with a three-level codebook, with each level having 256 codes, we saw collisions on ~10% of the 66k products. To solve this, I added a fourth level where I appended a sequentially increasing token to each ID to ensure every product is uniquely identifiable.\n\n### SASRec, Qwen3-Embedding-0.6B, and Qwen3-8b\n\nIn addition to the RQ-VAE, we use three other models. First, we’ll train a **SASRec** on semantic IDs to validate their quality and compare it to an item ID SASRec baseline. Then, we use the **Qwen3-Embedding-0.6B model** to encode product metadata to embeddings. Finally, we finetune the **Qwen3-8B model** to understand and recommend items via semantic IDs.\n\n[SASRec (Kang & McAuley, 2018)](https://arxiv.org/abs/1808.09781) is a sequential recommender inspired by the [Transformer](https://arxiv.org/abs/1706.03762) architecture. It encodes a user’s interaction history and uses a self-attention mechanism to weigh the most relevant past items in order to predict the next one. This allows the model to learn long-term dependencies in user behavior and thus outperform older recurrent models like RNNs and GRUs while being more efficient due to its parallelizable nature.\n\n[Qwen3-Embedding-0.6B (Zhang et al., 2025)](https://arxiv.org/abs/2506.05176) is part of a series of embedding models available in 0.6B, 4B, and 8B sizes. They are trained via a multi-stage process that includes pre-training on synthetic data, followed by supervised finetuning and model merging for robustness. The 8B model achieves SOTA performance on the MTEB Multilingual benchmark.\n\n[Qwen3-8B (Yang et al., 2025)](https://arxiv.org/abs/2505.09388) is a dense language model in the Qwen3 family. Despite being one of the smaller models, its post-training is optimized through strong-to-weak distillation on Qwen3-235B-A22B and Qwen3-32B. This makes the Qwen3-8B relatively capable for its size, surpassing bigger, previous-generation models like Qwen2.5-14B on more than half of the evaluated benchmarks, especially STEM and coding. Like other models in the Qwen3 series, the Qwen3-8B has dual thinking and non-thinking modes.\n\n## Cleaning data and creating user sequences\n\nFirst, we prepare the item metadata to ensure high-quality inputs for the semantic ID model. We start by excluding items that have titles with less than 20 characters or descriptions with less than 100 characters. This reduced the item count by half, from 137k to 66k unique items.\n\nNext, we clean item descriptions with Gemini 2.5 Flash Lite ([Comanici et al., 2025](https://arxiv.org/abs/2507.06261)) by fixing truncated sentences, removing HTML, and reducing verbosity. This halved the average description length from 1,038 to 538 characters. Similarly, we remove promotional text and standardize formatting for titles, turning verbose listings like “NEW! LIMITED! Sega Saturn RGB SCART LEAD CABLE…” into a clean “Sega Saturn RGB SCART Cable”.\n\nThen, we augmented the data by extracting structured metadata such as product type (Game, Hardware, Accessory), platform (PS4, Xbox, Wii), genre (Roguelike, Soulslike, Metroidvania), hardware type, brand, multiplayer modes, etc. This process had 98% coverage for platform information, 78% for brand identification, and 51% for genre classification.\n\nFinally, to build the user sequences, we deduplicated on users and built interaction histories, resulting in 91.5k sequences. From these sequences, we excluded items that had no metadata, and then filtered out sequences that had less than three items. We also truncated sequences to a maximum length of 100 items (only 28 sequences were truncated). This gave us a dataset of 78.6k sequences that have a median length of 5 items and a mean length of 6.5 items.\n\n## Training an RQ-VAE to output Semantic IDs\n\nTo embed the items, we use the Qwen3-Embedding-0.6B model. It supports customization of the input instruction for various tasks, and we add the prefix of “Given a product description, generate a semantic embedding that captures its key features and characteristics”. From this, we get 1024-dim embeddings via last token pooling and L2-normalize them before saving.\n\nThe RQ-VAE consists of an encoder, three quantization levels with 256 codes each, and a symmetric decoder. For training stability, we use the [rotation trick (Fifty et al., 2025)](https://arxiv.org/abs/2410.06424) as a replacement for the Straight-Through Estimator (to calculate the gradient for $L\\_\\\\text{recon}$). Other optimizations included initializing codebooks with k-means clustering, resetting unused codes, and using a large batch size. I also tried a few techniques that didn’t help, such as updating the codebook with EMA and stopping gradients to the decoder.\n\nThe trained RQ-VAE achieved 89% unique semantic IDs across 66k products on the three quantization levels. To resolve the remaining collisions, I appended a fourth token that assigns a unique, sequential ID (0, 1, 2, …) to any products that share the same first three codes. This ensures every product has a unique 4-part semantic ID.\n\nI ran a few dozen experiments to understand more about RQ-VAEs and their output semantic IDs, and to find an optimal configuration for the model. Here are some key findings.\n\nFirst, I experimented with the commitment weight $\\\\beta$ that balances reconstruction accuracy and codebook commitment. I tested values of 0.25 (yellow), 0.5 (orange), and 1.0 (red), and found that a higher $\\\\beta$ of 1.0 led to the most unique IDs but also had the highest validation loss. And while a lower $\\\\beta$ of 0.25 led to slightly more unique IDs, $\\\\beta$ of 0.5 had the lowest validation loss. Thus, on this dataset, I trained subsequent RQ-VAEs with $\\\\beta$ = 0.5. (Note: This differs from the Semantic ID papers which used $\\\\beta$ = 0.25.)\n\n![Image](https://eugeneyan.com/assets/rqvae-curves.webp \"Image\")\n\nCurves with beta = 0.25 (yellow), 0.5 (orange), and 1.0 (red)\n\nAside: A quick primer on what each of these metrics mean:\n\n- **loss/reconstruction**: Measures how well the RQ-VAE can reconstruct the original item embeddings after compressing it into semantic IDs and then decompressing it.\n- **loss/vq**: Combined codebook and commitment loss across all levels. Ensures encoder outputs stay closs to the codebook vectors and codebook vectors adapt to encoder outputs. Essential for compressing embeddings into meaningful semantic IDs.\n- **loss/total**: Sum of reconstruction loss and VQ loss to monitor overall progress.\n- **loss/validation**: Total loss on held-out validation set to monitor generalization.\n- **metrics/avg\\_residual\\_norm**: The amount of “leftover” residual after all quatization levels. Lower residual = codebooks are better at capturing the input embedding.\n- **metrics/unique\\_id\\_proportion**: % of items with unique IDs in a batch. Checks for codebook collapse. Higher = better ability to distingush between items.\n\nI also experimented with a shallower encoder and the impact of metadata cleaning. The shallower encoder (green) performed worse, increasing validation loss and reducing the number of unique IDs. However, investing in data cleaning paid off (blue). It led to a model with the lowest reconstruction and validation losses while having the highest proportion of unique IDs. I used the RQ-VAE from this run.\n\n![Image](https://eugeneyan.com/assets/rqvae-curves2.webp \"Image\")\n\nCurves with beta = 0.5 (orange), shallower encoder (green), and clean data (blue)\n\nAnother way to evaluate RQ-VAEs is to inspect the codebook utilization. Relatively uniform usage across all codes suggests the model is using its full expressive capacity. The final RQ-VAE demonstrates this well; across all three quantization levels, usage is spread evenly with low variance, as shown in the histogram below.\n\n![Image](https://eugeneyan.com/assets/codebook-usage.webp \"Image\")\n\nExample of an RQVAE with well distributed codebook usage\n\nIn contrast, a poorly converged RQ-VAE will have sparse and highly concentrated code usage. The histogram below shows this failure mode, where a few codes are overused, and the majority of the codebook is ignored.\n\n![Image](https://eugeneyan.com/assets/codebook-usage2.webp \"Image\")\n\nExample of an RQVAE with poorly distributed codebook usage\n\nWith the trained RQ-VAE, we encode all item embeddings into their semantic ID format, such as `<|sid_start|><|sid_191|><|sid_260|><|sid_716|><|sid_768|><|sid_end|>`. Then, we transformed the 78.6k user purchase sequences from regular IDs to semantic ID sequences. These sequences are the training data used to both validate the quality of the IDs with a SASRec baseline and to finetune the Qwen3-8B model.\n\n## Training a SASRec on regular item IDs vs. semantic IDs\n\nTo validate that our semantic IDs capture meaningful product relationships, we train two variants of the SASRec, a baseline trained on regular item IDs and a variant trained on semantic IDs, and then compare their performance.\n\nThe **baseline SASRec** follows the standard architecture. It treats each product as a distinct, atomic unit, learning an embedding for it from scratch. This is based purely on behavioral patterns. The model uses 2 causal self-attention blocks, a 64-dimensional hidden state, and is trained on the discriminative task of distinguishing the next item in a sequence from randomly sampled negative items using a binary cross-entropy (BCE) loss.\n\n```python\n# Baseline SASRec predict function\ndef predict(self, input_ids: torch.Tensor, candidate_ids: torch.Tensor) -> torch.Tensor:\n    \"\"\"Predict scores for candidate items.\n\n    Args:\n        input_ids: Item sequences [batch_size, seq_length]\n        candidate_ids: Candidate items to score [batch_size, num_candidates]\n\n    Returns:\n        Scores for each candidate [batch_size, num_candidates]\n    \"\"\"\n    # Get sequence representations\n    hidden_states = self.forward(input_ids)  # [B, T, H]\n\n    # Use only the last hidden state for prediction\n    final_hidden = hidden_states[:, -1, :]  # [B, H]\n\n    # Get candidate embeddings\n    candidate_embs = self.item_emb(candidate_ids)  # [B, C, H]\n\n    # Compute scores via dot product\n    scores = torch.bmm(candidate_embs, final_hidden.unsqueeze(-1)).squeeze(-1)  # [B, C]\n\n    return scores\n```\n\nIn contrast, the **Semantic ID SASRec** reframes recommendation as a conditional generative task. Instead of scoring items, its objective is to generate the next item’s 4-part semantic ID, token-by-token. This requires a larger architecture with 4 transformer blocks and 384-dim hidden states. Unlike the [TIGER paper](https://arxiv.org/abs/2305.05065) which uses a T5 encoder-decoder, this SASRec variant is decoder-only, making it a more direct and equitable comparison to the baseline SASRec. Because we use semantic IDs, instead having an embedding for each of the 66k items, we have a total of 1,024 token-level embeddings, with 256 tokens per level in the semantic ID.\n\n```python\n# Semantic ID SASRec predict function\ndef predict_next_item(self, input_ids: torch.Tensor, teacher_forcing: bool = True, \n    target_tokens: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:\n    \"\"\"Predict the next item's semantic ID tokens sequentially.\n\n    Args:\n        input_ids: Token sequences [batch_size, seq_length * num_levels]\n        teacher_forcing: Use ground truth for conditioning during training\n        target_tokens: Ground truth tokens for next item [batch_size, num_levels]\n\n    Returns:\n        Dictionary with logits for each level\n    \"\"\"\n    hidden_states = self.forward(input_ids)  # [B, T*L, H]\n\n    # Get representation at the last position as context for all previous items\n    last_hidden = hidden_states[:, -1, :]  # [B, H]\n\n    predictions = {}\n\n    # Sequential generation: Predict each level conditioned on previous\n    for level in range(self.num_levels):\n        if level == 0:\n            # Level 0: predict directly from sequence representation\n            context = last_hidden\n        else:\n            # Levels 1-3: condition on previously predicted/true tokens\n            if teacher_forcing and target_tokens is not None:\n                # Use ground truth previous levels during training\n                prev_tokens = target_tokens[:, :level]  # [B, level]\n            else:\n                # Use predicted tokens during inference\n                prev_tokens = self._sample_from_predictions(predictions, level)\n\n            prev_embeds = self.token_emb(prev_tokens)  # [B, level, input_dim]\n            prev_embeds_projected = self.input_projection(prev_embeds)  # [B, level, H]\n            prev_context = prev_embeds_projected.mean(dim=1)  # [B, H]\n\n            # Combine with sequence context\n            combined = torch.cat([last_hidden, prev_context], dim=-1)  # [B, 2*H]\n            context = self.context_combiners[level - 1](combined)  # [B, H]\n\n        # Predict current level\n        logits = self.level_heads[level](context)  # [B, codebook_size]\n        predictions[f\"logits_l{level}\"] = logits\n\n    return predictions\n```\n\nThis generative approach changes how we train and evaluate. The loss function is no longer a simple BCE loss but a sum of cross-entropy losses across each level of the semantic ID, forcing the model to predict the entire sequence correctly. Evaluation is also more complex, where instead of a dot product, an item’s score is its joint log-probability, calculated by summing the log-probs of generating each token. To improve training stability, we apply teacher forcing, where the ground-truth token from a prior level helps guide the prediction for the next level.\n\nTo evaluate both models, we use a validation set where we added 500 negative samples for each positive next item. While the baseline SASRec outperformed the semantic ID variant, the semantic model’s performance is respectable given the difficult generative task of predicting four correct tokens. Furthermore, the semantic ID variant has the **ability to handle cold-start items** by leveraging shared token prefixes from similar products, a capability the baseline lacks. This reveals the core trade-off, where gaining this ability to generalize requires 4x more predictions per item and requires higher training and inference compute.\n\n| Model                  | Hit@10 | NDCG@10 | MRR    | Mean Rank | Median Rank |\n| ---------------------- | ------ | ------- | ------ | --------- | ----------- |\n| **Baseline SASRec**    | 0.2812 | 0.1535  | 0.1300 | 138.9     | 41.0        |\n| **Semantic ID SASRec** | 0.2020 | 0.1138  | 0.1007 | 179.7     | 79.0        |\n\n## Fine-tuning Qwen3-8B to Recommend Semantic IDs\n\nNext, we teach a language model to converse in semantic IDs. For this, we finetune **Qwen3-8B** to become “bilingual”, fluent in both natural language and semantic IDs.\n\nFirst, we build a training dataset of 4.2 million conversational examples to teach the model about semantic IDs and recommendations. The data covers several task types, including mapping semantic IDs to their corresponding text descriptions (and vice-versa), predicting the next item in a user’s sequence, understanding relationships between item categories, and multi-hop reasoning. Each of these examples are formatted as conversations with a system prompt, a user instruction, and an assistant’s response.\n\nThen, we finetune the model in two phases. The first phase focuses on vocabulary extension, where we add 1,027 new tokens for the semantic IDs (i.e., `<|sid_start|>`, `<|sid_end|>`, `<|sid_0|>` to `<|sid_1023|>`, and `<|rec|>`) to the Qwen3-8B tokenizer and resize the model’s embedding matrix for them. During this phase, we freeze all model parameters except the input and output embedding layers, training 1.23B parameters (15.3% of the total) for 1,000 steps with a relatively high learning rate.\n\nWith the semantic ID token embeddings initialized, we proceed to the next phase of full finetuning on all 8.3B parameters. For this, we used a lower learning rate, a 3% warmup schedule, and the 8-bit AdamW optimizer for memory efficiency, training for three epochs on the full dataset. To monitor progress, we used several custom callbacks that tracked training metrics like gradient norms, measured hierarchical accuracy on recommendation tasks, and sampled model outputs on a fixed set of conversational inputs.\n\n## Chatting with the LLM-recommender hybrid\n\nThe finetuned model can then be used to recommend items, and is conversant in semantic IDs and natural language. Because semantic IDs are a native part of its vocabulary, the model generates recommendations by simply predicting the next ID in a sequence, token-by-token. For interactive sessions, a chat interface translates the model’s raw output (that contains semantic IDs) back into product titles, creating a human-readable conversation.\n\n> **User:** “Halo 3 Limited Edition -Xbox 360”, “Lost Planet: Extreme Condition - Xbox 360”, “Lost Planet: Extreme Condition Collector’s Edition -Xbox 360” `<|rec|>`\n>\n> > *Model takes the user’s interaction history (a sequence of Xbox 360 sci-fi shooters) and performs next-item prediction.*\n> >\n> > ```text\n> > <|sid_start|><|sid_64|><|sid_351|><|sid_733|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Mass Effect - Xbox 360”\n\nBasic tests show the model can provide standard next-item recommendations based on a user’s history and refine recs based on natural language like platform constraints (*“Xbox games similar to…“*) or genre (*“I like animal and cute games.”*).\n\n> **User:** Xbox games similar to “The Legend of Zelda: Ocarina of Time 3D”: `<|rec|>`\n>\n> > *Model takes a seed item (“The Legend of Zelda”) and a natural language constraint (“Xbox”) to steer recommendations to a different platform.*\n> >\n> > ```text\n> > <|sid_start|><|sid_64|><|sid_271|><|sid_576|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_64|><|sid_435|><|sid_615|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_167|><|sid_271|><|sid_578|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Fallout: New Vegas - Xbox 360 Ultimate Edition”, “Halo 4 - Xbox 360 (Standard Game)”, “Halo Reach - Legendary Edition”\n\nThe model can also explain its recommendations, describe the relationship between two items, and even suggest creative names and descriptions for product bundles.\n\n> **User:** > I just finished “Dragon Quest Heroes II Explorer’s Edition - PlayStation 4”. Suggest another `<|rec|>` and explain why:\n>\n> > *Model performs a hybrid task where it generates a relevant semantic ID and then provides an explanation for the recommendation.*\n> >\n> > ```text\n> > <|sid_start|><|sid_139|><|sid_289|><|sid_534|><|sid_768|><|sid_end|>\n> > \n> > If you liked Dragon Quest Heroes II, you might like Nights of Azure \n> > because both are action RPGs for the PlayStation 4 with a focus on \n> > combat and character progression. Both games offer a narrative-\n> > driven experience with a strong emphasis on combat mechanics, \n> > suggesting a shared player interest in this genre.\n> > ```\n>\n> **Assistant:** “Nights of Azure - PlayStation 4”\n>\n> If you liked Dragon Quest Heroes II, you might like Nights of Azure because both are action RPGs for the PlayStation 4 with a focus on combat and character progression. Both games offer a narrative-driven experience with a strong emphasis on combat mechanics, suggesting a shared player interest in this genre.\n\nFinally, while it may not match the raw accuracy of a specialized recommender, this unified interface allows users to discover products from our catalog through multi-turn dialogue, leading to the convergence of search, recsys, and chat.\n\nAnd this is all done via the model itself, without intent classifiers, routers, retrieval, etc.\n\n> **\\[TURN 1\\] User:** I’m looking for games similar to Mario Kart. `<|rec|>`\n>\n> > *Model generates semantic IDs for three kart-style racing games.*:\n> >\n> > ```text\n> > <|sid_start|><|sid_131|><|sid_503|><|sid_738|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_131|><|sid_260|><|sid_577|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_145|><|sid_416|><|sid_671|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Need for Speed Most Wanted - Sony PSP”, “Need for Speed Hot Pursuit 2 - PlayStation 2”, “Burnout Legends - Nintendo DS”\n>\n> **\\[TURN 2\\] User:** How about something similar but for Xbox? `<|rec|>`\n>\n> > *Model uses the conversation history and the new “Xbox” constraint to generate semantic IDs for relevant Xbox arcade-racers.*\n> >\n> > ```text\n> > <|sid_start|><|sid_183|><|sid_461|><|sid_517|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_183|><|sid_461|><|sid_562|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_183|><|sid_461|><|sid_616|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Need for Speed Carbon - Xbox 360”, “Project Gotham Racing 3 - Xbox 360”, “Forza Motorsport 4 - Xbox 360”\n>\n> **\\[TURN 3\\] User:** Suggest a name and description for the bundle.\n>\n> > *Model understands the context has shifted from recommendation to a creative text generation task.*\n>\n> **Assistant:**: Xbox Racing Legends: Speed & Style Pack\n\nOkay, that’s it! I hope you found this as exciting as I do! If I had more compute, I’d experiment with sentence piece tokenization on the semantic IDs, which [Deepmind and Youtube have found helpful](https://arxiv.org/abs/2306.08121), and further explore the impact of semantic IDs on cold-start. I’d also train RQ-VAEs and language models on datasets with millions of products and a greater variety of tasks and conversations to test how this approach scales and generalizes.\n\nHave you trained models or built UXes with semantic IDs? I’d love to chat, please [reach out](https://x.com/eugeneyan)!\n\nThanks to [Devansh](https://x.com/devanshtandon_) and [Shrikar](https://x.com/shrikar84) for the discussions, and [Luke](https://x.com/LukePiette) and [Runpod](https://www.runpod.io/?ref=4uddqig9) for the compute to run these experiments. (Please use my [affiliate code](https://runpod.io?ref=4uddqig9) so I get more credit for experiments!) Thanks to [Vibhu](https://x.com/vibhuuuus), [Shawn](https://x.com/swyx), [Cheng](https://x.com/_chenglou), and everyone who had to tolerate my yapping on this.\n\n## References\n\nHou, Yupeng, Jiacheng Li, Zhankui He, An Yan, Xiusi Chen, and Julian McAuley. “Bridging Language and Items for Retrieval and Recommendation.” arXiv:2403.03952. Preprint, arXiv, March 6, 2024. https://doi.org/10.48550/arXiv.2403.03952.\n\nAmazon. “Amazon KDD Cup ‘23 - Multilingual Recommendation Challenge Dataset.” AIcrowd, 2023. https://www.aicrowd.com/challenges/amazon-kdd-cup-23-multilingual-recommendation-challenge.\n\nRajput, Shashank, Nikhil Mehta, Anima Singh, et al. “Recommender Systems with Generative Retrieval.” arXiv:2305.05065. Preprint, arXiv, November 3, 2023. https://doi.org/10.48550/arXiv.2305.05065.\n\nSingh, Anima, Trung Vu, Nikhil Mehta, et al. “Better Generalization with Semantic IDs: A Case Study in Ranking for Recommendations.” arXiv:2306.08121. Preprint, arXiv, May 30, 2024. https://doi.org/10.48550/arXiv.2306.08121.\n\nZeghidour, Neil, Alejandro Luebs, Ahmed Omran, Jan Skoglund, and Marco Tagliasacchi. “SoundStream: An End-to-End Neural Audio Codec.” arXiv:2107.03312. Preprint, arXiv, July 7, 2021. https://doi.org/10.48550/arXiv.2107.03312.\n\nLee, Doyup, Chiheon Kim, Saehoon Kim, Minsu Cho, and Wook-Shin Han. “Autoregressive Image Generation Using Residual Quantization.” arXiv:2203.01941. Preprint, arXiv, March 9, 2022. https://doi.org/10.48550/arXiv.2203.01941.\n\nKang, Wang-Cheng, and Julian McAuley. “Self-Attentive Sequential Recommendation.” arXiv:1808.09781. Preprint, arXiv, August 20, 2018. https://doi.org/10.48550/arXiv.1808.09781.\n\nVaswani, Ashish, Noam Shazeer, Niki Parmar, et al. “Attention Is All You Need.” arXiv:1706.03762. Preprint, arXiv, August 2, 2023. https://doi.org/10.48550/arXiv.1706.03762.\n\nZhang, Yanzhao, Mingxin Li, Dingkun Long, et al. “Qwen3 Embedding: Advancing Text Embedding and Reranking Through Foundation Models.” arXiv:2506.05176. Preprint, arXiv, June 11, 2025. https://doi.org/10.48550/arXiv.2506.05176.\n\nYang, An, Anfeng Li, Baosong Yang, et al. “Qwen3 Technical Report.” arXiv:2505.09388. Preprint, arXiv, May 14, 2025. https://doi.org/10.48550/arXiv.2505.09388.\n\nComanici, Gheorghe, Eric Bieber, Mike Schaekermann, et al. “Gemini 2.5: Pushing the Frontier with Advanced Reasoning, Multimodality, Long Context, and Next Generation Agentic Capabilities.” arXiv:2507.06261. Preprint, arXiv, July 22, 2025. https://doi.org/10.48550/arXiv.2507.06261.\n\nIf you found this useful, please cite this write-up as:\n\n> Yan, Ziyou. (Sep 2025). Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs. eugeneyan.com. https://eugeneyan.com/writing/semantic-ids/.\n\nor\n\n```\n@article{yan2025semantic-ids,\n  title   = {Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs},\n  author  = {Yan, Ziyou},\n  journal = {eugeneyan.com},\n  year    = {2025},\n  month   = {Sep},\n  url     = {https://eugeneyan.com/writing/semantic-ids/}\n}\n```\n\nShare on:\n\n![](https://eugeneyan.com/assets/icon-twitter.svg)\n\n![](https://eugeneyan.com/assets/icon-linkedin.svg)\n\n![](https://eugeneyan.com/assets/bluesky.svg)\n\n![](https://eugeneyan.com/assets/icon-facebook.svg)\n\n![](https://eugeneyan.com/assets/icon-mail.svg)\n\nJoin **11,800+** readers getting updates on machine learning, RecSys, LLMs, and engineering.\n",
  "authors": [],
  "captured_at": "2026-09-27T19:29:15.293681927+00:00",
  "category": "programming",
  "content_html": "<p>I got nerdsniped when I first heard about Semantic IDs. The idea is simple: Instead of using random hash IDs for videos or songs or products, we can use semantically meaningful tokens that an LLM can natively understand. I wondered, could we train an LLM-recommender hybrid on the rich behavioral data that makes today’s recommender systems so effective?</p>\n<p>To my surprise, we can! The result is a language model that can converse in both English and item IDs, not with retrieval or other tools, but as a single, “bilingual” model where items (i.e., semantic IDs) are part of its vocabulary. Like a recommender model, it can recommend items given historical interactions. But the big surprise—and capability unlock—was when I found that <strong>I could simply chat with the model to steer its recommendations, and it could reason about its choices, offer explanations, and creatively name product bundles</strong>.</p>\n<p>Here’s a demo video. And here’s the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/eugeneyan/semantic-ids-llm\">code</a> to prep the data, train the model, and chat with it. (Note: This is a small model with very basic finetuning, so prompting matters a lot. Also, it’s not as general purpose and robust as most LLMs because of the limited finetuning.)</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.youtube.com/watch?v=_0n4QS--3V8\">YouTube video player</a></p>\n<h2>Wait, why is this useful?</h2>\n<p>This marries the best of recsys and language modeling. On one hand, language models have world knowledge and can eloquently talk about products, but are unaware of our catalog. Also, their recommendations are generic and suffer from popularity bias. Conversely, search and recommendation models are trained on our catalog and billions of user interactions. They excel at predicting what a user will click or buy next, but can’t be steered via natural language or reason on their choices. (What they have in common though, is learning on sequences.)</p>\n<p>To do this, we first extend the language model’s vocabulary with semantic ID tokens like <code>&lt;|sid_0|&gt;</code>, <code>&lt;|sid_1|&gt;</code>, <code>&lt;|sid_2|&gt;</code>, etc. These tokens are used to represent the catalog. Then, we apply continued pretraining to teach the model the relationships between semantic IDs and the catalog, and then further finetune it on sequences of user behavior. Together, this teaches the model to make recommendations—from our catalog—based on user preferences.</p>\n<p>While this LLM-recommender hybrid may not match the raw precision of a specialized multi-stage recsys, it offers a new capability: <strong>steerability and reasoning on recommendations</strong>. The result is a unified experience across search, recommendations, and chat, giving users the ability to find what they need in our catalog by simply asking for it.</p>\n<blockquote>\n<p><strong>User:</strong> I like animal and cute games. <code>&lt;|rec|&gt;</code></p>\n<blockquote>\n<p><em>Model interprets the natural language query (“animal and cute”) and generates semantic IDs for relevant games.</em></p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">&lt;|sid_start|&gt;&lt;|sid_173|&gt;&lt;|sid_324|&gt;&lt;|sid_764|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;, \n&lt;|sid_start|&gt;&lt;|sid_201|&gt;&lt;|sid_397|&gt;&lt;|sid_701|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;, \n&lt;|sid_start|&gt;&lt;|sid_173|&gt;&lt;|sid_305|&gt;&lt;|sid_670|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;\n</span></code></pre>\n</blockquote>\n<p><strong>Assistant:</strong> “Animal Crossing: New Leaf”, “DISNEY INFINITY Starter Pack 3DS”, “Nintendogs + Cats: Golden Retriever and New Friends”</p>\n</blockquote>\n<h2>Data and models</h2>\n<h3>Finding training data with both item metadata and user sequences</h3>\n<p>We’ll use data from the Video Games category of <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://amazon-reviews-2023.github.io\">Amazon Reviews 2023 (Hou et al., 2024)</a>. The data has rich product metadata and we can create user interaction sequences from it.</p>\n<p>The dataset contains 137k products, each with fields for title, description, features, category, store information, ratings, prices, etc. After keeping only products with titles longer than 20 characters and descriptions longer than 100 characters, we’re left with 66k products. The behavioral data contains 737k records. From this, I built 79k user purchase sequences, each containing at least three valid items. These sequences had an average length of 6.5 items.</p>\n<p>I also considered the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.aicrowd.com/challenges/amazon-kdd-cup-23-multilingual-recommendation-challenge\">Amazon KDD Cup 2023 data (Amazon, 2023)</a>. It had 500k items and a focus on sequential behavior data. However, its multilingual nature added complexity, and the lack of a product category field made it difficult to work with. In the end, I went with the Amazon Reviews dataset to start simple and be frugal with my compute budget.</p>\n<h3>Semantic IDs from RQ-VAEs</h3>\n<p><strong>Semantic IDs</strong> (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2305.05065\">Rajput et al., 2023</a>; <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2306.08121\">Singh et al., 2023</a>) are hierarchical representations that <strong>encode items into a sequence of tokens, replacing embeddings or hash-based IDs</strong>. Unlike a conventional item ID (<code>B0040JHNQG</code>) that has no inherent meaning, a semantic ID (<code>&lt;|sid_0|&gt;&lt;|sid_256|&gt;&lt;|sid_512|&gt;&lt;|sid_768|&gt;</code>) encodes item information. As a result of the training process, similar items naturally share common prefixes, forming a tree-like structure where each level of the ID represents increasingly fine-grained information about the item.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/semantic-id.webp\" alt=\"Image\" title=\"Image\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>The hierarchical structure of semantic IDs for food videos (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2306.08121\">source</a>)</p>\n<p><strong>Residual Quantized Variational Autoencoders</strong> (RQ-VAEs; <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2107.03312\">Zeghidour et al., 2021</a>, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2203.01941\">Lee et al., 2022</a>) is what we use to convert continuous embeddings into discrete semantic IDs. We start by encoding an item’s metadata (e.g., title, description) into an embedding. The RQ-VAE then uses hierarchical quantization to convert this embedding into a sequence of discrete tokens.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/rqvae.webp\" alt=\"Image\" title=\"Image\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>How RQVAEs convert embeddings to semantic IDs (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2305.05065\">source</a>)</p>\n<p>This is an iterative process. For the first level, the model finds the closest vector in the first codebook to the input embedding; this vector becomes the first token of the Semantic ID. The model then calculates the quantization error, or <em>residual</em>, by subtracting the chosen codebook vector from the input embedding. For the second level, it finds the closest vector in the second codebook to this residual, which gives us the second token. This process repeats for each level, with each step capturing progressively finer details that the previous levels missed.</p>\n<p>The loss function of the RQ-VAE is worth discussing, as understanding it is key to generating high-quality semantic IDs. The overall loss has two main components:</p>\n<p>\\[L(x) = L_\\text{recon} + L_\\text{rqvae}\\]</p>\n<p>The first component, the <strong>reconstruction loss</strong>, ensures the decoder can accurately reconstruct the original item embedding (<span class=\"math\">x</span>) from the final quantized representation (<span class=\"math\">x̂</span>). It’s a standard squared error loss:</p>\n<p>\\[L_{recon} = ||x-\\hat{x}||^2\\]</p>\n<p>The second component, the <strong>quantization loss</strong>, measures how well the codebook vectors match the residuals generated by the encoder. It contains two terms:</p>\n<p>\\[L_{\\text{rqvae}} := \\sum_{i=0}^{m-1} \\left[ \\|\\text{sg}[r_i] - e_{c_i}\\|^2 + \\beta\\|r_i - \\text{sg}[e_{c_i}]\\|^2 \\right]\\]</p>\n<p>The first term (<span class=\"math\">|sg[rᵢ] − e_(cᵢ)|²)</span>, the <strong>codebook loss</strong>, is responsible for training the codebook embeddings. It measures the distance between the encoder’s residual (<span class=\"math\">rᵢ</span>) and the chosen codebook vector (<span class=\"math\">e_(cᵢ)</span>). The stop-gradient is applied to the encoder’s output (<span class=\"math\">sg[rᵢ]</span>) to treat the residuals as the fixed target. Thus, the gradients only flow to the codebook vector, pulling it closer to the encoder’s output.</p>\n<p>The second term (<span class=\"math\">β|rᵢ − sg[e_(cᵢ)]|²</span>), the <strong>commitment loss</strong>, is responsible for training the encoder. It measures the same distance, but the stop-gradient is applied to the codebook vector (<span class=\"math\">sg[e_(cᵢ)]</span>) instead. This stops updates to the codebook and forces the encoder to produce outputs, or <em>commit</em> to, vectors that are already in the codebook. The hyperparameter <span class=\"math\">β</span> controls the strength of this commitment penalty.</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Pytorch code for loss function (without recursive loop)\n</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">reconstruction_loss</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">F</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">mse_loss</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">x</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">x_reconstructed</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n\n<span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">codebook_loss</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">F</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">mse_loss</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">residual</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">detach</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">codebook_vector</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n<span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">commitment_loss</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">F</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">mse_loss</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">residual</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">codebook_vector</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">detach</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n<span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">quantization_loss</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">codebook_loss</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">+</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">commitment_weight</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">*</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">commitment_loss</span></span>\n\n<span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">total_loss</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">recon_loss</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">+</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">quantization_loss</span></span>\n</span></span></code></pre>\n<p>Through this process, the RQ-VAE produces a semantic ID as a sequence of tokens, one from each quantization level. Because similar items share common prefixes, language models can better understand product relationships, which is also useful for tree-based retrieval.</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> How to hierarchically encode embeddings to semantic IDs\n</span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">encode_to_semantic_ids</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">x</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"> </span><span class=\"syntax-meta syntax-function syntax-annotation syntax-return syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-return syntax-python\">-&gt;</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n\n    <span class=\"syntax-meta syntax-statement syntax-with syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-with syntax-python\">with</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">no_grad</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-with syntax-python\">:</span></span>\n        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">residual</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">encode</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">x</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">indices_list</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span><span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span>\n\n        <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-python\">for</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">vq_layer</span> <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span></span><span class=\"syntax-meta syntax-statement syntax-for syntax-python\"> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">vq_layers</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-for syntax-python\">:</span></span>\n            <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">vq_output</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">vq_layer</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">residual</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n            <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">indices_list</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">append</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">vq_output</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">indices</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n            <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">residual</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">residual</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">vq_output</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">quantized</span></span>\n        \n    <span class=\"syntax-keyword syntax-control syntax-flow syntax-return syntax-python\">return</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">stack</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">indices_list</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">dim</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p>Nonetheless, one practical challenge is that this doesn’t guarantee a unique ID for every item. In my experiments with a three-level codebook, with each level having 256 codes, we saw collisions on ~10% of the 66k products. To solve this, I added a fourth level where I appended a sequentially increasing token to each ID to ensure every product is uniquely identifiable.</p>\n<h3>SASRec, Qwen3-Embedding-0.6B, and Qwen3-8b</h3>\n<p>In addition to the RQ-VAE, we use three other models. First, we’ll train a <strong>SASRec</strong> on semantic IDs to validate their quality and compare it to an item ID SASRec baseline. Then, we use the <strong>Qwen3-Embedding-0.6B model</strong> to encode product metadata to embeddings. Finally, we finetune the <strong>Qwen3-8B model</strong> to understand and recommend items via semantic IDs.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/1808.09781\">SASRec (Kang &amp; McAuley, 2018)</a> is a sequential recommender inspired by the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/1706.03762\">Transformer</a> architecture. It encodes a user’s interaction history and uses a self-attention mechanism to weigh the most relevant past items in order to predict the next one. This allows the model to learn long-term dependencies in user behavior and thus outperform older recurrent models like RNNs and GRUs while being more efficient due to its parallelizable nature.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2506.05176\">Qwen3-Embedding-0.6B (Zhang et al., 2025)</a> is part of a series of embedding models available in 0.6B, 4B, and 8B sizes. They are trained via a multi-stage process that includes pre-training on synthetic data, followed by supervised finetuning and model merging for robustness. The 8B model achieves SOTA performance on the MTEB Multilingual benchmark.</p>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2505.09388\">Qwen3-8B (Yang et al., 2025)</a> is a dense language model in the Qwen3 family. Despite being one of the smaller models, its post-training is optimized through strong-to-weak distillation on Qwen3-235B-A22B and Qwen3-32B. This makes the Qwen3-8B relatively capable for its size, surpassing bigger, previous-generation models like Qwen2.5-14B on more than half of the evaluated benchmarks, especially STEM and coding. Like other models in the Qwen3 series, the Qwen3-8B has dual thinking and non-thinking modes.</p>\n<h2>Cleaning data and creating user sequences</h2>\n<p>First, we prepare the item metadata to ensure high-quality inputs for the semantic ID model. We start by excluding items that have titles with less than 20 characters or descriptions with less than 100 characters. This reduced the item count by half, from 137k to 66k unique items.</p>\n<p>Next, we clean item descriptions with Gemini 2.5 Flash Lite (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2507.06261\">Comanici et al., 2025</a>) by fixing truncated sentences, removing HTML, and reducing verbosity. This halved the average description length from 1,038 to 538 characters. Similarly, we remove promotional text and standardize formatting for titles, turning verbose listings like “NEW! LIMITED! Sega Saturn RGB SCART LEAD CABLE…” into a clean “Sega Saturn RGB SCART Cable”.</p>\n<p>Then, we augmented the data by extracting structured metadata such as product type (Game, Hardware, Accessory), platform (PS4, Xbox, Wii), genre (Roguelike, Soulslike, Metroidvania), hardware type, brand, multiplayer modes, etc. This process had 98% coverage for platform information, 78% for brand identification, and 51% for genre classification.</p>\n<p>Finally, to build the user sequences, we deduplicated on users and built interaction histories, resulting in 91.5k sequences. From these sequences, we excluded items that had no metadata, and then filtered out sequences that had less than three items. We also truncated sequences to a maximum length of 100 items (only 28 sequences were truncated). This gave us a dataset of 78.6k sequences that have a median length of 5 items and a mean length of 6.5 items.</p>\n<h2>Training an RQ-VAE to output Semantic IDs</h2>\n<p>To embed the items, we use the Qwen3-Embedding-0.6B model. It supports customization of the input instruction for various tasks, and we add the prefix of “Given a product description, generate a semantic embedding that captures its key features and characteristics”. From this, we get 1024-dim embeddings via last token pooling and L2-normalize them before saving.</p>\n<p>The RQ-VAE consists of an encoder, three quantization levels with 256 codes each, and a symmetric decoder. For training stability, we use the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2410.06424\">rotation trick (Fifty et al., 2025)</a> as a replacement for the Straight-Through Estimator (to calculate the gradient for <span class=\"math\">L_(recon)</span>). Other optimizations included initializing codebooks with k-means clustering, resetting unused codes, and using a large batch size. I also tried a few techniques that didn’t help, such as updating the codebook with EMA and stopping gradients to the decoder.</p>\n<p>The trained RQ-VAE achieved 89% unique semantic IDs across 66k products on the three quantization levels. To resolve the remaining collisions, I appended a fourth token that assigns a unique, sequential ID (0, 1, 2, …) to any products that share the same first three codes. This ensures every product has a unique 4-part semantic ID.</p>\n<p>I ran a few dozen experiments to understand more about RQ-VAEs and their output semantic IDs, and to find an optimal configuration for the model. Here are some key findings.</p>\n<p>First, I experimented with the commitment weight <span class=\"math\">β</span> that balances reconstruction accuracy and codebook commitment. I tested values of 0.25 (yellow), 0.5 (orange), and 1.0 (red), and found that a higher <span class=\"math\">β</span> of 1.0 led to the most unique IDs but also had the highest validation loss. And while a lower <span class=\"math\">β</span> of 0.25 led to slightly more unique IDs, <span class=\"math\">β</span> of 0.5 had the lowest validation loss. Thus, on this dataset, I trained subsequent RQ-VAEs with <span class=\"math\">β</span> = 0.5. (Note: This differs from the Semantic ID papers which used <span class=\"math\">β</span> = 0.25.)</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/rqvae-curves.webp\" alt=\"Image\" title=\"Image\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Curves with beta = 0.25 (yellow), 0.5 (orange), and 1.0 (red)</p>\n<p>Aside: A quick primer on what each of these metrics mean:</p>\n<ul>\n<li><strong>loss/reconstruction</strong>: Measures how well the RQ-VAE can reconstruct the original item embeddings after compressing it into semantic IDs and then decompressing it.</li>\n<li><strong>loss/vq</strong>: Combined codebook and commitment loss across all levels. Ensures encoder outputs stay closs to the codebook vectors and codebook vectors adapt to encoder outputs. Essential for compressing embeddings into meaningful semantic IDs.</li>\n<li><strong>loss/total</strong>: Sum of reconstruction loss and VQ loss to monitor overall progress.</li>\n<li><strong>loss/validation</strong>: Total loss on held-out validation set to monitor generalization.</li>\n<li><strong>metrics/avg_residual_norm</strong>: The amount of “leftover” residual after all quatization levels. Lower residual = codebooks are better at capturing the input embedding.</li>\n<li><strong>metrics/unique_id_proportion</strong>: % of items with unique IDs in a batch. Checks for codebook collapse. Higher = better ability to distingush between items.</li>\n</ul>\n<p>I also experimented with a shallower encoder and the impact of metadata cleaning. The shallower encoder (green) performed worse, increasing validation loss and reducing the number of unique IDs. However, investing in data cleaning paid off (blue). It led to a model with the lowest reconstruction and validation losses while having the highest proportion of unique IDs. I used the RQ-VAE from this run.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/rqvae-curves2.webp\" alt=\"Image\" title=\"Image\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Curves with beta = 0.5 (orange), shallower encoder (green), and clean data (blue)</p>\n<p>Another way to evaluate RQ-VAEs is to inspect the codebook utilization. Relatively uniform usage across all codes suggests the model is using its full expressive capacity. The final RQ-VAE demonstrates this well; across all three quantization levels, usage is spread evenly with low variance, as shown in the histogram below.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/codebook-usage.webp\" alt=\"Image\" title=\"Image\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Example of an RQVAE with well distributed codebook usage</p>\n<p>In contrast, a poorly converged RQ-VAE will have sparse and highly concentrated code usage. The histogram below shows this failure mode, where a few codes are overused, and the majority of the codebook is ignored.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/codebook-usage2.webp\" alt=\"Image\" title=\"Image\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Example of an RQVAE with poorly distributed codebook usage</p>\n<p>With the trained RQ-VAE, we encode all item embeddings into their semantic ID format, such as <code>&lt;|sid_start|&gt;&lt;|sid_191|&gt;&lt;|sid_260|&gt;&lt;|sid_716|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;</code>. Then, we transformed the 78.6k user purchase sequences from regular IDs to semantic ID sequences. These sequences are the training data used to both validate the quality of the IDs with a SASRec baseline and to finetune the Qwen3-8B model.</p>\n<h2>Training a SASRec on regular item IDs vs. semantic IDs</h2>\n<p>To validate that our semantic IDs capture meaningful product relationships, we train two variants of the SASRec, a baseline trained on regular item IDs and a variant trained on semantic IDs, and then compare their performance.</p>\n<p>The <strong>baseline SASRec</strong> follows the standard architecture. It treats each product as a distinct, atomic unit, learning an embedding for it from scratch. This is based purely on behavioral patterns. The model uses 2 causal self-attention blocks, a 64-dimensional hidden state, and is trained on the discriminative task of distinguishing the next item in a sequence from randomly sampled negative items using a binary cross-entropy (BCE) loss.</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Baseline SASRec predict function\n</span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">predict</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">input_ids</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">candidate_ids</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"> </span><span class=\"syntax-meta syntax-function syntax-annotation syntax-return syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-return syntax-python\">-&gt;</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n    <span class=\"syntax-comment syntax-block syntax-documentation syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-python\">&quot;&quot;&quot;</span>Predict scores for candidate items.\n\n    Args:\n        input_ids: Item sequences [batch_size, seq_length]\n        candidate_ids: Candidate items to score [batch_size, num_candidates]\n\n    Returns:\n        Scores for each candidate [batch_size, num_candidates]\n    <span class=\"syntax-punctuation syntax-definition syntax-comment syntax-end syntax-python\">&quot;&quot;&quot;</span></span>\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Get sequence representations\n</span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">hidden_states</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">forward</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">input_ids</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, T, H]\n</span>\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Use only the last hidden state for prediction\n</span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">final_hidden</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">hidden_states</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span>, <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span>, <span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, H]\n</span>\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Get candidate embeddings\n</span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">candidate_embs</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">item_emb</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">candidate_ids</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, C, H]\n</span>\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Compute scores via dot product\n</span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">scores</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">bmm</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">candidate_embs</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">final_hidden</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">unsqueeze</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">squeeze</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, C]\n</span>\n    <span class=\"syntax-keyword syntax-control syntax-flow syntax-return syntax-python\">return</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">scores</span></span>\n</span></span></code></pre>\n<p>In contrast, the <strong>Semantic ID SASRec</strong> reframes recommendation as a conditional generative task. Instead of scoring items, its objective is to generate the next item’s 4-part semantic ID, token-by-token. This requires a larger architecture with 4 transformer blocks and 384-dim hidden states. Unlike the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2305.05065\">TIGER paper</a> which uses a T5 encoder-decoder, this SASRec variant is decoder-only, making it a more direct and equitable comparison to the baseline SASRec. Because we use semantic IDs, instead having an embedding for each of the 66k items, we have a total of 1,024 token-level embeddings, with 256 tokens per level in the semantic ID.</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Semantic ID SASRec predict function\n</span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">predict_next_item</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">input_ids</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">teacher_forcing</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">bool</span></span> </span><span class=\"syntax-meta syntax-function syntax-parameters syntax-default-value syntax-python\"><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-constant syntax-language syntax-python\">True</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> \n    <span class=\"syntax-variable syntax-parameter syntax-python\">target_tokens</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">Optional</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> </span><span class=\"syntax-meta syntax-function syntax-parameters syntax-default-value syntax-python\"><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-constant syntax-language syntax-python\">None</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"> </span><span class=\"syntax-meta syntax-function syntax-annotation syntax-return syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-return syntax-python\">-&gt;</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">Dict</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span>, <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n    <span class=\"syntax-comment syntax-block syntax-documentation syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-python\">&quot;&quot;&quot;</span>Predict the next item&#39;s semantic ID tokens sequentially.\n\n    Args:\n        input_ids: Token sequences [batch_size, seq_length * num_levels]\n        teacher_forcing: Use ground truth for conditioning during training\n        target_tokens: Ground truth tokens for next item [batch_size, num_levels]\n\n    Returns:\n        Dictionary with logits for each level\n    <span class=\"syntax-punctuation syntax-definition syntax-comment syntax-end syntax-python\">&quot;&quot;&quot;</span></span>\n    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">hidden_states</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">forward</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">input_ids</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, T*L, H]\n</span>\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Get representation at the last position as context for all previous items\n</span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">last_hidden</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">hidden_states</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span>, <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span>, <span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, H]\n</span>\n    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">predictions</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-structure syntax-dictionary syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-dictionary syntax-begin syntax-python\">{</span><span class=\"syntax-meta syntax-empty-dictionary syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-dictionary syntax-end syntax-python\">}</span></span></span>\n\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Sequential generation: Predict each level conditioned on previous\n</span>    <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-python\">for</span> <span class=\"syntax-meta syntax-generic-name syntax-python\">level</span> <span class=\"syntax-meta syntax-statement syntax-for syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-for syntax-in syntax-python\">in</span></span></span><span class=\"syntax-meta syntax-statement syntax-for syntax-python\"> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-function syntax-builtin syntax-python\">range</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">num_levels</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-for syntax-python\">:</span></span>\n        <span class=\"syntax-meta syntax-statement syntax-if syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">if</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">level</span></span> <span class=\"syntax-keyword syntax-operator syntax-comparison syntax-python\">==</span> <span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">0</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-conditional syntax-python\">:</span></span>\n            <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Level 0: predict directly from sequence representation\n</span>            <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">context</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">last_hidden</span></span>\n        <span class=\"syntax-meta syntax-statement syntax-conditional syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">else</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-python\">:</span></span>\n            <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Levels 1-3: condition on previously predicted/true tokens\n</span>            <span class=\"syntax-meta syntax-statement syntax-if syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">if</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">teacher_forcing</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">and</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">target_tokens</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">is</span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-python\">not</span> <span class=\"syntax-constant syntax-language syntax-python\">None</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-conditional syntax-python\">:</span></span>\n                <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Use ground truth previous levels during training\n</span>                <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_tokens</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">target_tokens</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span>, <span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">level</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, level]\n</span>            <span class=\"syntax-meta syntax-statement syntax-conditional syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">else</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-python\">:</span></span>\n                <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Use predicted tokens during inference\n</span>                <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_tokens</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">_sample_from_predictions</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">predictions</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">level</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n\n            <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_embeds</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">token_emb</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_tokens</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, level, input_dim]\n</span>            <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_embeds_projected</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">input_projection</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_embeds</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, level, H]\n</span>            <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_context</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_embeds_projected</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">mean</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">dim</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, H]\n</span>\n            <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Combine with sequence context\n</span>            <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">combined</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">cat</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">last_hidden</span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">prev_context</span></span><span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">dim</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, 2*H]\n</span>            <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">context</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">context_combiners</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">level</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span> <span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">combined</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, H]\n</span>\n        <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Predict current level\n</span>        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">logits</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">level_heads</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">level</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">context</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> [B, codebook_size]\n</span>        <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">predictions</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-storage syntax-type syntax-string syntax-python\">f</span><span class=\"syntax-meta syntax-string syntax-interpolated syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-interpolated syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">logits_l</span><span class=\"syntax-meta syntax-interpolation syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-interpolation syntax-begin syntax-python\">{</span><span class=\"syntax-source syntax-python syntax-embedded\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">level</span></span></span></span><span class=\"syntax-meta syntax-interpolation syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-interpolation syntax-end syntax-python\">}</span></span><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">logits</span></span>\n\n    <span class=\"syntax-keyword syntax-control syntax-flow syntax-return syntax-python\">return</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">predictions</span></span>\n</span></span></code></pre>\n<p>This generative approach changes how we train and evaluate. The loss function is no longer a simple BCE loss but a sum of cross-entropy losses across each level of the semantic ID, forcing the model to predict the entire sequence correctly. Evaluation is also more complex, where instead of a dot product, an item’s score is its joint log-probability, calculated by summing the log-probs of generating each token. To improve training stability, we apply teacher forcing, where the ground-truth token from a prior level helps guide the prediction for the next level.</p>\n<p>To evaluate both models, we use a validation set where we added 500 negative samples for each positive next item. While the baseline SASRec outperformed the semantic ID variant, the semantic model’s performance is respectable given the difficult generative task of predicting four correct tokens. Furthermore, the semantic ID variant has the <strong>ability to handle cold-start items</strong> by leveraging shared token prefixes from similar products, a capability the baseline lacks. This reveals the core trade-off, where gaining this ability to generalize requires 4x more predictions per item and requires higher training and inference compute.</p>\n<table>\n<thead>\n<tr>\n<th>Model</th>\n<th>Hit@10</th>\n<th>NDCG@10</th>\n<th>MRR</th>\n<th>Mean Rank</th>\n<th>Median Rank</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Baseline SASRec</strong></td>\n<td>0.2812</td>\n<td>0.1535</td>\n<td>0.1300</td>\n<td>138.9</td>\n<td>41.0</td>\n</tr>\n<tr>\n<td><strong>Semantic ID SASRec</strong></td>\n<td>0.2020</td>\n<td>0.1138</td>\n<td>0.1007</td>\n<td>179.7</td>\n<td>79.0</td>\n</tr>\n</tbody>\n</table>\n<h2>Fine-tuning Qwen3-8B to Recommend Semantic IDs</h2>\n<p>Next, we teach a language model to converse in semantic IDs. For this, we finetune <strong>Qwen3-8B</strong> to become “bilingual”, fluent in both natural language and semantic IDs.</p>\n<p>First, we build a training dataset of 4.2 million conversational examples to teach the model about semantic IDs and recommendations. The data covers several task types, including mapping semantic IDs to their corresponding text descriptions (and vice-versa), predicting the next item in a user’s sequence, understanding relationships between item categories, and multi-hop reasoning. Each of these examples are formatted as conversations with a system prompt, a user instruction, and an assistant’s response.</p>\n<p>Then, we finetune the model in two phases. The first phase focuses on vocabulary extension, where we add 1,027 new tokens for the semantic IDs (i.e., <code>&lt;|sid_start|&gt;</code>, <code>&lt;|sid_end|&gt;</code>, <code>&lt;|sid_0|&gt;</code> to <code>&lt;|sid_1023|&gt;</code>, and <code>&lt;|rec|&gt;</code>) to the Qwen3-8B tokenizer and resize the model’s embedding matrix for them. During this phase, we freeze all model parameters except the input and output embedding layers, training 1.23B parameters (15.3% of the total) for 1,000 steps with a relatively high learning rate.</p>\n<p>With the semantic ID token embeddings initialized, we proceed to the next phase of full finetuning on all 8.3B parameters. For this, we used a lower learning rate, a 3% warmup schedule, and the 8-bit AdamW optimizer for memory efficiency, training for three epochs on the full dataset. To monitor progress, we used several custom callbacks that tracked training metrics like gradient norms, measured hierarchical accuracy on recommendation tasks, and sampled model outputs on a fixed set of conversational inputs.</p>\n<h2>Chatting with the LLM-recommender hybrid</h2>\n<p>The finetuned model can then be used to recommend items, and is conversant in semantic IDs and natural language. Because semantic IDs are a native part of its vocabulary, the model generates recommendations by simply predicting the next ID in a sequence, token-by-token. For interactive sessions, a chat interface translates the model’s raw output (that contains semantic IDs) back into product titles, creating a human-readable conversation.</p>\n<blockquote>\n<p><strong>User:</strong> “Halo 3 Limited Edition -Xbox 360”, “Lost Planet: Extreme Condition - Xbox 360”, “Lost Planet: Extreme Condition Collector’s Edition -Xbox 360” <code>&lt;|rec|&gt;</code></p>\n<blockquote>\n<p><em>Model takes the user’s interaction history (a sequence of Xbox 360 sci-fi shooters) and performs next-item prediction.</em></p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">&lt;|sid_start|&gt;&lt;|sid_64|&gt;&lt;|sid_351|&gt;&lt;|sid_733|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;\n</span></code></pre>\n</blockquote>\n<p><strong>Assistant:</strong> “Mass Effect - Xbox 360”</p>\n</blockquote>\n<p>Basic tests show the model can provide standard next-item recommendations based on a user’s history and refine recs based on natural language like platform constraints (<em>“Xbox games similar to…“</em>) or genre (<em>“I like animal and cute games.”</em>).</p>\n<blockquote>\n<p><strong>User:</strong> Xbox games similar to “The Legend of Zelda: Ocarina of Time 3D”: <code>&lt;|rec|&gt;</code></p>\n<blockquote>\n<p><em>Model takes a seed item (“The Legend of Zelda”) and a natural language constraint (“Xbox”) to steer recommendations to a different platform.</em></p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">&lt;|sid_start|&gt;&lt;|sid_64|&gt;&lt;|sid_271|&gt;&lt;|sid_576|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;, \n&lt;|sid_start|&gt;&lt;|sid_64|&gt;&lt;|sid_435|&gt;&lt;|sid_615|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;, \n&lt;|sid_start|&gt;&lt;|sid_167|&gt;&lt;|sid_271|&gt;&lt;|sid_578|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;\n</span></code></pre>\n</blockquote>\n<p><strong>Assistant:</strong> “Fallout: New Vegas - Xbox 360 Ultimate Edition”, “Halo 4 - Xbox 360 (Standard Game)”, “Halo Reach - Legendary Edition”</p>\n</blockquote>\n<p>The model can also explain its recommendations, describe the relationship between two items, and even suggest creative names and descriptions for product bundles.</p>\n<blockquote>\n<p><strong>User:</strong> &gt; I just finished “Dragon Quest Heroes II Explorer’s Edition - PlayStation 4”. Suggest another <code>&lt;|rec|&gt;</code> and explain why:</p>\n<blockquote>\n<p><em>Model performs a hybrid task where it generates a relevant semantic ID and then provides an explanation for the recommendation.</em></p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">&lt;|sid_start|&gt;&lt;|sid_139|&gt;&lt;|sid_289|&gt;&lt;|sid_534|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;\n\nIf you liked Dragon Quest Heroes II, you might like Nights of Azure \nbecause both are action RPGs for the PlayStation 4 with a focus on \ncombat and character progression. Both games offer a narrative-\ndriven experience with a strong emphasis on combat mechanics, \nsuggesting a shared player interest in this genre.\n</span></code></pre>\n</blockquote>\n<p><strong>Assistant:</strong> “Nights of Azure - PlayStation 4”</p>\n<p>If you liked Dragon Quest Heroes II, you might like Nights of Azure because both are action RPGs for the PlayStation 4 with a focus on combat and character progression. Both games offer a narrative-driven experience with a strong emphasis on combat mechanics, suggesting a shared player interest in this genre.</p>\n</blockquote>\n<p>Finally, while it may not match the raw accuracy of a specialized recommender, this unified interface allows users to discover products from our catalog through multi-turn dialogue, leading to the convergence of search, recsys, and chat.</p>\n<p>And this is all done via the model itself, without intent classifiers, routers, retrieval, etc.</p>\n<blockquote>\n<p><strong>[TURN 1] User:</strong> I’m looking for games similar to Mario Kart. <code>&lt;|rec|&gt;</code></p>\n<blockquote>\n<p><em>Model generates semantic IDs for three kart-style racing games.</em>:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">&lt;|sid_start|&gt;&lt;|sid_131|&gt;&lt;|sid_503|&gt;&lt;|sid_738|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;, \n&lt;|sid_start|&gt;&lt;|sid_131|&gt;&lt;|sid_260|&gt;&lt;|sid_577|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;, \n&lt;|sid_start|&gt;&lt;|sid_145|&gt;&lt;|sid_416|&gt;&lt;|sid_671|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;\n</span></code></pre>\n</blockquote>\n<p><strong>Assistant:</strong> “Need for Speed Most Wanted - Sony PSP”, “Need for Speed Hot Pursuit 2 - PlayStation 2”, “Burnout Legends - Nintendo DS”</p>\n<p><strong>[TURN 2] User:</strong> How about something similar but for Xbox? <code>&lt;|rec|&gt;</code></p>\n<blockquote>\n<p><em>Model uses the conversation history and the new “Xbox” constraint to generate semantic IDs for relevant Xbox arcade-racers.</em></p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">&lt;|sid_start|&gt;&lt;|sid_183|&gt;&lt;|sid_461|&gt;&lt;|sid_517|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;, \n&lt;|sid_start|&gt;&lt;|sid_183|&gt;&lt;|sid_461|&gt;&lt;|sid_562|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;, \n&lt;|sid_start|&gt;&lt;|sid_183|&gt;&lt;|sid_461|&gt;&lt;|sid_616|&gt;&lt;|sid_768|&gt;&lt;|sid_end|&gt;\n</span></code></pre>\n</blockquote>\n<p><strong>Assistant:</strong> “Need for Speed Carbon - Xbox 360”, “Project Gotham Racing 3 - Xbox 360”, “Forza Motorsport 4 - Xbox 360”</p>\n<p><strong>[TURN 3] User:</strong> Suggest a name and description for the bundle.</p>\n<blockquote>\n<p><em>Model understands the context has shifted from recommendation to a creative text generation task.</em></p>\n</blockquote>\n<p><strong>Assistant:</strong>: Xbox Racing Legends: Speed &amp; Style Pack</p>\n</blockquote>\n<p>Okay, that’s it! I hope you found this as exciting as I do! If I had more compute, I’d experiment with sentence piece tokenization on the semantic IDs, which <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2306.08121\">Deepmind and Youtube have found helpful</a>, and further explore the impact of semantic IDs on cold-start. I’d also train RQ-VAEs and language models on datasets with millions of products and a greater variety of tasks and conversations to test how this approach scales and generalizes.</p>\n<p>Have you trained models or built UXes with semantic IDs? I’d love to chat, please <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/eugeneyan\">reach out</a>!</p>\n<p>Thanks to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/devanshtandon_\">Devansh</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/shrikar84\">Shrikar</a> for the discussions, and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/LukePiette\">Luke</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.runpod.io/?ref=4uddqig9\">Runpod</a> for the compute to run these experiments. (Please use my <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://runpod.io?ref=4uddqig9\">affiliate code</a> so I get more credit for experiments!) Thanks to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/vibhuuuus\">Vibhu</a>, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/swyx\">Shawn</a>, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/_chenglou\">Cheng</a>, and everyone who had to tolerate my yapping on this.</p>\n<h2>References</h2>\n<p>Hou, Yupeng, Jiacheng Li, Zhankui He, An Yan, Xiusi Chen, and Julian McAuley. “Bridging Language and Items for Retrieval and Recommendation.” arXiv:2403.03952. Preprint, arXiv, March 6, 2024. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.2403.03952\">https://doi.org/10.48550/arXiv.2403.03952</a>.</p>\n<p>Amazon. “Amazon KDD Cup ‘23 - Multilingual Recommendation Challenge Dataset.” AIcrowd, 2023. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.aicrowd.com/challenges/amazon-kdd-cup-23-multilingual-recommendation-challenge\">https://www.aicrowd.com/challenges/amazon-kdd-cup-23-multilingual-recommendation-challenge</a>.</p>\n<p>Rajput, Shashank, Nikhil Mehta, Anima Singh, et al. “Recommender Systems with Generative Retrieval.” arXiv:2305.05065. Preprint, arXiv, November 3, 2023. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.2305.05065\">https://doi.org/10.48550/arXiv.2305.05065</a>.</p>\n<p>Singh, Anima, Trung Vu, Nikhil Mehta, et al. “Better Generalization with Semantic IDs: A Case Study in Ranking for Recommendations.” arXiv:2306.08121. Preprint, arXiv, May 30, 2024. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.2306.08121\">https://doi.org/10.48550/arXiv.2306.08121</a>.</p>\n<p>Zeghidour, Neil, Alejandro Luebs, Ahmed Omran, Jan Skoglund, and Marco Tagliasacchi. “SoundStream: An End-to-End Neural Audio Codec.” arXiv:2107.03312. Preprint, arXiv, July 7, 2021. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.2107.03312\">https://doi.org/10.48550/arXiv.2107.03312</a>.</p>\n<p>Lee, Doyup, Chiheon Kim, Saehoon Kim, Minsu Cho, and Wook-Shin Han. “Autoregressive Image Generation Using Residual Quantization.” arXiv:2203.01941. Preprint, arXiv, March 9, 2022. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.2203.01941\">https://doi.org/10.48550/arXiv.2203.01941</a>.</p>\n<p>Kang, Wang-Cheng, and Julian McAuley. “Self-Attentive Sequential Recommendation.” arXiv:1808.09781. Preprint, arXiv, August 20, 2018. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.1808.09781\">https://doi.org/10.48550/arXiv.1808.09781</a>.</p>\n<p>Vaswani, Ashish, Noam Shazeer, Niki Parmar, et al. “Attention Is All You Need.” arXiv:1706.03762. Preprint, arXiv, August 2, 2023. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.1706.03762\">https://doi.org/10.48550/arXiv.1706.03762</a>.</p>\n<p>Zhang, Yanzhao, Mingxin Li, Dingkun Long, et al. “Qwen3 Embedding: Advancing Text Embedding and Reranking Through Foundation Models.” arXiv:2506.05176. Preprint, arXiv, June 11, 2025. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.2506.05176\">https://doi.org/10.48550/arXiv.2506.05176</a>.</p>\n<p>Yang, An, Anfeng Li, Baosong Yang, et al. “Qwen3 Technical Report.” arXiv:2505.09388. Preprint, arXiv, May 14, 2025. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.2505.09388\">https://doi.org/10.48550/arXiv.2505.09388</a>.</p>\n<p>Comanici, Gheorghe, Eric Bieber, Mike Schaekermann, et al. “Gemini 2.5: Pushing the Frontier with Advanced Reasoning, Multimodality, Long Context, and Next Generation Agentic Capabilities.” arXiv:2507.06261. Preprint, arXiv, July 22, 2025. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://doi.org/10.48550/arXiv.2507.06261\">https://doi.org/10.48550/arXiv.2507.06261</a>.</p>\n<p>If you found this useful, please cite this write-up as:</p>\n<blockquote>\n<p>Yan, Ziyou. (Sep 2025). Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs. eugeneyan.com. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://eugeneyan.com/writing/semantic-ids/\">https://eugeneyan.com/writing/semantic-ids/</a>.</p>\n</blockquote>\n<p>or</p>\n<pre><code><span class=\"code-snippet\">@article{yan2025semantic-ids,\n  title   = {Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs},\n  author  = {Yan, Ziyou},\n  journal = {eugeneyan.com},\n  year    = {2025},\n  month   = {Sep},\n  url     = {https://eugeneyan.com/writing/semantic-ids/}\n}\n</span></code></pre>\n<p>Share on:</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/icon-twitter.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/icon-linkedin.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/bluesky.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/icon-facebook.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://eugeneyan.com/assets/icon-mail.svg\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Join <strong>11,800+</strong> readers getting updates on machine learning, RecSys, LLMs, and engineering.</p>\n",
  "content_markdown": "I got nerdsniped when I first heard about Semantic IDs. The idea is simple: Instead of using random hash IDs for videos or songs or products, we can use semantically meaningful tokens that an LLM can natively understand. I wondered, could we train an LLM-recommender hybrid on the rich behavioral data that makes today’s recommender systems so effective?\n\nTo my surprise, we can! The result is a language model that can converse in both English and item IDs, not with retrieval or other tools, but as a single, “bilingual” model where items (i.e., semantic IDs) are part of its vocabulary. Like a recommender model, it can recommend items given historical interactions. But the big surprise—and capability unlock—was when I found that **I could simply chat with the model to steer its recommendations, and it could reason about its choices, offer explanations, and creatively name product bundles**.\n\nHere’s a demo video. And here’s the [code](https://github.com/eugeneyan/semantic-ids-llm) to prep the data, train the model, and chat with it. (Note: This is a small model with very basic finetuning, so prompting matters a lot. Also, it’s not as general purpose and robust as most LLMs because of the limited finetuning.)\n\n[YouTube video player](https://www.youtube.com/watch?v=_0n4QS--3V8)\n\n## Wait, why is this useful?\n\nThis marries the best of recsys and language modeling. On one hand, language models have world knowledge and can eloquently talk about products, but are unaware of our catalog. Also, their recommendations are generic and suffer from popularity bias. Conversely, search and recommendation models are trained on our catalog and billions of user interactions. They excel at predicting what a user will click or buy next, but can’t be steered via natural language or reason on their choices. (What they have in common though, is learning on sequences.)\n\nTo do this, we first extend the language model’s vocabulary with semantic ID tokens like `<|sid_0|>`, `<|sid_1|>`, `<|sid_2|>`, etc. These tokens are used to represent the catalog. Then, we apply continued pretraining to teach the model the relationships between semantic IDs and the catalog, and then further finetune it on sequences of user behavior. Together, this teaches the model to make recommendations—from our catalog—based on user preferences.\n\nWhile this LLM-recommender hybrid may not match the raw precision of a specialized multi-stage recsys, it offers a new capability: **steerability and reasoning on recommendations**. The result is a unified experience across search, recommendations, and chat, giving users the ability to find what they need in our catalog by simply asking for it.\n\n> **User:** I like animal and cute games. `<|rec|>`\n>\n> > *Model interprets the natural language query (“animal and cute”) and generates semantic IDs for relevant games.*\n> >\n> > ```text\n> > <|sid_start|><|sid_173|><|sid_324|><|sid_764|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_201|><|sid_397|><|sid_701|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_173|><|sid_305|><|sid_670|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Animal Crossing: New Leaf”, “DISNEY INFINITY Starter Pack 3DS”, “Nintendogs + Cats: Golden Retriever and New Friends”\n\n## Data and models\n\n### Finding training data with both item metadata and user sequences\n\nWe’ll use data from the Video Games category of [Amazon Reviews 2023 (Hou et al., 2024)](https://amazon-reviews-2023.github.io). The data has rich product metadata and we can create user interaction sequences from it.\n\nThe dataset contains 137k products, each with fields for title, description, features, category, store information, ratings, prices, etc. After keeping only products with titles longer than 20 characters and descriptions longer than 100 characters, we’re left with 66k products. The behavioral data contains 737k records. From this, I built 79k user purchase sequences, each containing at least three valid items. These sequences had an average length of 6.5 items.\n\nI also considered the [Amazon KDD Cup 2023 data (Amazon, 2023)](https://www.aicrowd.com/challenges/amazon-kdd-cup-23-multilingual-recommendation-challenge). It had 500k items and a focus on sequential behavior data. However, its multilingual nature added complexity, and the lack of a product category field made it difficult to work with. In the end, I went with the Amazon Reviews dataset to start simple and be frugal with my compute budget.\n\n### Semantic IDs from RQ-VAEs\n\n**Semantic IDs** ([Rajput et al., 2023](https://arxiv.org/abs/2305.05065); [Singh et al., 2023](https://arxiv.org/abs/2306.08121)) are hierarchical representations that **encode items into a sequence of tokens, replacing embeddings or hash-based IDs**. Unlike a conventional item ID (`B0040JHNQG`) that has no inherent meaning, a semantic ID (`<|sid_0|><|sid_256|><|sid_512|><|sid_768|>`) encodes item information. As a result of the training process, similar items naturally share common prefixes, forming a tree-like structure where each level of the ID represents increasingly fine-grained information about the item.\n\n![Image](https://eugeneyan.com/assets/semantic-id.webp \"Image\")\n\nThe hierarchical structure of semantic IDs for food videos ([source](https://arxiv.org/abs/2306.08121))\n\n**Residual Quantized Variational Autoencoders** (RQ-VAEs; [Zeghidour et al., 2021](https://arxiv.org/abs/2107.03312), [Lee et al., 2022](https://arxiv.org/abs/2203.01941)) is what we use to convert continuous embeddings into discrete semantic IDs. We start by encoding an item’s metadata (e.g., title, description) into an embedding. The RQ-VAE then uses hierarchical quantization to convert this embedding into a sequence of discrete tokens.\n\n![Image](https://eugeneyan.com/assets/rqvae.webp \"Image\")\n\nHow RQVAEs convert embeddings to semantic IDs ([source](https://arxiv.org/abs/2305.05065))\n\nThis is an iterative process. For the first level, the model finds the closest vector in the first codebook to the input embedding; this vector becomes the first token of the Semantic ID. The model then calculates the quantization error, or *residual*, by subtracting the chosen codebook vector from the input embedding. For the second level, it finds the closest vector in the second codebook to this residual, which gives us the second token. This process repeats for each level, with each step capturing progressively finer details that the previous levels missed.\n\nThe loss function of the RQ-VAE is worth discussing, as understanding it is key to generating high-quality semantic IDs. The overall loss has two main components:\n\n\\\\\\[L(x) = L\\_\\\\text{recon} + L\\_\\\\text{rqvae}\\\\\\]\n\nThe first component, the **reconstruction loss**, ensures the decoder can accurately reconstruct the original item embedding ($x$) from the final quantized representation ($\\\\hat{x}$). It’s a standard squared error loss:\n\n\\\\\\[L\\_{recon} = ||x-\\\\hat{x}||^2\\\\\\]\n\nThe second component, the **quantization loss**, measures how well the codebook vectors match the residuals generated by the encoder. It contains two terms:\n\n\\\\\\[L\\_{\\\\text{rqvae}} := \\\\sum\\_{i=0}^{m-1} \\\\left\\[ \\\\|\\\\text{sg}\\[r\\_i\\] - e\\_{c\\_i}\\\\|^2 + \\\\beta\\\\|r\\_i - \\\\text{sg}\\[e\\_{c\\_i}\\]\\\\|^2 \\\\right\\]\\\\\\]\n\nThe first term ($|\\\\text{sg}\\[r\\_i\\] - e\\_{c\\_i}|^2)$, the **codebook loss**, is responsible for training the codebook embeddings. It measures the distance between the encoder’s residual ($r\\_i$) and the chosen codebook vector ($e\\_{c\\_i}$). The stop-gradient is applied to the encoder’s output ($\\\\text{sg}\\[r\\_i\\]$) to treat the residuals as the fixed target. Thus, the gradients only flow to the codebook vector, pulling it closer to the encoder’s output.\n\nThe second term ($\\\\beta|r\\_i - \\\\text{sg}\\[e\\_{c\\_i}\\]|^2$), the **commitment loss**, is responsible for training the encoder. It measures the same distance, but the stop-gradient is applied to the codebook vector ($\\\\text{sg}\\[e\\_{c\\_i}\\]$) instead. This stops updates to the codebook and forces the encoder to produce outputs, or *commit* to, vectors that are already in the codebook. The hyperparameter $\\\\beta$ controls the strength of this commitment penalty.\n\n```python\n# Pytorch code for loss function (without recursive loop)\nreconstruction_loss = F.mse_loss(x, x_reconstructed)\n\ncodebook_loss = F.mse_loss(residual.detach(), codebook_vector)\ncommitment_loss = F.mse_loss(residual, codebook_vector.detach())\nquantization_loss = codebook_loss + commitment_weight * commitment_loss\n\ntotal_loss = recon_loss + quantization_loss\n```\n\nThrough this process, the RQ-VAE produces a semantic ID as a sequence of tokens, one from each quantization level. Because similar items share common prefixes, language models can better understand product relationships, which is also useful for tree-based retrieval.\n\n```python\n# How to hierarchically encode embeddings to semantic IDs\ndef encode_to_semantic_ids(self, x: Tensor) -> Tensor:\n\n    with torch.no_grad():\n        residual = self.encode(x)\n        indices_list = []\n\n        for vq_layer in self.vq_layers:\n            vq_output = vq_layer(residual)\n            indices_list.append(vq_output.indices)\n            residual = residual - vq_output.quantized\n        \n    return torch.stack(indices_list, dim=-1)\n```\n\nNonetheless, one practical challenge is that this doesn’t guarantee a unique ID for every item. In my experiments with a three-level codebook, with each level having 256 codes, we saw collisions on ~10% of the 66k products. To solve this, I added a fourth level where I appended a sequentially increasing token to each ID to ensure every product is uniquely identifiable.\n\n### SASRec, Qwen3-Embedding-0.6B, and Qwen3-8b\n\nIn addition to the RQ-VAE, we use three other models. First, we’ll train a **SASRec** on semantic IDs to validate their quality and compare it to an item ID SASRec baseline. Then, we use the **Qwen3-Embedding-0.6B model** to encode product metadata to embeddings. Finally, we finetune the **Qwen3-8B model** to understand and recommend items via semantic IDs.\n\n[SASRec (Kang & McAuley, 2018)](https://arxiv.org/abs/1808.09781) is a sequential recommender inspired by the [Transformer](https://arxiv.org/abs/1706.03762) architecture. It encodes a user’s interaction history and uses a self-attention mechanism to weigh the most relevant past items in order to predict the next one. This allows the model to learn long-term dependencies in user behavior and thus outperform older recurrent models like RNNs and GRUs while being more efficient due to its parallelizable nature.\n\n[Qwen3-Embedding-0.6B (Zhang et al., 2025)](https://arxiv.org/abs/2506.05176) is part of a series of embedding models available in 0.6B, 4B, and 8B sizes. They are trained via a multi-stage process that includes pre-training on synthetic data, followed by supervised finetuning and model merging for robustness. The 8B model achieves SOTA performance on the MTEB Multilingual benchmark.\n\n[Qwen3-8B (Yang et al., 2025)](https://arxiv.org/abs/2505.09388) is a dense language model in the Qwen3 family. Despite being one of the smaller models, its post-training is optimized through strong-to-weak distillation on Qwen3-235B-A22B and Qwen3-32B. This makes the Qwen3-8B relatively capable for its size, surpassing bigger, previous-generation models like Qwen2.5-14B on more than half of the evaluated benchmarks, especially STEM and coding. Like other models in the Qwen3 series, the Qwen3-8B has dual thinking and non-thinking modes.\n\n## Cleaning data and creating user sequences\n\nFirst, we prepare the item metadata to ensure high-quality inputs for the semantic ID model. We start by excluding items that have titles with less than 20 characters or descriptions with less than 100 characters. This reduced the item count by half, from 137k to 66k unique items.\n\nNext, we clean item descriptions with Gemini 2.5 Flash Lite ([Comanici et al., 2025](https://arxiv.org/abs/2507.06261)) by fixing truncated sentences, removing HTML, and reducing verbosity. This halved the average description length from 1,038 to 538 characters. Similarly, we remove promotional text and standardize formatting for titles, turning verbose listings like “NEW! LIMITED! Sega Saturn RGB SCART LEAD CABLE…” into a clean “Sega Saturn RGB SCART Cable”.\n\nThen, we augmented the data by extracting structured metadata such as product type (Game, Hardware, Accessory), platform (PS4, Xbox, Wii), genre (Roguelike, Soulslike, Metroidvania), hardware type, brand, multiplayer modes, etc. This process had 98% coverage for platform information, 78% for brand identification, and 51% for genre classification.\n\nFinally, to build the user sequences, we deduplicated on users and built interaction histories, resulting in 91.5k sequences. From these sequences, we excluded items that had no metadata, and then filtered out sequences that had less than three items. We also truncated sequences to a maximum length of 100 items (only 28 sequences were truncated). This gave us a dataset of 78.6k sequences that have a median length of 5 items and a mean length of 6.5 items.\n\n## Training an RQ-VAE to output Semantic IDs\n\nTo embed the items, we use the Qwen3-Embedding-0.6B model. It supports customization of the input instruction for various tasks, and we add the prefix of “Given a product description, generate a semantic embedding that captures its key features and characteristics”. From this, we get 1024-dim embeddings via last token pooling and L2-normalize them before saving.\n\nThe RQ-VAE consists of an encoder, three quantization levels with 256 codes each, and a symmetric decoder. For training stability, we use the [rotation trick (Fifty et al., 2025)](https://arxiv.org/abs/2410.06424) as a replacement for the Straight-Through Estimator (to calculate the gradient for $L\\_\\\\text{recon}$). Other optimizations included initializing codebooks with k-means clustering, resetting unused codes, and using a large batch size. I also tried a few techniques that didn’t help, such as updating the codebook with EMA and stopping gradients to the decoder.\n\nThe trained RQ-VAE achieved 89% unique semantic IDs across 66k products on the three quantization levels. To resolve the remaining collisions, I appended a fourth token that assigns a unique, sequential ID (0, 1, 2, …) to any products that share the same first three codes. This ensures every product has a unique 4-part semantic ID.\n\nI ran a few dozen experiments to understand more about RQ-VAEs and their output semantic IDs, and to find an optimal configuration for the model. Here are some key findings.\n\nFirst, I experimented with the commitment weight $\\\\beta$ that balances reconstruction accuracy and codebook commitment. I tested values of 0.25 (yellow), 0.5 (orange), and 1.0 (red), and found that a higher $\\\\beta$ of 1.0 led to the most unique IDs but also had the highest validation loss. And while a lower $\\\\beta$ of 0.25 led to slightly more unique IDs, $\\\\beta$ of 0.5 had the lowest validation loss. Thus, on this dataset, I trained subsequent RQ-VAEs with $\\\\beta$ = 0.5. (Note: This differs from the Semantic ID papers which used $\\\\beta$ = 0.25.)\n\n![Image](https://eugeneyan.com/assets/rqvae-curves.webp \"Image\")\n\nCurves with beta = 0.25 (yellow), 0.5 (orange), and 1.0 (red)\n\nAside: A quick primer on what each of these metrics mean:\n\n- **loss/reconstruction**: Measures how well the RQ-VAE can reconstruct the original item embeddings after compressing it into semantic IDs and then decompressing it.\n- **loss/vq**: Combined codebook and commitment loss across all levels. Ensures encoder outputs stay closs to the codebook vectors and codebook vectors adapt to encoder outputs. Essential for compressing embeddings into meaningful semantic IDs.\n- **loss/total**: Sum of reconstruction loss and VQ loss to monitor overall progress.\n- **loss/validation**: Total loss on held-out validation set to monitor generalization.\n- **metrics/avg\\_residual\\_norm**: The amount of “leftover” residual after all quatization levels. Lower residual = codebooks are better at capturing the input embedding.\n- **metrics/unique\\_id\\_proportion**: % of items with unique IDs in a batch. Checks for codebook collapse. Higher = better ability to distingush between items.\n\nI also experimented with a shallower encoder and the impact of metadata cleaning. The shallower encoder (green) performed worse, increasing validation loss and reducing the number of unique IDs. However, investing in data cleaning paid off (blue). It led to a model with the lowest reconstruction and validation losses while having the highest proportion of unique IDs. I used the RQ-VAE from this run.\n\n![Image](https://eugeneyan.com/assets/rqvae-curves2.webp \"Image\")\n\nCurves with beta = 0.5 (orange), shallower encoder (green), and clean data (blue)\n\nAnother way to evaluate RQ-VAEs is to inspect the codebook utilization. Relatively uniform usage across all codes suggests the model is using its full expressive capacity. The final RQ-VAE demonstrates this well; across all three quantization levels, usage is spread evenly with low variance, as shown in the histogram below.\n\n![Image](https://eugeneyan.com/assets/codebook-usage.webp \"Image\")\n\nExample of an RQVAE with well distributed codebook usage\n\nIn contrast, a poorly converged RQ-VAE will have sparse and highly concentrated code usage. The histogram below shows this failure mode, where a few codes are overused, and the majority of the codebook is ignored.\n\n![Image](https://eugeneyan.com/assets/codebook-usage2.webp \"Image\")\n\nExample of an RQVAE with poorly distributed codebook usage\n\nWith the trained RQ-VAE, we encode all item embeddings into their semantic ID format, such as `<|sid_start|><|sid_191|><|sid_260|><|sid_716|><|sid_768|><|sid_end|>`. Then, we transformed the 78.6k user purchase sequences from regular IDs to semantic ID sequences. These sequences are the training data used to both validate the quality of the IDs with a SASRec baseline and to finetune the Qwen3-8B model.\n\n## Training a SASRec on regular item IDs vs. semantic IDs\n\nTo validate that our semantic IDs capture meaningful product relationships, we train two variants of the SASRec, a baseline trained on regular item IDs and a variant trained on semantic IDs, and then compare their performance.\n\nThe **baseline SASRec** follows the standard architecture. It treats each product as a distinct, atomic unit, learning an embedding for it from scratch. This is based purely on behavioral patterns. The model uses 2 causal self-attention blocks, a 64-dimensional hidden state, and is trained on the discriminative task of distinguishing the next item in a sequence from randomly sampled negative items using a binary cross-entropy (BCE) loss.\n\n```python\n# Baseline SASRec predict function\ndef predict(self, input_ids: torch.Tensor, candidate_ids: torch.Tensor) -> torch.Tensor:\n    \"\"\"Predict scores for candidate items.\n\n    Args:\n        input_ids: Item sequences [batch_size, seq_length]\n        candidate_ids: Candidate items to score [batch_size, num_candidates]\n\n    Returns:\n        Scores for each candidate [batch_size, num_candidates]\n    \"\"\"\n    # Get sequence representations\n    hidden_states = self.forward(input_ids)  # [B, T, H]\n\n    # Use only the last hidden state for prediction\n    final_hidden = hidden_states[:, -1, :]  # [B, H]\n\n    # Get candidate embeddings\n    candidate_embs = self.item_emb(candidate_ids)  # [B, C, H]\n\n    # Compute scores via dot product\n    scores = torch.bmm(candidate_embs, final_hidden.unsqueeze(-1)).squeeze(-1)  # [B, C]\n\n    return scores\n```\n\nIn contrast, the **Semantic ID SASRec** reframes recommendation as a conditional generative task. Instead of scoring items, its objective is to generate the next item’s 4-part semantic ID, token-by-token. This requires a larger architecture with 4 transformer blocks and 384-dim hidden states. Unlike the [TIGER paper](https://arxiv.org/abs/2305.05065) which uses a T5 encoder-decoder, this SASRec variant is decoder-only, making it a more direct and equitable comparison to the baseline SASRec. Because we use semantic IDs, instead having an embedding for each of the 66k items, we have a total of 1,024 token-level embeddings, with 256 tokens per level in the semantic ID.\n\n```python\n# Semantic ID SASRec predict function\ndef predict_next_item(self, input_ids: torch.Tensor, teacher_forcing: bool = True, \n    target_tokens: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:\n    \"\"\"Predict the next item's semantic ID tokens sequentially.\n\n    Args:\n        input_ids: Token sequences [batch_size, seq_length * num_levels]\n        teacher_forcing: Use ground truth for conditioning during training\n        target_tokens: Ground truth tokens for next item [batch_size, num_levels]\n\n    Returns:\n        Dictionary with logits for each level\n    \"\"\"\n    hidden_states = self.forward(input_ids)  # [B, T*L, H]\n\n    # Get representation at the last position as context for all previous items\n    last_hidden = hidden_states[:, -1, :]  # [B, H]\n\n    predictions = {}\n\n    # Sequential generation: Predict each level conditioned on previous\n    for level in range(self.num_levels):\n        if level == 0:\n            # Level 0: predict directly from sequence representation\n            context = last_hidden\n        else:\n            # Levels 1-3: condition on previously predicted/true tokens\n            if teacher_forcing and target_tokens is not None:\n                # Use ground truth previous levels during training\n                prev_tokens = target_tokens[:, :level]  # [B, level]\n            else:\n                # Use predicted tokens during inference\n                prev_tokens = self._sample_from_predictions(predictions, level)\n\n            prev_embeds = self.token_emb(prev_tokens)  # [B, level, input_dim]\n            prev_embeds_projected = self.input_projection(prev_embeds)  # [B, level, H]\n            prev_context = prev_embeds_projected.mean(dim=1)  # [B, H]\n\n            # Combine with sequence context\n            combined = torch.cat([last_hidden, prev_context], dim=-1)  # [B, 2*H]\n            context = self.context_combiners[level - 1](combined)  # [B, H]\n\n        # Predict current level\n        logits = self.level_heads[level](context)  # [B, codebook_size]\n        predictions[f\"logits_l{level}\"] = logits\n\n    return predictions\n```\n\nThis generative approach changes how we train and evaluate. The loss function is no longer a simple BCE loss but a sum of cross-entropy losses across each level of the semantic ID, forcing the model to predict the entire sequence correctly. Evaluation is also more complex, where instead of a dot product, an item’s score is its joint log-probability, calculated by summing the log-probs of generating each token. To improve training stability, we apply teacher forcing, where the ground-truth token from a prior level helps guide the prediction for the next level.\n\nTo evaluate both models, we use a validation set where we added 500 negative samples for each positive next item. While the baseline SASRec outperformed the semantic ID variant, the semantic model’s performance is respectable given the difficult generative task of predicting four correct tokens. Furthermore, the semantic ID variant has the **ability to handle cold-start items** by leveraging shared token prefixes from similar products, a capability the baseline lacks. This reveals the core trade-off, where gaining this ability to generalize requires 4x more predictions per item and requires higher training and inference compute.\n\n| Model                  | Hit@10 | NDCG@10 | MRR    | Mean Rank | Median Rank |\n| ---------------------- | ------ | ------- | ------ | --------- | ----------- |\n| **Baseline SASRec**    | 0.2812 | 0.1535  | 0.1300 | 138.9     | 41.0        |\n| **Semantic ID SASRec** | 0.2020 | 0.1138  | 0.1007 | 179.7     | 79.0        |\n\n## Fine-tuning Qwen3-8B to Recommend Semantic IDs\n\nNext, we teach a language model to converse in semantic IDs. For this, we finetune **Qwen3-8B** to become “bilingual”, fluent in both natural language and semantic IDs.\n\nFirst, we build a training dataset of 4.2 million conversational examples to teach the model about semantic IDs and recommendations. The data covers several task types, including mapping semantic IDs to their corresponding text descriptions (and vice-versa), predicting the next item in a user’s sequence, understanding relationships between item categories, and multi-hop reasoning. Each of these examples are formatted as conversations with a system prompt, a user instruction, and an assistant’s response.\n\nThen, we finetune the model in two phases. The first phase focuses on vocabulary extension, where we add 1,027 new tokens for the semantic IDs (i.e., `<|sid_start|>`, `<|sid_end|>`, `<|sid_0|>` to `<|sid_1023|>`, and `<|rec|>`) to the Qwen3-8B tokenizer and resize the model’s embedding matrix for them. During this phase, we freeze all model parameters except the input and output embedding layers, training 1.23B parameters (15.3% of the total) for 1,000 steps with a relatively high learning rate.\n\nWith the semantic ID token embeddings initialized, we proceed to the next phase of full finetuning on all 8.3B parameters. For this, we used a lower learning rate, a 3% warmup schedule, and the 8-bit AdamW optimizer for memory efficiency, training for three epochs on the full dataset. To monitor progress, we used several custom callbacks that tracked training metrics like gradient norms, measured hierarchical accuracy on recommendation tasks, and sampled model outputs on a fixed set of conversational inputs.\n\n## Chatting with the LLM-recommender hybrid\n\nThe finetuned model can then be used to recommend items, and is conversant in semantic IDs and natural language. Because semantic IDs are a native part of its vocabulary, the model generates recommendations by simply predicting the next ID in a sequence, token-by-token. For interactive sessions, a chat interface translates the model’s raw output (that contains semantic IDs) back into product titles, creating a human-readable conversation.\n\n> **User:** “Halo 3 Limited Edition -Xbox 360”, “Lost Planet: Extreme Condition - Xbox 360”, “Lost Planet: Extreme Condition Collector’s Edition -Xbox 360” `<|rec|>`\n>\n> > *Model takes the user’s interaction history (a sequence of Xbox 360 sci-fi shooters) and performs next-item prediction.*\n> >\n> > ```text\n> > <|sid_start|><|sid_64|><|sid_351|><|sid_733|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Mass Effect - Xbox 360”\n\nBasic tests show the model can provide standard next-item recommendations based on a user’s history and refine recs based on natural language like platform constraints (*“Xbox games similar to…“*) or genre (*“I like animal and cute games.”*).\n\n> **User:** Xbox games similar to “The Legend of Zelda: Ocarina of Time 3D”: `<|rec|>`\n>\n> > *Model takes a seed item (“The Legend of Zelda”) and a natural language constraint (“Xbox”) to steer recommendations to a different platform.*\n> >\n> > ```text\n> > <|sid_start|><|sid_64|><|sid_271|><|sid_576|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_64|><|sid_435|><|sid_615|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_167|><|sid_271|><|sid_578|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Fallout: New Vegas - Xbox 360 Ultimate Edition”, “Halo 4 - Xbox 360 (Standard Game)”, “Halo Reach - Legendary Edition”\n\nThe model can also explain its recommendations, describe the relationship between two items, and even suggest creative names and descriptions for product bundles.\n\n> **User:** > I just finished “Dragon Quest Heroes II Explorer’s Edition - PlayStation 4”. Suggest another `<|rec|>` and explain why:\n>\n> > *Model performs a hybrid task where it generates a relevant semantic ID and then provides an explanation for the recommendation.*\n> >\n> > ```text\n> > <|sid_start|><|sid_139|><|sid_289|><|sid_534|><|sid_768|><|sid_end|>\n> > \n> > If you liked Dragon Quest Heroes II, you might like Nights of Azure \n> > because both are action RPGs for the PlayStation 4 with a focus on \n> > combat and character progression. Both games offer a narrative-\n> > driven experience with a strong emphasis on combat mechanics, \n> > suggesting a shared player interest in this genre.\n> > ```\n>\n> **Assistant:** “Nights of Azure - PlayStation 4”\n>\n> If you liked Dragon Quest Heroes II, you might like Nights of Azure because both are action RPGs for the PlayStation 4 with a focus on combat and character progression. Both games offer a narrative-driven experience with a strong emphasis on combat mechanics, suggesting a shared player interest in this genre.\n\nFinally, while it may not match the raw accuracy of a specialized recommender, this unified interface allows users to discover products from our catalog through multi-turn dialogue, leading to the convergence of search, recsys, and chat.\n\nAnd this is all done via the model itself, without intent classifiers, routers, retrieval, etc.\n\n> **\\[TURN 1\\] User:** I’m looking for games similar to Mario Kart. `<|rec|>`\n>\n> > *Model generates semantic IDs for three kart-style racing games.*:\n> >\n> > ```text\n> > <|sid_start|><|sid_131|><|sid_503|><|sid_738|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_131|><|sid_260|><|sid_577|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_145|><|sid_416|><|sid_671|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Need for Speed Most Wanted - Sony PSP”, “Need for Speed Hot Pursuit 2 - PlayStation 2”, “Burnout Legends - Nintendo DS”\n>\n> **\\[TURN 2\\] User:** How about something similar but for Xbox? `<|rec|>`\n>\n> > *Model uses the conversation history and the new “Xbox” constraint to generate semantic IDs for relevant Xbox arcade-racers.*\n> >\n> > ```text\n> > <|sid_start|><|sid_183|><|sid_461|><|sid_517|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_183|><|sid_461|><|sid_562|><|sid_768|><|sid_end|>, \n> > <|sid_start|><|sid_183|><|sid_461|><|sid_616|><|sid_768|><|sid_end|>\n> > ```\n>\n> **Assistant:** “Need for Speed Carbon - Xbox 360”, “Project Gotham Racing 3 - Xbox 360”, “Forza Motorsport 4 - Xbox 360”\n>\n> **\\[TURN 3\\] User:** Suggest a name and description for the bundle.\n>\n> > *Model understands the context has shifted from recommendation to a creative text generation task.*\n>\n> **Assistant:**: Xbox Racing Legends: Speed & Style Pack\n\nOkay, that’s it! I hope you found this as exciting as I do! If I had more compute, I’d experiment with sentence piece tokenization on the semantic IDs, which [Deepmind and Youtube have found helpful](https://arxiv.org/abs/2306.08121), and further explore the impact of semantic IDs on cold-start. I’d also train RQ-VAEs and language models on datasets with millions of products and a greater variety of tasks and conversations to test how this approach scales and generalizes.\n\nHave you trained models or built UXes with semantic IDs? I’d love to chat, please [reach out](https://x.com/eugeneyan)!\n\nThanks to [Devansh](https://x.com/devanshtandon_) and [Shrikar](https://x.com/shrikar84) for the discussions, and [Luke](https://x.com/LukePiette) and [Runpod](https://www.runpod.io/?ref=4uddqig9) for the compute to run these experiments. (Please use my [affiliate code](https://runpod.io?ref=4uddqig9) so I get more credit for experiments!) Thanks to [Vibhu](https://x.com/vibhuuuus), [Shawn](https://x.com/swyx), [Cheng](https://x.com/_chenglou), and everyone who had to tolerate my yapping on this.\n\n## References\n\nHou, Yupeng, Jiacheng Li, Zhankui He, An Yan, Xiusi Chen, and Julian McAuley. “Bridging Language and Items for Retrieval and Recommendation.” arXiv:2403.03952. Preprint, arXiv, March 6, 2024. https://doi.org/10.48550/arXiv.2403.03952.\n\nAmazon. “Amazon KDD Cup ‘23 - Multilingual Recommendation Challenge Dataset.” AIcrowd, 2023. https://www.aicrowd.com/challenges/amazon-kdd-cup-23-multilingual-recommendation-challenge.\n\nRajput, Shashank, Nikhil Mehta, Anima Singh, et al. “Recommender Systems with Generative Retrieval.” arXiv:2305.05065. Preprint, arXiv, November 3, 2023. https://doi.org/10.48550/arXiv.2305.05065.\n\nSingh, Anima, Trung Vu, Nikhil Mehta, et al. “Better Generalization with Semantic IDs: A Case Study in Ranking for Recommendations.” arXiv:2306.08121. Preprint, arXiv, May 30, 2024. https://doi.org/10.48550/arXiv.2306.08121.\n\nZeghidour, Neil, Alejandro Luebs, Ahmed Omran, Jan Skoglund, and Marco Tagliasacchi. “SoundStream: An End-to-End Neural Audio Codec.” arXiv:2107.03312. Preprint, arXiv, July 7, 2021. https://doi.org/10.48550/arXiv.2107.03312.\n\nLee, Doyup, Chiheon Kim, Saehoon Kim, Minsu Cho, and Wook-Shin Han. “Autoregressive Image Generation Using Residual Quantization.” arXiv:2203.01941. Preprint, arXiv, March 9, 2022. https://doi.org/10.48550/arXiv.2203.01941.\n\nKang, Wang-Cheng, and Julian McAuley. “Self-Attentive Sequential Recommendation.” arXiv:1808.09781. Preprint, arXiv, August 20, 2018. https://doi.org/10.48550/arXiv.1808.09781.\n\nVaswani, Ashish, Noam Shazeer, Niki Parmar, et al. “Attention Is All You Need.” arXiv:1706.03762. Preprint, arXiv, August 2, 2023. https://doi.org/10.48550/arXiv.1706.03762.\n\nZhang, Yanzhao, Mingxin Li, Dingkun Long, et al. “Qwen3 Embedding: Advancing Text Embedding and Reranking Through Foundation Models.” arXiv:2506.05176. Preprint, arXiv, June 11, 2025. https://doi.org/10.48550/arXiv.2506.05176.\n\nYang, An, Anfeng Li, Baosong Yang, et al. “Qwen3 Technical Report.” arXiv:2505.09388. Preprint, arXiv, May 14, 2025. https://doi.org/10.48550/arXiv.2505.09388.\n\nComanici, Gheorghe, Eric Bieber, Mike Schaekermann, et al. “Gemini 2.5: Pushing the Frontier with Advanced Reasoning, Multimodality, Long Context, and Next Generation Agentic Capabilities.” arXiv:2507.06261. Preprint, arXiv, July 22, 2025. https://doi.org/10.48550/arXiv.2507.06261.\n\nIf you found this useful, please cite this write-up as:\n\n> Yan, Ziyou. (Sep 2025). Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs. eugeneyan.com. https://eugeneyan.com/writing/semantic-ids/.\n\nor\n\n```\n@article{yan2025semantic-ids,\n  title   = {Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs},\n  author  = {Yan, Ziyou},\n  journal = {eugeneyan.com},\n  year    = {2025},\n  month   = {Sep},\n  url     = {https://eugeneyan.com/writing/semantic-ids/}\n}\n```\n\nShare on:\n\n![](https://eugeneyan.com/assets/icon-twitter.svg)\n\n![](https://eugeneyan.com/assets/icon-linkedin.svg)\n\n![](https://eugeneyan.com/assets/bluesky.svg)\n\n![](https://eugeneyan.com/assets/icon-facebook.svg)\n\n![](https://eugeneyan.com/assets/icon-mail.svg)\n\nJoin **11,800+** readers getting updates on machine learning, RecSys, LLMs, and engineering.\n",
  "dateCreated": "2026-09-27T19:29:15.293681927+00:00",
  "date_created": "2025-09-14T00:00:00+00:00",
  "date_modified": "2025-09-14T00:00:00+00:00",
  "date_published": "2025-09-14T00:00:00+00:00",
  "description": "An LLM that can converse in English & item IDs, and make recommendations w/o retrieval or tools.",
  "external_url": "https://eugeneyan.com//writing/semantic-ids/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/eugeneyan-com/2025/09/2025-09-14-training-an-llm-recsys-hybrid-for-steerable-recs-with.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/eugeneyan-com/2025/09/2025-09-14-training-an-llm-recsys-hybrid-for-steerable-recs-with.md"
  },
  "id": "urn:aggr:item:a13d1e300d30cb65648a26eb46fad27aa8f9c0d5",
  "isBasedOn": {
    "@id": "https://eugeneyan.com//writing/semantic-ids/"
  },
  "mainEntity": {
    "@id": "https://eugeneyan.com//writing/semantic-ids/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/eugeneyan-com/2025-09-14-training-an-llm-recsys-hybrid-for-steerable-recs-with/#webpage"
    },
    "dateModified": "2025-09-14T00:00:00+00:00",
    "datePublished": "2025-09-14T00:00:00+00:00",
    "headline": "Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs",
    "timeRequired": "PT21M",
    "url": "https://eugeneyan.com//writing/semantic-ids/",
    "wordCount": 4722
  },
  "name": "Archived snapshot: Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs",
  "preview": null,
  "reading_minutes": 21,
  "replicated_at": null,
  "source": "eugeneyan-com",
  "source_name": "Eugene Yan",
  "summary": "An LLM that can converse in English & item IDs, and make recommendations w/o retrieval or tools.",
  "tags": [
    "learning",
    "llm",
    "recsys",
    "🛠",
    "🩷"
  ],
  "temporalCoverage": "2026-09-27T19:29:15.293681927+00:00",
  "title": "Training an LLM-RecSys Hybrid for Steerable Recs with Semantic IDs",
  "url": "https://aggr.aymericbeaumet.com/items/eugeneyan-com/2025-09-14-training-an-llm-recsys-hybrid-for-steerable-recs-with/",
  "word_count": 4722
}