拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

连续空间中的下一概念预测:向量回归替代分类的实践与应用

连续空间中的下一概念预测:向量回归替代分类的实践与应用 这个系列我写到第四篇今天集中聊一下 Next Concept Prediction下一概念预测在连续空间里的落地玩法。很多做序列建模的朋友一提到预测下一个第一反应就是接一个 softmax 分类层把候选概念挨个算一遍概率。但在真实场景里这个概念集合经常大到分类层放不下或者概念本身是动态变化的新增项软max根本兜不住。这时候把预测目标放到一个连续向量空间里让模型直接回归坐标就成了一个非常实用的替代思路。这篇文章会用一套完整的推荐系统场景作为贯穿案例用户行为序列是历史的概念序列我们要预测下一个概念在 embedding 空间里的连续坐标然后通过最近邻检索拿到具体候选。当然这套方法不止能用在推荐里图像自监督表征、机器人状态预测、异常检测等方向都能复用。适合正在做序列预测、推荐召回、表征学习的同学也适合那些被分类头卡脖子的从业者。1. 先搞清楚这是个什么问题1.1 从分类预测到连续预测的思维转变常规的序列预测模型比如语言模型里的 next token prediction本质是在一个有限的词表上做分类。模型最后一层输出一个词表大小的 logits 向量然后取 argmax 或者采样。这套逻辑在词表规模几万、几十万的时候还能扛住但一旦扩到商品库里的几个亿 item或者视频平台里的内容池直接软max在计算上就是灾难因为你需要对全量候选计算相似度这在大规模场景下没法落地。连续空间预测的核心思路完全不一样。它不关心候选集合到底有多大它只关心下一个概念的表示长什么样。我们预先通过某种方式把每个概念映射成一个固定维度的稠密向量比如 64 维、128 维把这个向量当作概念的坐标。模型学的是给定前面的历史概念序列预测下一个概念的坐标应该落在哪。预测完之后用这个坐标去向量库里做最近邻搜索就能得到具体的候选列表。这个转变的价值不只是计算量。分类模型学的是哪个类被选中的相对关系它对没见过的概念无能为力而连续空间模型学的是下一个点在空间里的大致位置它天然具备泛化能力。比如电商场景里新上架一批商品只要能把它们映射到同一个向量空间老模型就能直接去预测因为模型根本不关心这些商品是不是在训练时见过。这就是为什么现在越来越多人把预测头从线性分类器换成回归头。1.2 什么场景适合连续空间预测不是所有预测下一个的问题都适合改造成连续空间预测。我总结了几个非常典型的适用场景同时也划出反例免得你选错方向白费功夫。场景为什么适合连续空间预测关键难点推荐系统候选召回候选池巨大全量 softmax 不现实新 item 持续入库分类头无法处理embedding 空间质量、检索效率和预测准确性自监督对比学习预测目标本身就是表征向量天然是回归问题避免表征坍塌、负样本构造机器人状态轨迹预测下一时刻的状态量是连续数值比如关节角度和速度输出维度高需要合理的损失函数约束异常检测重建误差是基于向量距离来衡量的连续预测顺理成章正常态建模是否足够稳定反例我也说清楚如果你的候选集合是封闭的、规模不大比如几万个标签而且这些概念之间没有天然的相似性度量那么老老实实做分类比强行做向量回归要好。分类模型在每个类上都有独立参数学到的边界更尖锐回归模型试图在空间里建立结构如果概念之间本来就没什么可比较的关系那这个空间结构就会学得很勉强。另一个重要的判断标准是连续空间预测适合概念间相似性有意义的场景。比如在电商推荐里用户买了 iPhone 之后更可能买 AirPods这两个商品的向量距离应该是近的但在错误码分类场景里网络超时和参数缺失就没有哪个离哪个更近的说法这种就不适合。2. 方案设计与模型选型2.1 先把概念变成空间里的点Embedding 预训练在搞连续空间预测之前必须先有一个稳定的向量空间。这个空间的质量基本决定了预测的天花板因为模型再怎么调预测结果最终都要落到这个空间里去检索。我见过不少新手上来就直接随机初始化 embedding然后端到端去训整个模型结果预测方向学得一团糟因为空间本身还没成形的时候模型根本不知道该往哪走。比较稳妥的做法是分两阶段第一阶段单独训练概念嵌入第二阶段再在这个嵌入空间上做序列预测。嵌入训练可以用矩阵分解比如对用户-商品交互矩阵做 SVD也可以用双塔模型把用户侧的序列特征和商品侧的属性特征拉近。也可以直接用预训练好的向量比如自然语言场景用Sentence-BERT商品场景用 image-text 双塔产出。这里有个我反复强调的细节要做空间对齐。如果嵌入是通过不同模型产出的比如老商品用了一版模型新商品用了另一版它们的向量空间可能在方向上就错位了甚至每个维度都没有可比性。这就好比两个团队各画了一张城市地图一张以北为正一张以东为正拿出来直接用必然乱套。所以预训练阶段最好固定单一模型或者至少对向量做一次全局对齐校准比如通过重叠物品做正交变换对齐。2.2 序列编码器的选型与对比拿到概念向量之后下一步是把历史序列编码成一个固定长度的上下文向量。这一步可以选的模型很多简单点的有均值池化、GRU/LSTM复杂点的有 Transformer。我先给一张对比表格再展开说。编码器序列长度适应范围优点缺点均值池化任意长度但长序列信息丢失严重简单、快、不容易过拟合没有时序建模能力丢失顺序GRU/LSTM中短序列 100 步效果不错轻量时序建模自然训练快长序列下记忆衰减并行性差Transformer长序列几十到几百步全局依赖建模强并行训练快数据量要求高小数据容易过拟合以我自己的经验如果序列长度普遍小于 30GRU 往往比 Transformer 更省心参数少、收敛快而且预测效果不差。如果序列能到 50 以上或者存在很久之前的行为对当下有决定性影响的情况那 Transformer 值得上。还有一个折中方案是用 Transformer 的 encoder 去提取特征后面接轻量 GRU 做最终预测这种混合结构在小数据上比纯 Transformer 稳很多。这里要提醒一点连续空间预测的任务本身已经比分类要软了因为回归目标天然有模糊性。所以编码器不要一味追求大模型参数越多越容易把训练集里的历史序列背下来导致预测向量对训练样本过拟合到测试集上全都偏到训练样本的均值附近。2.3 回归头的关键设计输出向量与归一化模型编码完序列之后最后接一个输出层。这个输出层怎么设计直接决定训练是否稳定、检索是否好用。最常见的形式是把输出维度设置为 embedding 维度一致比如 embedding 是 128 维那么回归头就输出 128 维的向量。但这里有个大坑直接输出原始坐标并用 L2 损失训练模型很容易走捷径去预测所有序列的均值。为什么因为回归任务里如果模型不确定下一个点在哪最小化平方误差的最优策略就是输出条件分布的均值而均值往往处于整个空间的高密度中心区域。训练后期你会发现所有预测向量都挤在中心地带区分度大幅下降。我的解决办法有两个方向可以组合使用。第一个方向是让回归头接一个 Normalization 层限制输出向量的模长。因为在很多 embedding 空间里向量做过 L2 归一化分布在一个单位球面上。模型只能在球面上挪动空间被压缩了坍塌现象会好很多。第二个方向是不要直接预测绝对坐标而是预测残差也就是下一个向量相对于当前位置的偏移量再加上当前概念向量作为兜底。这样可以减轻模型从零开始预测坐标的压力模型只需要学怎么走。输出层的具体结构也不复杂常用的是两层 MLP中间接 GELU 或者 SiLU 激活最后输出维度对齐 embedding。层数不用太多两到三层的非线性就已经足够。如果你发现预测向量老是震荡不收敛可以尝试在回归头里加一层 LayerNorm很多时候能解决训练初期 loss 剧烈波动的问题。3. 实操过程从数据到模型3.1 序列数据构造与窗口化整个实操我从数据开始讲。假设我们的场景是电商平台用户的历史行为是一串商品 ID每个商品 ID 都有对应的 128 维向量。我们的目标是用最近 N 次点击或购买的商品序列预测用户下一次点击的商品向量坐标。构造训练样本时滑动窗口是最常用的方式。比如用户的完整行为序列是 [item_3, item_7, item_1, item_9, item_4]窗口长度 N3那么我们可以生成输入序列[item_3, item_7, item_1]目标item_9 的向量输入序列[item_7, item_1, item_9]目标item_4 的向量注意不要把最后一个不完整窗口拿去训练除非你想做的是预测未来。窗口的长度 N 本身是个超参我建议先用 5 和 20 各跑一个实验看哪个效果好。短窗口适合行为模式简单、注意力短的场景长窗口适合需要长期兴趣建模的场景。数据清洗方面有三个点容易被忽略。第一是过滤掉行为太短的用户比如只有 3 个行为的用户窗口滑动后样本太少还会引入噪声。第二是过滤掉出现频率极低的 item这些 item 的 embedding 质量通常不好学出来的向量本身就在乱飘拿它当目标会把模型带偏。第三是考虑是否要区分行为类型如果你有曝光、点击、购买多种行为建议分开建模至少给不同行为分配不同的权重不要让垃圾输入稀释真正的强信号。3.2 模型结构与训练代码先说模型结构。我会用一个 Transformer Encoder 做序列编码再加一个两层 MLP 回归头。代码用 PyTorch 写方便你直接改造。Transformer 的好处是能从全局建模行为序列但如果你想快速验证方案替换成 GRU 也很简单。import torch import torch.nn as nn import torch.nn.functional as F class NextConceptPredictor(nn.Module): def __init__(self, embed_dim128, seq_len20, num_heads4, ff_dim256): super().__init__() # 位置编码保留序列顺序信息 self.pos_embedding nn.Parameter(torch.randn(1, seq_len, embed_dim) * 0.02) # 用 Transformer EncoderLayer 堆叠两层 encoder_layer nn.TransformerEncoderLayer( d_modelembed_dim, nheadnum_heads, dim_feedforwardff_dim, batch_firstTrue ) self.transformer nn.TransformerEncoder(encoder_layer, num_layers2) # 回归头把整个序列压缩成 128 维坐标 self.regressor nn.Sequential( nn.Linear(embed_dim, ff_dim), nn.GELU(), nn.Linear(ff_dim, embed_dim) ) # 输出层归一化开关 self.apply_layer_norm nn.LayerNorm(embed_dim) def forward(self, embeddings): # embeddings: (batch, seq_len, embed_dim) seq_len embeddings.size(1) x embeddings self.pos_embedding[:, :seq_len, :] x self.transformer(x) # 取最后一步的输出作为序列表示 x self.regressor(x[:, -1, :]) # 输出层加 LayerNorm稳定训练 x self.apply_layer_norm(x) return x这里解释几个容易写错的地方。第一是位置编码因为输入已经很模态化了位置编码我直接用了可学习的如果你的序列长度固定为 20 就没问题如果长度动态变化建议改成相对位置编码或者 attention mask。第二是x[:, -1, :]意思是取最后一个 token 的输出。Transformer 不像 RNN 有天然的顺序尾巴取最后一位是约定俗成的做法等价于让前面的 token 通过注意力把信息汇聚到最后一位。损失函数我这里先用余弦相似度的负对数形式来写。很多人上来就用 MSE但推荐场景里我们更关心方向和位置对不对而不是绝对坐标的每一维分毫不差。用余弦相似度可以让模型把精力放在空间方向学习上泛化能力也会好一些。class CosineLoss(nn.Module): def forward(self, pred, target): # pred, target: (batch, embed_dim) # 两者都做归一化确保余弦相似度稳定 pred_norm F.normalize(pred, dim-1) target_norm F.normalize(target, dim-1) cosine_sim (pred_norm * target_norm).sum(dim-1) return (1 - cosine_sim).mean()训练主循环没啥特别的常规的 Adam LearningRateScheduler。一个值得注意的点是 batch size 尽量开大因为连续空间预测的目标本身噪声比较大大 batch 能有效缓解单个样本标签不稳定的问题。另外在训练早期看 loss 曲线时别指望它降得特别快因为一开始模型还没搞懂空间结构loss 会先在一个高位平台震荡一阵这是正常的。3.3 训练参数与调参经验我把我实测下来比较稳的一套参数配置写在这里供你参考。还是那句话超参这东西跟数据关系很大我的配置只能当作起点不要照搬就完事。参数建议值说明优化器AdamW比原生 Adam 的权重衰减更干净初始学习率1e-4 到 3e-4Transformer 用 1e-4 偏稳GRU 可以稍微放大批次大小128 到 512推荐场景数据量够batch 尽量大学习率调度warmup 10% cosine decay防止训练初期振荡梯度裁剪1.0序列模型很容易梯度爆炸dropout0.1 到 0.2Transformer 的默认配置调参过程中我最有感触的是学习率。连续空间预测容易因为目标空间分布广出现梯度方向不一致的现象学习率稍微大一点loss 曲线就直接变成心电图。所以我的习惯是 warmup 做得长一点比如总步数的 10% 到 20% 都拿来热启动让模型先适应一下空间规模再开始正式学习。3.4 预测之后的最近邻检索模型训练完输出的是一个 128 维的向量。这个向量怎么变成真正要展示给用户的商品列表答案是最近邻检索。这一步是整个系统里最容易被人忽略、但出问题概率最高的环节。如果你没有独立的向量检索服务一个小规模实现可以直接用 numpy 暴力算百万级候选还能扛得住。规模更大的场景需要交给专用索引用好 Faiss 或者向量数据库。核心是把所有候选 item 的向量建成索引然后把模型输出的向量作为 query 去查 top-k。这里有个细节query 向量最好和索引向量做相同的归一化处理否则距离计算会有偏差。实际应用中你会遇到一个尴尬的问题模型预测的向量可能在空间里处于两个真实物品的中间位置距离它最近的物品看着反而像四不像。这种情况通常是损失函数健壮性不够我会在下一节集中讲怎么排查。4. 常见问题与排查技巧实录4.1 预测结果偏向中心连续回归的均值化坍塌这是连续空间预测里最常见、也最让人头大的问题。现象很典型你训练完之后拿一批样本去预测发现预测出来的向量都落在向量空间的中心地带彼此之间距离很小。然后去检索的时候每个用户拿到的候选基本都差不多的热门商品个性化完全消失。根本原因我在前面已经提过MSE 或 L2 这类损失函数下模型在目标不确定时最安全的策略是输出条件分布的均值。如果目标分布的方差本来就大那所有预测都会往全局均值收缩产生中心化坍塌。解决手段按优先级排序把 embedding 先做均值中心化并归一化到球面上迫使模型在有限区域内做输出。把 L2 损失换成余弦损失让模型更重视方向而非绝对坐标。降低输出层结构的容量有时候模型复杂度太高反而更容易走捷径。使用高斯负对数似然损失在回归的同时预测一个不确定性理论上可以缓解中心化问题但实现成本也更高。我用余弦损失在多个数据集上都明显改善了中心化问题。注意加了F.normalize之后输出的向量模长恒定为 1L2 距离实际上只在球面角度上有意义中心化坍塌的空间就被压缩了很多。4.2 序列很短预测效果差怎么办有朋友问过我用户行为就三五个窗口都没法填满怎么办。这种情况下再强的模型也没辙因为信息量就是不够。我的处理方式是按用户历史长度分桶给短序列用户走单独的兜底策略不算中间层去硬预测。比如短序列用户直接取最近一个行为向量的若干邻居或者对历史向量做一个带时间衰减的加权平均拿这个加权平均去检索。这本质上是记忆型策略不依赖模型泛化。实测下来短序列用户用加权平均往往比用深度学习模型预测效果更好因为数据太少时模型学不到稳定映射。等到用户行为积累到一定程度再切换成模型预测。这个分桶策略在实践中非常重要。你不需要让一个模型同时应对极端短序列和长序列两种截然不同的样本。强行混合只会让模型在短序列上学到平均化的映射又拉低了长序列用户的个性化上限。4.3 指标上不去先怀疑 embedding 还是模型每次做实验发现 Recall10 死活上不去我的自查顺序基本都是固定的先查 embedding 质量再查训练目标噪声最后才查模型结构。如何快速判断 embedding 有没有问题老办法人工抽样看最近邻。随机取 20 个 item对每个 item 找它的 top-5 最近邻肉眼检查这 5 个是不是语义相关。如果 5 个里有 3 个以上和原 item 八竿子打不着那就别在预测模型上白费力气了先把空间修炼好。判断训练目标噪声可以从损失数值入手。训练集上的 loss 如果降得很低但验证集的 loss 高得离谱那大概率是目标本身存在多义性同一个历史序列后面可能跟着很多种不同的合法商品模型没法从训练数据里找到一个唯一正确答案只能记下训练集里的特定样本。解法是增强数据或者把目标从单一向量改成目标分布采样。4.4 梯度爆炸与训练不稳定Transformer 做序列编码时梯度爆炸是个频繁出现的问题。特别是当你把序列长度拉长之后attention 矩阵的数值范围会变大梯度极其容易失控。我见过太多人loss 还完全没有下降趋势就先遇到了loss nan十有八九是梯度爆了。梯度裁剪是我必开的开关但很多人裁剪阈值设置不对。设太大等于没裁设太小又会导致学不动。我的经验是阈值 1.0 起步如果你是 AdamW可以先设 0.5 试试观察训练 loss 变化再微调。另外注意学习率不要一次给到位warmup 能显著降低训练初期的梯度冲击。还有一种不太显眼的不稳定来源是被把位置编码初始化得太大。位置编码是可学习参数初始随机范围如果设成均匀分布添加之后会直接把 embedding 信号淹没掉。建议初始范围控制在 0.02 左右甚至可以初始化成全零让网络自己去决定怎么调整稳定得多。5. 一点个人体会连续空间预测这件事我在多个项目里试过从推荐系统召回、到用户行为向量预测、再到自监督表征的预测头都验证了一套结论这个方案的价值不在于比分类模型在离线指标上高多少而在于它给了你一个处理大规模、开放候选集合的通用框架。分类模型面对新增概念需要改词表、重新训练连续空间预测只需要把新概念嵌入到已有空间里预测部分完全不用动。当然它也不是银弹。连续空间的回归目标天然存在不确定性你不可能要求模型像分类那样输出一个精确的指针。所以做这类模型的正确姿势是预测分布而不是预测点把各种缓解均值化、中心化的手段设计到模型里并且接受它的召回结果是一种模糊匹配而不是精确命中。如果你正打算把手里的分类预测改成连续空间预测我的建议是别一上来就改动整个模型架构。先冻结现有 embedding单独假设一条回归头用一小批数据快速实验看看预测向量近邻的可解释性如何。这一步跑通了再逐步扩大数据、优化损失函数你会发现这条路虽然绕但能覆盖的场景远比分类宽得多。
返回列表