1. 从零看懂生成式推荐这个竞赛到底在比什么第一次看到“生成式推荐”这个词很多人会下意识觉得又是拿大模型套个壳。我一开始也这么想直到把竞赛的任务描述反复读了三遍才发现事情没那么简单。生成式推荐的核心思路是把推荐系统里“召回—排序”这套传统漏斗重新理解成一个“序列到序列”的生成问题给定用户过去的行为序列直接生成他接下来最可能交互的物品序列。这跟过去我们熟悉的“给每个候选物品打分再排序”是两种完全不同的范式。这个竞赛面向的是有一定推荐系统基础、同时对大语言模型有实操经验的开发者。如果你只会调API、没做过召回排序或者只懂传统协同过滤、没碰过Transformer都会有点吃力。但它也不是高不可攀——竞赛方通常会给出baseline和数据集真正拉开差距的是你对“如何把推荐问题翻译成语言模型能理解的形式”这件事的理解深度。我先说清楚它解决了什么问题。传统推荐有两个老大难一是候选集规模一大排序阶段的计算成本就爆炸二是用户兴趣漂移快的时候基于ID embedding的模型很难快速泛化到新物品上。生成式推荐把物品用语义ID或者文本描述表示模型学的是“行为序列到目标物品”的映射规律天然具备更强的泛化能力也能把冷启动问题转化成“语义相似度”问题。这就是为什么这两年这个方向这么热。适合谁来参考这份笔记我的判断是三类人第一类是想入门生成式推荐但不知道从哪下手的学生或转行者第二类是做传统推荐、想看看新范式怎么落地的工程师第三类是要打这个竞赛、需要一份能直接抄作业的实操路线图的人。接下来我会把整个竞赛涉及的核心设计、数据构造、模型选型、训练细节和踩坑经验全部拆开讲尽量做到你看完就能自己跑起来一版。2. 竞赛任务整体设计与方案选型思路2.1 任务形式拆解为什么是“生成”而不是“排序”要理解这个竞赛先得接受一个前提推荐本质上可以看成一个条件生成问题。给定用户的历史交互序列 $S [i_1, i_2, ..., i_n]$我们要建模的是 $P(i_{n1} | S)$。传统做法是把这个概率拆成“先召回几百个候选再对候选打分排序”而生成式做法是直接让模型输出 $i_{n1}$ 的表示。这里的关键在于物品怎么表示。如果还用物品ID那词表可能有几百万甚至上千万softmax层直接爆炸而且新物品没有ID就没法处理。所以竞赛里几乎一定会要求你把物品转成语义ID或者文本token序列。语义ID的常见做法是对物品的content embedding做层次聚类得到类似“12-45-78”这样的多级编码每一级都是一个小的码本。这样词表规模就可控了同时语义相近的物品共享前缀模型更容易学到泛化规律。我在实际拆解任务时发现竞赛的评估指标通常不会是简单的准确率而是RecallK、NDCGK这类排序指标。这意味着你生成出来的不能只是一个物品而是一个有序列表。所以模型结构上要么是自回归地一个一个生成要么是一次性生成多个token再解码成物品列表。这两种路线的取舍后面会细讲。2.2 方案选型T5、GPT还是Encoder-Decoder选型这件事我踩过坑。一开始我想当然地用了纯Decoder的GPT结构结果发现训练效率很低收敛慢。后来换成Encoder-Decoder架构类似T5效果明显好转。原因在于用户行为序列作为输入其实更像是一个“理解任务”Encoder可以充分双向建模序列内部关系而输出端是生成目标物品Decoder自回归生成正好合适。这种分工比纯Decoder把输入输出混在一起要清晰得多。具体到竞赛场景我建议优先考虑以下几个方向T5-style Encoder-Decoder输入行为序列输出语义ID序列。成熟、稳定、易调试是baseline的首选。LLM指令微调路线把推荐任务写成自然语言指令用LoRA微调一个开源大模型。优点是泛化强、可解释缺点是推理成本高、训练数据构造复杂。混合路线用轻量Encoder提取行为表示再接一个小的自回归Decoder生成语义ID。这是性价比最高的方案也是我在竞赛里最终采用的。选型背后的逻辑其实就一条在有限算力下让模型结构匹配任务结构。推荐任务的输入是“理解”输出是“生成”那就别硬用一个纯生成模型去干两件事。2.3 数据构造把行为日志变成训练样本数据这块是很多人忽略的重头戏。竞赛给的数据通常是用户-物品交互日志格式大概是(user_id, item_id, timestamp, action_type)。你要做的是把它切成一个个训练样本。我的做法是滑动窗口。假设窗口长度是20预测目标是第21个物品。那么对每个用户我从头开始滑每滑一步就产生一个样本前20个物品作为输入序列第21个作为标签。这里有几个细节要注意窗口长度不是越长越好。太长会引入噪声太短学不到长期兴趣。我实测下来20到50之间比较合适具体要看数据稀疏度。要过滤掉噪声交互。比如停留时间极短的点击、误触这些如果混进去会严重干扰模型。时间戳要用来做序列排序不能打乱。推荐序列的顺序信息非常重要打乱等于丢了一半信息。另外物品的语义ID生成是独立的一步。我一般用物品的标题、类目、属性文本过一个预训练语言模型拿到embedding然后做层次KMeans。层数一般2到3层每层码本大小256或512。这个参数后面会详细算。3. 核心细节解析与实操要点3.1 语义ID生成层次聚类到底怎么调语义ID是整个生成式推荐的地基地基没打好后面模型再强也白搭。我见过有人直接用随机ID结果模型完全学不动因为随机ID之间没有任何语义关系模型只能死记硬背。层次聚类的流程是这样的先对所有物品的embedding做第一层KMeans聚成比如256个簇每个簇给一个编号然后在每个簇内部再做一次KMeans再聚成256个得到第二级编号如果需要还可以做第三级。最终每个物品得到一个类似(a, b, c)的元组这就是它的语义ID。这里有几个实操要点embedding质量决定上限。如果物品文本信息很少可以考虑用协同过滤的隐向量或者多模态特征融合。我试过纯文本embedding和纯协同embedding前者冷启动好后者热物品表达强最好融合。码本大小要权衡。码本太大每个码字训练样本少学不好码本太小区分度不够。256是一个比较稳的默认值。层数方面2层能表达65536个物品3层能表达1600万一般2层够用。要处理冲突。不同物品可能被分到同一个语义ID这时候要么加一层要么在ID后面加一个区分位。冲突率我一般控制在1%以下。提示语义ID一旦生成就固定下来训练和推理必须用同一套映射。建议把映射表存成json方便复现。3.2 输入序列构造行为序列怎么喂给模型输入序列的构造直接决定模型能学到什么。我的做法是把用户行为序列转成token序列每个物品用它的语义ID表示多个语义ID之间用分隔符隔开。比如物品A的语义ID是(12, 45)物品B是(12, 67)那序列可能就是12 45 [SEP] 12 67 [SEP] ...。这里有个细节要不要加行为类型比如点击、购买、收藏。我的经验是加但要用特殊token区分。购买行为的权重应该比点击高可以在序列里用不同的前缀token或者在loss里给购买样本更高权重。另一个细节是序列截断。用户行为可能几百上千条全喂进去不现实。我一般取最近N条N取50左右。如果用户行为少就padding。padding的位置要mask掉不能让模型学到padding。还有一点容易被忽略时间间隔信息。两个行为之间隔了1分钟还是1个月含义完全不同。我试过把时间间隔离散化成几个桶作为额外token拼在物品后面效果有提升但提升幅度不大算力紧张时可以省略。3.3 模型结构细节Encoder和Decoder怎么设计Encoder部分我用的是标准的Transformer Encoder层数4到6层hidden size 256到512注意力头4到8个。输入是行为序列的token embedding加上位置编码。位置编码用可学习的那种比正弦编码在推荐任务上表现稍好。Decoder部分输入是目标语义ID序列训练时用teacher forcing输出是每个位置的下一个token概率。这里要注意语义ID的每一级是独立的token所以解码时要按级生成。比如先生成第一级再生成第二级每一级都是一个分类问题。损失函数用交叉熵但有个技巧对每一级语义ID的loss加权。第一级是粗粒度第二级是细粒度细粒度的loss应该权重更高因为它更接近最终目标。我一般设第一级权重0.3第二级0.7。训练时还有一个关键点负采样。如果只用正样本模型会学成“所有物品都可能”没有区分度。我一般对每个正样本采4到8个负样本负样本从同batch其他用户的正样本里取这叫in-batch negative实现简单效果好。3.4 评估与推理生成出来的东西怎么用推理阶段模型自回归地生成语义ID序列。但这里有个问题生成出来的语义ID可能不对应任何真实物品。这时候需要做一步“解码映射”把语义ID映射回物品。如果映射不到就取语义最接近的物品。评估指标我一般看Recall10和NDCG10。Recall看的是有没有命中NDCG看的是排序质量。竞赛里通常两个都看但权重可能不同要仔细读规则。推理时还有一个技巧beam search。贪心解码容易陷入局部最优beam search保留top-k候选路径效果通常更好。beam size我一般取5到10再大收益递减且速度慢。注意推理时要保证生成的语义ID序列长度和训练时一致否则解码会出错。建议在训练时就固定输出长度。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把环境搭起来。我用的技术栈是PyTorch Transformers Faiss做近邻检索用。Python版本3.9以上CUDA 11.7以上。pip install torch2.0.1 transformers4.35.0 faiss-cpu scikit-learn pandas numpy tqdm如果要用GPU加速KMeans可以装faiss-gpu。数据量大的时候CPU版KMeans会非常慢GPU版能快十几倍。环境搭好后先跑通一个最小demo造1000个物品的假embedding做两层KMeans看看语义ID生成是否正常。这一步能帮你提前发现维度不匹配、码本为空等问题。4.2 数据预处理完整流程数据预处理我分成四步走加载原始日志读入交互数据按用户和时间排序。过滤与清洗去掉交互少于5条的用户去掉被交互少于10次的物品。这一步能显著减少噪声。生成物品embedding用物品文本过预训练模型拿到768维向量再用PCA降到128维。降维能加速聚类且损失不大。生成语义ID对128维向量做两层KMeans每层256簇得到每个物品的语义ID。这里有个参数计算假设有100万物品128维两层KMeans。第一层KMeans复杂度是O(NKd)N100万K256d128大概需要3.3e10次运算。GPU上几秒到几十秒CPU上可能要几分钟。所以数据量大一定要用GPU。预处理完把用户行为序列转成语义ID序列存成训练文件。每个样本格式是(输入序列, 目标序列)输入序列长度固定50目标序列长度固定3对应3级语义ID。4.3 模型训练完整代码框架训练主循环我写成一个标准流程。先定义模型import torch import torch.nn as nn from transformers import T5Config, T5ForConditionalGeneration class GenerativeRecommender(nn.Module): def __init__(self, vocab_size, d_model256, nhead4, num_layers4): super().__init__() config T5Config( vocab_sizevocab_size, d_modeld_model, num_headsnhead, num_layersnum_layers, decoder_start_token_id0, ) self.model T5ForConditionalGeneration(config) def forward(self, input_ids, attention_mask, labels): outputs self.model( input_idsinput_ids, attention_maskattention_mask, labelslabels, ) return outputs.loss训练参数我一般这样设batch size 64学习率3e-4warmup 1000步训练10个epoch。优化器用AdamW权重衰减0.01。学习率调度用cosine最后降到1e-5。训练过程中要监控两个指标训练loss和验证集Recall10。如果训练loss降但验证Recall不升说明过拟合了要加dropout或减模型规模。如果两个都不动说明学习率太小或模型容量不够。我实测下来这个配置在单卡24G显存上能跑训练100万样本大概需要6到8小时。如果显存不够可以把batch size降到32学习率相应降到1.5e-4。4.4 推理与结果生成推理时我把用户最近50条行为转成语义ID序列喂给模型用beam search生成目标语义ID。beam size设5生成长度设3。生成出来的语义ID要映射回物品。我建了一个语义ID到物品列表的倒排索引如果生成的ID对应多个物品就按物品热度排序取top。如果对应不到任何物品就用Faiss在embedding空间找最近邻。最后输出top-10物品列表按生成概率排序。这里要注意beam search出来的概率是序列概率要归一化后再排序。提示推理时可以把batch size调大因为不涉及梯度显存占用小。我一般用256速度能快不少。5. 常见问题与排查技巧实录5.1 训练不收敛怎么办这是最常见的问题。我遇到过三种情况第一种是loss一直不降。原因通常是学习率太大或太小。我一般先试3e-4如果loss震荡就降到1e-4如果loss几乎不动就升到1e-3。另外要检查数据有没有问题比如标签是不是全0或者输入序列是不是全padding。第二种是loss降但验证指标不升。这是过拟合。解决办法是加dropout0.1到0.3、减模型层数、加更多训练数据。我试过把Encoder从6层减到4层验证Recall反而升了。第三种是loss突然变成nan。这是梯度爆炸。解决办法是加梯度裁剪clip值设1.0。如果还不行检查学习率是不是太大或者数据里有没有异常值。5.2 语义ID冲突怎么处理语义ID冲突是指多个物品映射到同一个ID。冲突率高会导致模型无法区分这些物品Recall上不去。我一般这样处理先统计冲突率如果低于1%可以忽略如果1%到5%加第三层聚类如果高于5%说明embedding质量太差要重新做embedding。加第三层聚类会增加词表大小但能显著降低冲突。我实测过两层256码本冲突率3%加第三层后降到0.1%以下。5.3 推理速度太慢怎么优化推理慢通常是因为beam search太宽或模型太大。我一般这样优化beam size从10降到5速度提升近一倍效果损失很小。用半精度推理速度提升30%到50%。把模型导出成ONNX再用ONNX Runtime推理速度能再提升一倍。如果还慢考虑用蒸馏把大模型的知识蒸馏到小模型。5.4 常见问题速查表问题现象可能原因解决办法训练loss不降学习率不当/数据有问题调整学习率/检查数据验证指标不升过拟合加dropout/减模型/加数据loss变nan梯度爆炸梯度裁剪/降学习率语义ID冲突高embedding质量差/层数不够重做embedding/加层推理慢beam太宽/模型太大减beam/半精度/ONNX生成ID映射不到物品语义ID覆盖不全加最近邻兜底5.5 几个独家避坑技巧第一个坑不要用全部数据训练。我一开始用全量数据结果训练极慢且效果一般。后来发现取活跃用户交互大于20条的数据训练效果更好且快得多。因为长尾用户的行为噪声大反而干扰模型。第二个坑验证集要按时间划分。如果用随机划分验证集里会有未来信息泄露指标虚高。我一般取最后一周的数据做验证这样更接近真实场景。第三个坑语义ID的映射表要版本化。我改过一次聚类参数结果旧模型加载新映射表推理全乱。后来我每次生成映射表都带版本号模型和映射表绑定。第四个坑不要忽略padding的影响。padding token如果参与loss计算会严重干扰模型。一定要在loss里mask掉padding位置。我见过有人忘了mask结果模型学会了一直输出padding。6. 进阶优化方向与个人经验6.1 多任务学习把点击、购买、收藏一起学单一目标比如只预测下一个点击学到的表示不够丰富。我试过多任务学习同时预测点击、购买、收藏三个目标共享Encoder各自有独立的Decoder。效果比单任务好不少Recall10提升了大概5个百分点。实现上我在Decoder输出后接三个不同的分类头loss是三个任务的加权和。权重我一般设点击0.5购买0.3收藏0.2。购买权重高是因为购买信号更强但样本少所以不能太高。6.2 对比学习让表示更有区分度对比学习是提升表示质量的有效手段。我在Encoder输出后加了一个对比loss让同一用户的不同行为序列表示更接近不同用户的表示更远离。这样学到的表示更有区分度对冷启动尤其有效。具体做法是对一个batch内的用户取他们的序列表示做InfoNCE loss。温度系数设0.1。这个loss和主loss加权求和权重0.1到0.3。6.3 知识蒸馏把大模型压小如果竞赛有推理时间限制蒸馏是必选项。我用一个大模型比如12层当teacher一个小模型4层当student让student学teacher的输出分布。蒸馏后小模型效果能到teacher的95%以上但速度快3倍。蒸馏的loss是KL散度温度设2到4。温度太高分布太平学不到细节温度太低又太尖锐学不到暗知识。我一般设3。6.4 我个人在实际操作中的体会打这个竞赛我最大的体会是数据质量比模型结构重要得多。我花在数据清洗和语义ID生成上的时间占了总时间的60%以上。模型结构换来换去提升可能就一两个点但数据处理好提升能有十几个点。另一个体会是不要迷信大模型。我试过用7B的大模型做推荐效果确实好但推理成本太高竞赛里根本不实用。反而是小模型加好的数据构造性价比最高。最后分享一个小技巧用验证集做早停。训练时每个epoch跑一次验证如果连续3个epoch验证指标不升就停。这样能省不少时间也能防止过拟合。我一般设patience为3学习率降为原来的0.5。这个方向后续还可以这样扩展一是引入多模态信息把物品图片也编码进去二是做在线学习让模型能实时更新三是探索强化学习直接用推荐效果做奖励信号。这些我还在摸索有新的心得再分享。