☰
生成式推荐竞赛实战:从语义ID到T5模型的完整方案
2026/10/18 4:37:26 网站建设 项目流程

1. 从零看懂生成式推荐:这个竞赛到底在比什么

第一次看到“生成式推荐”这个词,很多人会下意识觉得又是拿大模型套个壳。我一开始也这么想,直到把竞赛的任务描述反复读了三遍,才发现事情没那么简单。生成式推荐的核心思路,是把推荐系统里“召回—排序”这套传统漏斗,重新理解成一个“序列到序列”的生成问题:给定用户过去的行为序列,直接生成他接下来最可能交互的物品序列。这跟过去我们熟悉的“给每个候选物品打分再排序”是两种完全不同的范式。

这个竞赛面向的是有一定推荐系统基础、同时对大语言模型有实操经验的开发者。如果你只会调API、没做过召回排序,或者只懂传统协同过滤、没碰过Transformer,都会有点吃力。但它也不是高不可攀——竞赛方通常会给出baseline和数据集,真正拉开差距的是你对“如何把推荐问题翻译成语言模型能理解的形式”这件事的理解深度。

我先说清楚它解决了什么问题。传统推荐有两个老大难:一是候选集规模一大,排序阶段的计算成本就爆炸;二是用户兴趣漂移快的时候,基于ID embedding的模型很难快速泛化到新物品上。生成式推荐把物品用语义ID或者文本描述表示,模型学的是“行为序列到目标物品”的映射规律,天然具备更强的泛化能力,也能把冷启动问题转化成“语义相似度”问题。这就是为什么这两年这个方向这么热。

适合谁来参考这份笔记?我的判断是三类人:第一类是想入门生成式推荐但不知道从哪下手的学生或转行者;第二类是做传统推荐、想看看新范式怎么落地的工程师;第三类是要打这个竞赛、需要一份能直接抄作业的实操路线图的人。接下来我会把整个竞赛涉及的核心设计、数据构造、模型选型、训练细节和踩坑经验全部拆开讲,尽量做到你看完就能自己跑起来一版。

2. 竞赛任务整体设计与方案选型思路

2.1 任务形式拆解:为什么是“生成”而不是“排序”

要理解这个竞赛,先得接受一个前提:推荐本质上可以看成一个条件生成问题。给定用户的历史交互序列 $S = [i_1, i_2, ..., i_n]$,我们要建模的是 $P(i_{n+1} | S)$。传统做法是把这个概率拆成“先召回几百个候选,再对候选打分排序”,而生成式做法是直接让模型输出 $i_{n+1}$ 的表示。

这里的关键在于物品怎么表示。如果还用物品ID,那词表可能有几百万甚至上千万,softmax层直接爆炸,而且新物品没有ID就没法处理。所以竞赛里几乎一定会要求你把物品转成语义ID或者文本token序列。语义ID的常见做法是对物品的content embedding做层次聚类,得到类似“12-45-78”这样的多级编码,每一级都是一个小的码本。这样词表规模就可控了,同时语义相近的物品共享前缀,模型更容易学到泛化规律。

我在实际拆解任务时发现,竞赛的评估指标通常不会是简单的准确率,而是Recall@K、NDCG@K这类排序指标。这意味着你生成出来的不能只是一个物品,而是一个有序列表。所以模型结构上,要么是自回归地一个一个生成,要么是一次性生成多个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映射回物品。如果映射不到,就取语义最接近的物品。

评估指标我一般看Recall@10和NDCG@10。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 torch==2.0.1 transformers==4.35.0 faiss-cpu scikit-learn pandas numpy tqdm

如果要用GPU加速KMeans,可以装faiss-gpu。数据量大的时候,CPU版KMeans会非常慢,GPU版能快十几倍。

环境搭好后,先跑通一个最小demo:造1000个物品的假embedding,做两层KMeans,看看语义ID生成是否正常。这一步能帮你提前发现维度不匹配、码本为空等问题。

4.2 数据预处理完整流程

数据预处理我分成四步走:

  1. 加载原始日志:读入交互数据,按用户和时间排序。
  2. 过滤与清洗:去掉交互少于5条的用户,去掉被交互少于10次的物品。这一步能显著减少噪声。
  3. 生成物品embedding:用物品文本过预训练模型,拿到768维向量,再用PCA降到128维。降维能加速聚类且损失不大。
  4. 生成语义ID:对128维向量做两层KMeans,每层256簇,得到每个物品的语义ID。

这里有个参数计算:假设有100万物品,128维,两层KMeans。第一层KMeans复杂度是O(NKd),N=100万,K=256,d=128,大概需要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_model=256, nhead=4, num_layers=4): super().__init__() config = T5Config( vocab_size=vocab_size, d_model=d_model, num_heads=nhead, num_layers=num_layers, decoder_start_token_id=0, ) self.model = T5ForConditionalGeneration(config) def forward(self, input_ids, attention_mask, labels): outputs = self.model( input_ids=input_ids, attention_mask=attention_mask, labels=labels, ) return outputs.loss

训练参数我一般这样设:batch size 64,学习率3e-4,warmup 1000步,训练10个epoch。优化器用AdamW,权重衰减0.01。学习率调度用cosine,最后降到1e-5。

训练过程中要监控两个指标:训练loss和验证集Recall@10。如果训练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降但验证指标不升。这是过拟合。解决办法是加dropout(0.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。效果比单任务好不少,Recall@10提升了大概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。

这个方向后续还可以这样扩展:一是引入多模态信息,把物品图片也编码进去;二是做在线学习,让模型能实时更新;三是探索强化学习,直接用推荐效果做奖励信号。这些我还在摸索,有新的心得再分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询