1. 项目概述为什么我们需要重新思考推荐系统的“冷启动”如果你做过推荐系统尤其是涉及图片、视频、音频等多模态内容的推荐一定对“冷启动”这个词又爱又恨。爱的是它代表了系统对新用户或新物品的潜力挖掘恨的是它往往是整个系统效果最薄弱、最让人头疼的环节。传统的做法无论是基于内容的特征匹配还是基于协同过滤的隐式推断都绕不开一个核心问题如何在没有历史交互数据或者数据极其稀疏的情况下快速、准确地为用户建立一个有效的“数字画像”这就是用户表示初始化要解决的难题。想象一下一个新用户刚刚打开你的App系统对他一无所知。常规的深度推荐模型比如那些动辄几百万参数的神经网络需要依赖大量的用户-物品交互数据来训练和优化用户嵌入向量。对于这个“白板”用户模型要么给他一个随机向量要么给一个所有用户的平均向量。结果可想而知最初的推荐往往驴唇不对马嘴用户体验大打折扣用户可能因此流失永远失去了积累数据的机会。最近我团队在研究和实践中提出并验证了一种名为SG-URInit的方法。这个名字拆开看SG代表“语义图”Semantic GraphURInit代表“用户表示初始化”User Representation Initialization。它的核心思想非常直接不依赖任何历史交互数据也不需要对下游推荐模型进行任何重新训练仅利用物品本身的多模态内容信息就能为新用户生成一个高质量、个性化的初始表示向量。这个方法最吸引人的地方在于它的“模型无关性”和“免训练”。这意味着无论你后端用的是经典的矩阵分解MF、神经协同过滤NCF还是更复杂的Transformer-based模型SG-URInit 生成的那个初始向量都可以直接“喂”进去立刻提升新用户的首批推荐质量。我们内部在电商、短视频和音乐等多个场景下的A/B测试都表明采用这种方法后新用户的次日留存率和初期互动率有显著提升。2. 核心思路拆解从多模态内容到用户语义图要理解 SG-URInit得先抛开“用户-物品交互”这个传统视角回到一个更本质的问题一个物品是如何吸引一个用户的答案往往藏在物品的内容里。一件衣服的款式和颜色一段视频的封面和标题关键词一首歌的旋律风格和歌手信息——这些多模态特征本身就承载着强烈的语义信号能够吸引具有特定偏好的用户群体。SG-URInit 的核心思路就是构建并利用这种“物品-物品”之间的语义关联来间接推测新用户的可能偏好。整个过程不涉及任何用户行为数据完全基于物品的内容特征库。下面我拆解一下它的三个关键阶段。2.1 阶段一多模态特征提取与融合这是所有工作的基础。假设我们的系统里有数百万个物品商品、视频、文章等每个物品都拥有多种模态的信息。以电商商品为例文本模态商品标题、描述、用户评论摘要。视觉模态商品主图、细节图、场景图。属性模态品牌、类别、价格、材质等结构化标签。我们的第一步是使用预训练好的、领域通用的深度模型将这些原始内容转化为稠密的特征向量即嵌入向量。文本特征可以使用 BERT、Sentence-BERT 或 SimCSE 等模型将标题和描述编码成固定维度的向量。这里的关键是模型是在大规模通用语料上预训练的我们直接拿来用不需要用我们自己的业务数据再训练fine-tune。这保证了方法的“免训练”特性。视觉特征使用在 ImageNet 上预训练的 ResNet、EfficientNet 或 CLIP 的图像编码器提取商品图片的高层语义特征。属性特征对于品牌、类别这类离散特征可以采用嵌入层Embedding Layer进行编码对于价格这类连续特征可以归一化后直接使用或分桶嵌入。实操心得特征对齐的重要性不同模态的特征向量通常位于不同的语义空间维度也不同。直接拼接或简单加权效果可能不好。我们实践下来一个有效的方法是使用一个简单的投影层比如一个全连接网络将不同模态的特征映射到一个统一的、维度相同的公共语义空间。这个投影层的参数是固定的、随机初始化的或者用一个小规模的、无监督的目标如重构损失稍作调整。重点是这个投影层的训练不依赖于任何用户行为数据只依赖于物品内容本身因此依然符合“免训练”的设定。将所有模态的特征在公共空间进行融合就得到了每个物品的统一多模态语义向量。这个向量综合反映了物品在各个维度上的内容信息。2.2 阶段二语义图构建与增强有了所有物品的语义向量我们就可以构建一个全局的物品语义图。在这个图中每个节点代表一个物品节点之间的边权重代表物品之间的语义相似度。计算相似度最直接的方法是余弦相似度。但是对于百万甚至千万量级的物品库计算全连接图是不现实的存储开销也是巨大的。因此我们采用k-最近邻k-NN的方法来构建一个稀疏图对于每个物品只保留与它语义最相似的前k个物品作为邻居并记录相似度作为边权重。注意事项相似度计算的选择与优化效率问题直接两两计算余弦相似度复杂度是 O(N^2)。对于大规模数据必须使用近似最近邻搜索库如 FaissFacebook、AnnoySpotify或 ScaNNGoogle。这些库能在可接受的时间内从海量向量中快速找出每个向量的Top-k最近邻。相似度度量余弦相似度对于方向敏感欧氏距离对于尺度敏感。在我们的多模态融合空间中余弦相似度通常表现更好。但也可以尝试马氏距离等如果特征分布经过精心调整。图的对称性A是B的k近邻B不一定是A的k近邻。为了构建一个更稳定、连通性更好的图通常采用“并集”或“交集”策略来保证对称性或者直接使用互k近邻Mutual k-NN。构建出基础的k-NN图后这个图可能还比较“粗糙”只反映了局部的一阶相似关系。我们可以通过图扩散或图卷积的思想对其进行增强。简单来说就是让节点的信息沿着边在图中传播一步或多步。例如使用图卷积网络GCN的一层传播公式H D^(-1/2) A D^(-1/2) H其中A是图的邻接矩阵由k-NN相似度构成D是度矩阵H是所有节点的特征矩阵即我们的多模态语义向量。经过这样一次传播每个节点的特征就融合了其一阶邻居的信息使得图中语义相近的“群落”内部特征更加平滑、一致。这个过程可以看作是对物品语义的一次全局平滑和增强让后续的推断更鲁棒。2.3 阶段三基于图的用户表示初始化现在终于到了为新用户生成初始表示的时刻。假设新用户u在首次会话中与一组物品I_u {i1, i2, ..., im}发生了交互例如点击、浏览、购买。这些物品就是我们在冷启动阶段唯一的“线索”。SG-URInit 的核心操作如下种子节点定位将用户交互过的物品集合I_u映射到我们预先构建好的语义图上这些物品对应的节点就是“种子节点”。子图提取与聚合以这些种子节点为中心提取它们在图中的邻居可以是一阶或二阶邻居形成一个局部子图。这个子图包含了与用户初始兴趣直接相关和间接相关的物品群落。表示生成对这个局部子图中的所有节点物品的特征向量进行聚合。聚合方式有多种选择均值聚合最简单直接计算子图内所有节点特征的平均值。适合快速实现。加权聚合根据种子节点与邻居节点的相似度边权重进行加权平均。与用户直接交互的物品权重更高。注意力聚合引入一个简单的注意力机制让模型这里是一个轻量级网络自动学习在子图中哪些节点对于表征用户兴趣更重要。这个注意力网络的参数是固定的、通用的不随用户变化且训练不依赖用户行为数据例如可以用图重构任务预训练一个图自编码器然后复用其注意力权重生成器。最终聚合得到的向量就是为新用户u生成的初始表示embedding_u。这个向量天然地落在了由所有物品语义构成的向量空间中并且蕴含了基于内容语义的泛化信息——它不仅反映了用户当前点击的物品还反映了与这些物品语义相似的其他物品所构成的兴趣领域。3. 技术实现细节与实操要点理论讲完了我们来点硬核的看看具体怎么实现。我会结合我们工程实践中的代码片段和配置把关键细节讲透。3.1 物品特征库的构建与维护这是整个系统的基石必须设计成可增量更新、高效检索的。# 伪代码示例物品特征提取与存储管道 import torch import faiss from transformers import AutoModel, AutoTokenizer from PIL import Image import torchvision.models as models import torchvision.transforms as transforms class ItemFeaturePipeline: def __init__(self): # 1. 初始化各模态编码器全部使用预训练模型不fine-tune self.text_model AutoModel.from_pretrained(sentence-transformers/all-MiniLM-L6-v2) self.text_tokenizer AutoTokenizer.from_pretrained(sentence-transformers/all-MiniLM-L6-v2) self.image_model models.resnet50(pretrainedTrue) self.image_model torch.nn.Sequential(*(list(self.image_model.children())[:-1])) # 移除最后一层分类头 self.image_model.eval() self.image_transform transforms.Compose([...]) # 标准预处理 # 2. 初始化投影层将不同模态特征映射到统一空间dim256 self.text_proj torch.nn.Linear(384, 256) # MiniLM输出384维 self.image_proj torch.nn.Linear(2048, 256) # ResNet-50池化层后是2048维 # 属性投影层略... # 3. 初始化特征存储和索引 self.feature_db {} # 内存或Redis缓存item_id - unified_vector self.index faiss.IndexFlatIP(256) # 内积索引用于后续k-NN搜索 def extract_and_store(self, item_id, title, description, image_path, attributes): # 提取文本特征 text_inputs self.text_tokenizer([title description], ...) with torch.no_grad(): text_features self.text_model(**text_inputs).last_hidden_state[:, 0, :] # [CLS] token text_features self.text_proj(text_features).squeeze().numpy() # 提取图像特征 img Image.open(image_path) img_tensor self.image_transform(img).unsqueeze(0) with torch.no_grad(): image_features self.image_model(img_tensor).squeeze() image_features self.image_proj(image_features).squeeze().numpy() # 融合这里采用简单加权平均可根据业务调整 unified_vector 0.6 * text_features 0.4 * image_features # 可选L2归一化使余弦相似度等于内积 unified_vector unified_vector / np.linalg.norm(unified_vector) # 存储 self.feature_db[item_id] unified_vector self.index.add(np.array([unified_vector]).astype(float32))关键配置与调优点投影层训练虽然说是“免训练”但这里的投影层参数可以先用一个无监督目标进行初始化。例如利用对比学习思想让同一物品的不同模态特征经过投影后尽可能接近不同物品的特征尽可能远离。用物品ID自监督即可仍然不需要用户数据。特征融合权重0.6和0.4是经验值。更科学的方法是设计一个轻量的门控网络Gating Network根据物品类型动态调整权重。例如对于服装视觉权重大对于书籍文本权重大。这个门控网络同样可以用物品内容自监督训练。索引选择IndexFlatIP是精确搜索适合物品数在百万级以下。超过百万建议使用IndexIVFFlat或IndexHNSWFlat等近似索引在精度和速度间取得平衡。需要定期用index.train()在代表性数据上训练索引。3.2 语义图的高效构建与更新图不能每次都从头构建需要支持增量更新。# 伪代码示例基于Faiss的增量式k-NN图构建 class SemanticGraphBuilder: def __init__(self, index, k50, similarity_threshold0.5): self.index index # Faiss索引 self.k k self.threshold similarity_threshold self.graph defaultdict(list) # 邻接表item_id - list of (neighbor_id, similarity) def build_or_update_graph(self, new_item_ids): 增量构建图。new_item_ids是新加入的物品ID列表 all_vectors np.array([self.feature_db[iid] for iid in self.feature_db.keys()]) # 为所有物品或仅新物品搜索k近邻 for item_id in new_item_ids: vector self.feature_db[item_id].reshape(1, -1) # D是距离I是索引ID。这里用内积所以“距离”越大越相似 D, I self.index.search(vector, self.k1) # 1是因为自己总是第一个最近邻 similarities D[0][1:] # 排除自己 neighbor_indices I[0][1:] for sim, idx in zip(similarities, neighbor_indices): if sim self.threshold: neighbor_id self.id_from_index(idx) # 需要维护索引到ID的映射 # 添加双向边可选也可只加单向 self.graph[item_id].append((neighbor_id, sim)) self.graph[neighbor_id].append((item_id, sim)) # 可选图增强步骤如运行一次标签传播或简单的GCN平滑 self._graph_smoothing() def _graph_smoothing(self, num_iterations1): 简单的图平滑每个节点的特征用其邻居特征的加权平均来更新一次 new_features {} for node_id, neighbors in self.graph.items(): if not neighbors: new_features[node_id] self.feature_db[node_id] continue neighbor_vectors [self.feature_db[nid] * weight for nid, weight in neighbors] aggregated np.mean(neighbor_vectors, axis0) # 与原特征融合alpha是平滑系数 alpha 0.7 new_features[node_id] alpha * self.feature_db[node_id] (1-alpha) * aggregated # 更新特征库和索引注意更新索引成本高可定期进行 for node_id, vec in new_features.items(): self.feature_db[node_id] vec / np.linalg.norm(vec) # 需要重建或更新Faiss索引此处略具体操作依赖Faiss API实操心得图构建的工程权衡k值选择k太小图连通性差信息传播不远k太大图过于稠密会引入噪声且计算存储开销大。通常从20-100开始调试。可以观察图的连通分量Connected Components数量理想情况是只有一个大分量。相似度阈值必须设置阈值过滤掉弱关联。否则所有k个邻居都被连接会包含大量不相关物品严重稀释用户表示。阈值需要根据特征向量归一化后的相似度分布来确定通常取分布的高分位点如70%分位数。增量更新频率对于物品库日增量的场景可以每天凌晨全量重建图。对于实时性要求高的场景可以每小时增量更新一次。更新索引Faiss是重操作需要规划好时间窗口。3.3 用户初始表示的实时生成这是线上服务的关键部分要求低延迟。# 伪代码示例实时用户表示初始化服务 class SGURInitService: def __init__(self, graph_builder, feature_db): self.graph graph_builder.graph self.feature_db feature_db def init_user_representation(self, user_interacted_item_ids, aggregation_methodweighted, hop1): 根据用户交互的物品ID列表生成初始用户向量。 :param user_interacted_item_ids: 用户本次会话交互的物品ID列表 :param aggregation_method: 聚合方法mean, weighted, attention :param hop: 在图中扩展的跳数1或2 :return: 用户初始向量 (np.array) if not user_interacted_item_ids: # 如果用户没有任何交互返回全局物品向量的均值或零向量 return np.zeros(self.feature_db.vector_dim) seed_nodes set(user_interacted_item_ids) # 1. 提取局部子图节点 subgraph_nodes set(seed_nodes) for _ in range(hop): new_nodes set() for node in subgraph_nodes: if node in self.graph: neighbors [nid for nid, _ in self.graph[node]] new_nodes.update(neighbors) subgraph_nodes.update(new_nodes) # 2. 获取子图所有节点的特征向量 node_vectors {nid: self.feature_db[nid] for nid in subgraph_nodes if nid in self.feature_db} # 3. 聚合 if aggregation_method mean: user_vector np.mean(list(node_vectors.values()), axis0) elif aggregation_method weighted: # 计算权重种子节点权重为1一跳邻居权重为边权二跳邻居权重衰减 weights {} for node in subgraph_nodes: if node in seed_nodes: weights[node] 1.0 else: # 找到该节点到任意种子节点的最短路径最大边权简化处理 max_sim_to_seed 0.0 for seed in seed_nodes: # 需要查询graph中seed到node的边权这里假设有辅助函数get_edge_weight sim self._get_edge_weight(seed, node) if sim max_sim_to_seed: max_sim_to_seed sim weights[node] max_sim_to_seed * (0.5 ** (hop-1)) # 跳数衰减 weighted_vectors [node_vectors[nid] * weights[nid] for nid in node_vectors.keys()] user_vector np.sum(weighted_vectors, axis0) / (sum(weights.values()) 1e-8) elif aggregation_method attention: # 使用一个预训练好的轻量级注意力网络 # 将种子节点向量取平均作为“查询”子图节点向量作为“键”和“值” seed_vec np.mean([self.feature_db[sid] for sid in seed_nodes], axis0) # 这里简化表示实际是一个小的神经网络计算注意力分数 attn_scores self.attention_net(seed_vec, list(node_vectors.values())) user_vector np.sum([attn_scores[i] * vec for i, vec in enumerate(node_vectors.values())], axis0) else: raise ValueError(fUnsupported aggregation method: {aggregation_method}) # 归一化输出方便后续余弦相似度计算 norm np.linalg.norm(user_vector) if norm 0: user_vector user_vector / norm return user_vector注意事项线上服务的性能与稳定性延迟hop参数通常设为1就足够了。设为2会使子图节点数呈指数增长可能引发性能问题。务必对子图节点数量设置上限例如最多500个节点超过则进行随机采样或按相似度截断。缓存对于常见的用户初始行为模式例如首页热门物品的点击组合其生成的用户向量可以缓存起来避免重复计算。冷启动中的冷启动如果用户连一个物品都没交互怎么办我们有一个备选方案使用用户注册时填写的有限信息如选择的兴趣标签、性别、地域来匹配一组“预设种子物品”再走上述流程。或者直接返回一个由“大众流行物品”聚合而成的向量。4. 与下游推荐模型的集成方案SG-URInit 是模型无关的这意味着它生成的用户向量需要能够无缝“插入”到现有的推荐模型架构中。这里有几个典型的集成模式。4.1 集成到双塔召回模型在双塔模型中用户塔和物品塔分别产出用户向量和物品向量通过内积计算匹配分。常规做法用户塔的输入是用户历史行为序列ID通过嵌入层查找对应的物品嵌入再经过池化、神经网络等得到用户向量。SG-URInit集成对于新用户我们没有历史行为序列。此时我们可以用 SG-URInit 生成的向量v_u_init直接作为用户塔的输出绕过用户塔的计算。或者将v_u_init作为用户塔的一个额外输入特征与一个可学习的“冷启动偏置”嵌入向量拼接再输入用户塔的神经网络。后一种方法更灵活允许模型学习如何利用和调整我们提供的先验信息。# 伪代码在双塔模型用户侧集成SG-URInit class UserTowerWithColdStart(nn.Module): def __init__(self, item_embedding_layer, hidden_dims, init_embed_dim): super().__init__() self.item_embedding item_embedding_layer # 一个小的网络用于处理历史行为序列 self.seq_encoder ... # 一个投影层处理SG-URInit的初始向量 self.cold_start_proj nn.Linear(init_embed_dim, hidden_dims[0]) # 主网络 self.mlp nn.Sequential(...) def forward(self, user_history_seq, cold_start_vector): if user_history_seq is not None and len(user_history_seq) 0: # 有行为用户走正常序列编码 seq_emb self.item_embedding(user_history_seq) user_emb_from_seq self.seq_encoder(seq_emb) # 如果有冷启动向量可以拼接或加权融合这里用门控融合 if cold_start_vector is not None: gate torch.sigmoid(self.fusion_gate(torch.cat([user_emb_from_seq, cold_start_vector], dim-1))) combined gate * user_emb_from_seq (1 - gate) * self.cold_start_proj(cold_start_vector) else: combined user_emb_from_seq else: # 新用户完全依赖冷启动向量 combined self.cold_start_proj(cold_start_vector) final_user_emb self.mlp(combined) return final_user_emb4.2 集成到排序模型排序模型如 DeepFM、DIN通常有丰富的用户特征域。我们可以将 SG-URInit 生成的向量作为一个新的、高维的连续特征Dense Feature加入到特征工程中。特征拼接直接将v_u_init拼接到排序模型的输入层之后与其他特征一起输入后续的深度网络。作为注意力查询在像 DINDeep Interest Network这样的模型中用户兴趣是从历史行为中动态聚合的。对于新用户我们可以用v_u_init作为“虚拟”的兴趣查询向量去和候选物品计算注意力分数模拟出一个初始的兴趣分布。实操心得模型集成的训练技巧两阶段训练首先在拥有丰富行为数据的老用户上正常训练你的推荐模型保持 SG-URInit 相关参数如投影层、注意力网络固定或微调。然后在训练中模拟冷启动场景随机将一部分老用户的行为序列掩码mask掉强迫模型学习使用 SG-URInit 提供的初始向量。这样能让模型学会在“有数据”和“没数据”两种状态下平滑过渡。损失函数设计可以在损失函数中加入一项正则化鼓励模型对于行为稀疏的用户其输出的用户向量不要偏离 SG-URInit 初始向量太远。这相当于给模型一个基于内容的强先验。5. 效果评估、常见问题与调优指南上线一个新策略没有严谨的评估就是耍流氓。SG-URInit 的效果评估需要分场景、分阶段进行。5.1 离线评估指标在离线环境下我们无法获得真实的新用户反馈但可以通过模拟实验来评估。模拟冷启动实验从老用户数据中随机选取一部分用户隐藏他们早期如前10次的交互行为只用他们第一次交互的1-3个物品作为“种子”用 SG-URInit 生成初始向量。然后用这个向量去预测他们被隐藏的那些早期交互物品计算召回率RecallK、命中率Hit Rate或归一化折损累计增益NDCGK。与基线方法如随机初始化、全局平均向量初始化对比。向量质量分析簇内距离/簇间距离将用户按兴趣类别分组根据其长期行为定义用 SG-URInit 为每个用户生成初始向量。计算同一组内用户向量的平均距离应小不同组间用户向量的平均距离应大。与最终向量的相似度对于有后续行为的用户用其完整行为训练一个“最终”的用户向量。计算 SG-URInit 初始向量与这个“最终”向量的余弦相似度。相似度越高说明初始向量越准确指向了用户稳定的兴趣方向。5.2 线上A/B测试指标这是黄金标准。将新用户流量随机分为实验组使用 SG-URInit和对照组使用原有冷启动策略。核心指标初期互动率新用户首次会话的点击率CTR、播放完成率、点赞率等。留存率新用户的次日留存、7日留存。这是衡量冷启动成功与否的终极指标。探索效率用户在前N次交互中接触到的物品类目或标签的多样性。好的初始化应引导用户快速探索到其兴趣区而不是漫无目的。注意事项A/B测试需要跑够样本量通常需要数万新用户和足够长时间至少一周以观察留存效应同时要监控对老用户是否有负面影响理论上应该没有。5.3 常见问题排查与调优在实际部署中我们踩过不少坑这里总结一下问题现象可能原因排查与解决方案线上服务延迟高子图节点数过多hop过大或k过大相似度计算或图查询未缓存。限制hop1对子图节点数设置硬上限如200对高频种子物品组合的生成结果进行缓存。新用户推荐结果过于集中/发散聚合策略不当相似度阈值设置不合理。过于集中尝试mean聚合或降低权重衰减。过于发散尝试weighted聚合提高相似度阈值减少k。离线评估效果好线上效果差离线模拟与真实场景有gap线上特征与离线特征不一致物品特征更新不及时。检查线上特征提取管道是否与离线一致确保物品特征库是近实时更新的延迟小于1小时在A/B测试中做更细粒度的分析看哪些用户群有效。对某些小众兴趣捕捉很差小众物品在k-NN图中连接数少成为“孤岛”特征提取模型对该领域不敏感。在构建图时为小众物品适当增加k值或降低相似度阈值强制增加连接考虑引入领域知识图谱来补充语义关系尝试在该领域数据上微调特征提取模型但这会引入训练成本。内存占用过大物品特征向量全量存储在内存图结构存储冗余。对特征向量进行量化如PQ量化将不活跃的旧物品特征存入磁盘或数据库仅缓存热物使用更紧凑的图存储格式如CSR。5.4 高级调优方向当基础版本跑通后可以考虑以下方向进行优化动态权重聚合用户与不同种子物品的交互强度点击、购买、长停留不同。在聚合时应根据交互类型赋予种子节点不同的初始权重。会话感知用户的首次会话可能包含多个有逻辑关联的点击。可以利用会话内物品的顺序或共现关系构建一个更精细的会话子图再进行聚合。与元学习结合将 SG-URInit 看作一个为“新用户”这个任务生成先验知识的方法。可以结合 MAML 等元学习算法让下游推荐模型学会如何快速适应这个先验用极少的几次用户反馈就更新出更精准的用户表示。最后想说的是SG-URInit 的本质是利用物品内容本身的、丰富的语义结构来补偿用户行为数据的缺失。它不是一个取代深度推荐模型的银弹而是一个强大的“助推器”和“稳定器”尤其适用于用户生命周期的最初那段关键而脆弱的时期。在实际业务中落地这种方法不仅需要算法创新更需要工程上的精心设计和持续迭代。从特征管道的稳定性到图索引的构建效率再到线上服务的毫秒级响应每一个环节都关乎最终的用户体验。我们也是在经历了多次迭代和优化后才让这套系统真正稳定、高效地跑起来并看到了业务指标的切实提升。如果你也在为推荐系统的冷启动问题头疼不妨从构建一个高质量的物品语义图开始尝试这或许会为你打开一扇新的大门。
SG-URInit:基于语义图的免训练用户表示初始化,破解推荐系统冷启动难题
1. 项目概述为什么我们需要重新思考推荐系统的“冷启动”如果你做过推荐系统尤其是涉及图片、视频、音频等多模态内容的推荐一定对“冷启动”这个词又爱又恨。爱的是它代表了系统对新用户或新物品的潜力挖掘恨的是它往往是整个系统效果最薄弱、最让人头疼的环节。传统的做法无论是基于内容的特征匹配还是基于协同过滤的隐式推断都绕不开一个核心问题如何在没有历史交互数据或者数据极其稀疏的情况下快速、准确地为用户建立一个有效的“数字画像”这就是用户表示初始化要解决的难题。想象一下一个新用户刚刚打开你的App系统对他一无所知。常规的深度推荐模型比如那些动辄几百万参数的神经网络需要依赖大量的用户-物品交互数据来训练和优化用户嵌入向量。对于这个“白板”用户模型要么给他一个随机向量要么给一个所有用户的平均向量。结果可想而知最初的推荐往往驴唇不对马嘴用户体验大打折扣用户可能因此流失永远失去了积累数据的机会。最近我团队在研究和实践中提出并验证了一种名为SG-URInit的方法。这个名字拆开看SG代表“语义图”Semantic GraphURInit代表“用户表示初始化”User Representation Initialization。它的核心思想非常直接不依赖任何历史交互数据也不需要对下游推荐模型进行任何重新训练仅利用物品本身的多模态内容信息就能为新用户生成一个高质量、个性化的初始表示向量。这个方法最吸引人的地方在于它的“模型无关性”和“免训练”。这意味着无论你后端用的是经典的矩阵分解MF、神经协同过滤NCF还是更复杂的Transformer-based模型SG-URInit 生成的那个初始向量都可以直接“喂”进去立刻提升新用户的首批推荐质量。我们内部在电商、短视频和音乐等多个场景下的A/B测试都表明采用这种方法后新用户的次日留存率和初期互动率有显著提升。2. 核心思路拆解从多模态内容到用户语义图要理解 SG-URInit得先抛开“用户-物品交互”这个传统视角回到一个更本质的问题一个物品是如何吸引一个用户的答案往往藏在物品的内容里。一件衣服的款式和颜色一段视频的封面和标题关键词一首歌的旋律风格和歌手信息——这些多模态特征本身就承载着强烈的语义信号能够吸引具有特定偏好的用户群体。SG-URInit 的核心思路就是构建并利用这种“物品-物品”之间的语义关联来间接推测新用户的可能偏好。整个过程不涉及任何用户行为数据完全基于物品的内容特征库。下面我拆解一下它的三个关键阶段。2.1 阶段一多模态特征提取与融合这是所有工作的基础。假设我们的系统里有数百万个物品商品、视频、文章等每个物品都拥有多种模态的信息。以电商商品为例文本模态商品标题、描述、用户评论摘要。视觉模态商品主图、细节图、场景图。属性模态品牌、类别、价格、材质等结构化标签。我们的第一步是使用预训练好的、领域通用的深度模型将这些原始内容转化为稠密的特征向量即嵌入向量。文本特征可以使用 BERT、Sentence-BERT 或 SimCSE 等模型将标题和描述编码成固定维度的向量。这里的关键是模型是在大规模通用语料上预训练的我们直接拿来用不需要用我们自己的业务数据再训练fine-tune。这保证了方法的“免训练”特性。视觉特征使用在 ImageNet 上预训练的 ResNet、EfficientNet 或 CLIP 的图像编码器提取商品图片的高层语义特征。属性特征对于品牌、类别这类离散特征可以采用嵌入层Embedding Layer进行编码对于价格这类连续特征可以归一化后直接使用或分桶嵌入。实操心得特征对齐的重要性不同模态的特征向量通常位于不同的语义空间维度也不同。直接拼接或简单加权效果可能不好。我们实践下来一个有效的方法是使用一个简单的投影层比如一个全连接网络将不同模态的特征映射到一个统一的、维度相同的公共语义空间。这个投影层的参数是固定的、随机初始化的或者用一个小规模的、无监督的目标如重构损失稍作调整。重点是这个投影层的训练不依赖于任何用户行为数据只依赖于物品内容本身因此依然符合“免训练”的设定。将所有模态的特征在公共空间进行融合就得到了每个物品的统一多模态语义向量。这个向量综合反映了物品在各个维度上的内容信息。2.2 阶段二语义图构建与增强有了所有物品的语义向量我们就可以构建一个全局的物品语义图。在这个图中每个节点代表一个物品节点之间的边权重代表物品之间的语义相似度。计算相似度最直接的方法是余弦相似度。但是对于百万甚至千万量级的物品库计算全连接图是不现实的存储开销也是巨大的。因此我们采用k-最近邻k-NN的方法来构建一个稀疏图对于每个物品只保留与它语义最相似的前k个物品作为邻居并记录相似度作为边权重。注意事项相似度计算的选择与优化效率问题直接两两计算余弦相似度复杂度是 O(N^2)。对于大规模数据必须使用近似最近邻搜索库如 FaissFacebook、AnnoySpotify或 ScaNNGoogle。这些库能在可接受的时间内从海量向量中快速找出每个向量的Top-k最近邻。相似度度量余弦相似度对于方向敏感欧氏距离对于尺度敏感。在我们的多模态融合空间中余弦相似度通常表现更好。但也可以尝试马氏距离等如果特征分布经过精心调整。图的对称性A是B的k近邻B不一定是A的k近邻。为了构建一个更稳定、连通性更好的图通常采用“并集”或“交集”策略来保证对称性或者直接使用互k近邻Mutual k-NN。构建出基础的k-NN图后这个图可能还比较“粗糙”只反映了局部的一阶相似关系。我们可以通过图扩散或图卷积的思想对其进行增强。简单来说就是让节点的信息沿着边在图中传播一步或多步。例如使用图卷积网络GCN的一层传播公式H D^(-1/2) A D^(-1/2) H其中A是图的邻接矩阵由k-NN相似度构成D是度矩阵H是所有节点的特征矩阵即我们的多模态语义向量。经过这样一次传播每个节点的特征就融合了其一阶邻居的信息使得图中语义相近的“群落”内部特征更加平滑、一致。这个过程可以看作是对物品语义的一次全局平滑和增强让后续的推断更鲁棒。2.3 阶段三基于图的用户表示初始化现在终于到了为新用户生成初始表示的时刻。假设新用户u在首次会话中与一组物品I_u {i1, i2, ..., im}发生了交互例如点击、浏览、购买。这些物品就是我们在冷启动阶段唯一的“线索”。SG-URInit 的核心操作如下种子节点定位将用户交互过的物品集合I_u映射到我们预先构建好的语义图上这些物品对应的节点就是“种子节点”。子图提取与聚合以这些种子节点为中心提取它们在图中的邻居可以是一阶或二阶邻居形成一个局部子图。这个子图包含了与用户初始兴趣直接相关和间接相关的物品群落。表示生成对这个局部子图中的所有节点物品的特征向量进行聚合。聚合方式有多种选择均值聚合最简单直接计算子图内所有节点特征的平均值。适合快速实现。加权聚合根据种子节点与邻居节点的相似度边权重进行加权平均。与用户直接交互的物品权重更高。注意力聚合引入一个简单的注意力机制让模型这里是一个轻量级网络自动学习在子图中哪些节点对于表征用户兴趣更重要。这个注意力网络的参数是固定的、通用的不随用户变化且训练不依赖用户行为数据例如可以用图重构任务预训练一个图自编码器然后复用其注意力权重生成器。最终聚合得到的向量就是为新用户u生成的初始表示embedding_u。这个向量天然地落在了由所有物品语义构成的向量空间中并且蕴含了基于内容语义的泛化信息——它不仅反映了用户当前点击的物品还反映了与这些物品语义相似的其他物品所构成的兴趣领域。3. 技术实现细节与实操要点理论讲完了我们来点硬核的看看具体怎么实现。我会结合我们工程实践中的代码片段和配置把关键细节讲透。3.1 物品特征库的构建与维护这是整个系统的基石必须设计成可增量更新、高效检索的。# 伪代码示例物品特征提取与存储管道 import torch import faiss from transformers import AutoModel, AutoTokenizer from PIL import Image import torchvision.models as models import torchvision.transforms as transforms class ItemFeaturePipeline: def __init__(self): # 1. 初始化各模态编码器全部使用预训练模型不fine-tune self.text_model AutoModel.from_pretrained(sentence-transformers/all-MiniLM-L6-v2) self.text_tokenizer AutoTokenizer.from_pretrained(sentence-transformers/all-MiniLM-L6-v2) self.image_model models.resnet50(pretrainedTrue) self.image_model torch.nn.Sequential(*(list(self.image_model.children())[:-1])) # 移除最后一层分类头 self.image_model.eval() self.image_transform transforms.Compose([...]) # 标准预处理 # 2. 初始化投影层将不同模态特征映射到统一空间dim256 self.text_proj torch.nn.Linear(384, 256) # MiniLM输出384维 self.image_proj torch.nn.Linear(2048, 256) # ResNet-50池化层后是2048维 # 属性投影层略... # 3. 初始化特征存储和索引 self.feature_db {} # 内存或Redis缓存item_id - unified_vector self.index faiss.IndexFlatIP(256) # 内积索引用于后续k-NN搜索 def extract_and_store(self, item_id, title, description, image_path, attributes): # 提取文本特征 text_inputs self.text_tokenizer([title description], ...) with torch.no_grad(): text_features self.text_model(**text_inputs).last_hidden_state[:, 0, :] # [CLS] token text_features self.text_proj(text_features).squeeze().numpy() # 提取图像特征 img Image.open(image_path) img_tensor self.image_transform(img).unsqueeze(0) with torch.no_grad(): image_features self.image_model(img_tensor).squeeze() image_features self.image_proj(image_features).squeeze().numpy() # 融合这里采用简单加权平均可根据业务调整 unified_vector 0.6 * text_features 0.4 * image_features # 可选L2归一化使余弦相似度等于内积 unified_vector unified_vector / np.linalg.norm(unified_vector) # 存储 self.feature_db[item_id] unified_vector self.index.add(np.array([unified_vector]).astype(float32))关键配置与调优点投影层训练虽然说是“免训练”但这里的投影层参数可以先用一个无监督目标进行初始化。例如利用对比学习思想让同一物品的不同模态特征经过投影后尽可能接近不同物品的特征尽可能远离。用物品ID自监督即可仍然不需要用户数据。特征融合权重0.6和0.4是经验值。更科学的方法是设计一个轻量的门控网络Gating Network根据物品类型动态调整权重。例如对于服装视觉权重大对于书籍文本权重大。这个门控网络同样可以用物品内容自监督训练。索引选择IndexFlatIP是精确搜索适合物品数在百万级以下。超过百万建议使用IndexIVFFlat或IndexHNSWFlat等近似索引在精度和速度间取得平衡。需要定期用index.train()在代表性数据上训练索引。3.2 语义图的高效构建与更新图不能每次都从头构建需要支持增量更新。# 伪代码示例基于Faiss的增量式k-NN图构建 class SemanticGraphBuilder: def __init__(self, index, k50, similarity_threshold0.5): self.index index # Faiss索引 self.k k self.threshold similarity_threshold self.graph defaultdict(list) # 邻接表item_id - list of (neighbor_id, similarity) def build_or_update_graph(self, new_item_ids): 增量构建图。new_item_ids是新加入的物品ID列表 all_vectors np.array([self.feature_db[iid] for iid in self.feature_db.keys()]) # 为所有物品或仅新物品搜索k近邻 for item_id in new_item_ids: vector self.feature_db[item_id].reshape(1, -1) # D是距离I是索引ID。这里用内积所以“距离”越大越相似 D, I self.index.search(vector, self.k1) # 1是因为自己总是第一个最近邻 similarities D[0][1:] # 排除自己 neighbor_indices I[0][1:] for sim, idx in zip(similarities, neighbor_indices): if sim self.threshold: neighbor_id self.id_from_index(idx) # 需要维护索引到ID的映射 # 添加双向边可选也可只加单向 self.graph[item_id].append((neighbor_id, sim)) self.graph[neighbor_id].append((item_id, sim)) # 可选图增强步骤如运行一次标签传播或简单的GCN平滑 self._graph_smoothing() def _graph_smoothing(self, num_iterations1): 简单的图平滑每个节点的特征用其邻居特征的加权平均来更新一次 new_features {} for node_id, neighbors in self.graph.items(): if not neighbors: new_features[node_id] self.feature_db[node_id] continue neighbor_vectors [self.feature_db[nid] * weight for nid, weight in neighbors] aggregated np.mean(neighbor_vectors, axis0) # 与原特征融合alpha是平滑系数 alpha 0.7 new_features[node_id] alpha * self.feature_db[node_id] (1-alpha) * aggregated # 更新特征库和索引注意更新索引成本高可定期进行 for node_id, vec in new_features.items(): self.feature_db[node_id] vec / np.linalg.norm(vec) # 需要重建或更新Faiss索引此处略具体操作依赖Faiss API实操心得图构建的工程权衡k值选择k太小图连通性差信息传播不远k太大图过于稠密会引入噪声且计算存储开销大。通常从20-100开始调试。可以观察图的连通分量Connected Components数量理想情况是只有一个大分量。相似度阈值必须设置阈值过滤掉弱关联。否则所有k个邻居都被连接会包含大量不相关物品严重稀释用户表示。阈值需要根据特征向量归一化后的相似度分布来确定通常取分布的高分位点如70%分位数。增量更新频率对于物品库日增量的场景可以每天凌晨全量重建图。对于实时性要求高的场景可以每小时增量更新一次。更新索引Faiss是重操作需要规划好时间窗口。3.3 用户初始表示的实时生成这是线上服务的关键部分要求低延迟。# 伪代码示例实时用户表示初始化服务 class SGURInitService: def __init__(self, graph_builder, feature_db): self.graph graph_builder.graph self.feature_db feature_db def init_user_representation(self, user_interacted_item_ids, aggregation_methodweighted, hop1): 根据用户交互的物品ID列表生成初始用户向量。 :param user_interacted_item_ids: 用户本次会话交互的物品ID列表 :param aggregation_method: 聚合方法mean, weighted, attention :param hop: 在图中扩展的跳数1或2 :return: 用户初始向量 (np.array) if not user_interacted_item_ids: # 如果用户没有任何交互返回全局物品向量的均值或零向量 return np.zeros(self.feature_db.vector_dim) seed_nodes set(user_interacted_item_ids) # 1. 提取局部子图节点 subgraph_nodes set(seed_nodes) for _ in range(hop): new_nodes set() for node in subgraph_nodes: if node in self.graph: neighbors [nid for nid, _ in self.graph[node]] new_nodes.update(neighbors) subgraph_nodes.update(new_nodes) # 2. 获取子图所有节点的特征向量 node_vectors {nid: self.feature_db[nid] for nid in subgraph_nodes if nid in self.feature_db} # 3. 聚合 if aggregation_method mean: user_vector np.mean(list(node_vectors.values()), axis0) elif aggregation_method weighted: # 计算权重种子节点权重为1一跳邻居权重为边权二跳邻居权重衰减 weights {} for node in subgraph_nodes: if node in seed_nodes: weights[node] 1.0 else: # 找到该节点到任意种子节点的最短路径最大边权简化处理 max_sim_to_seed 0.0 for seed in seed_nodes: # 需要查询graph中seed到node的边权这里假设有辅助函数get_edge_weight sim self._get_edge_weight(seed, node) if sim max_sim_to_seed: max_sim_to_seed sim weights[node] max_sim_to_seed * (0.5 ** (hop-1)) # 跳数衰减 weighted_vectors [node_vectors[nid] * weights[nid] for nid in node_vectors.keys()] user_vector np.sum(weighted_vectors, axis0) / (sum(weights.values()) 1e-8) elif aggregation_method attention: # 使用一个预训练好的轻量级注意力网络 # 将种子节点向量取平均作为“查询”子图节点向量作为“键”和“值” seed_vec np.mean([self.feature_db[sid] for sid in seed_nodes], axis0) # 这里简化表示实际是一个小的神经网络计算注意力分数 attn_scores self.attention_net(seed_vec, list(node_vectors.values())) user_vector np.sum([attn_scores[i] * vec for i, vec in enumerate(node_vectors.values())], axis0) else: raise ValueError(fUnsupported aggregation method: {aggregation_method}) # 归一化输出方便后续余弦相似度计算 norm np.linalg.norm(user_vector) if norm 0: user_vector user_vector / norm return user_vector注意事项线上服务的性能与稳定性延迟hop参数通常设为1就足够了。设为2会使子图节点数呈指数增长可能引发性能问题。务必对子图节点数量设置上限例如最多500个节点超过则进行随机采样或按相似度截断。缓存对于常见的用户初始行为模式例如首页热门物品的点击组合其生成的用户向量可以缓存起来避免重复计算。冷启动中的冷启动如果用户连一个物品都没交互怎么办我们有一个备选方案使用用户注册时填写的有限信息如选择的兴趣标签、性别、地域来匹配一组“预设种子物品”再走上述流程。或者直接返回一个由“大众流行物品”聚合而成的向量。4. 与下游推荐模型的集成方案SG-URInit 是模型无关的这意味着它生成的用户向量需要能够无缝“插入”到现有的推荐模型架构中。这里有几个典型的集成模式。4.1 集成到双塔召回模型在双塔模型中用户塔和物品塔分别产出用户向量和物品向量通过内积计算匹配分。常规做法用户塔的输入是用户历史行为序列ID通过嵌入层查找对应的物品嵌入再经过池化、神经网络等得到用户向量。SG-URInit集成对于新用户我们没有历史行为序列。此时我们可以用 SG-URInit 生成的向量v_u_init直接作为用户塔的输出绕过用户塔的计算。或者将v_u_init作为用户塔的一个额外输入特征与一个可学习的“冷启动偏置”嵌入向量拼接再输入用户塔的神经网络。后一种方法更灵活允许模型学习如何利用和调整我们提供的先验信息。# 伪代码在双塔模型用户侧集成SG-URInit class UserTowerWithColdStart(nn.Module): def __init__(self, item_embedding_layer, hidden_dims, init_embed_dim): super().__init__() self.item_embedding item_embedding_layer # 一个小的网络用于处理历史行为序列 self.seq_encoder ... # 一个投影层处理SG-URInit的初始向量 self.cold_start_proj nn.Linear(init_embed_dim, hidden_dims[0]) # 主网络 self.mlp nn.Sequential(...) def forward(self, user_history_seq, cold_start_vector): if user_history_seq is not None and len(user_history_seq) 0: # 有行为用户走正常序列编码 seq_emb self.item_embedding(user_history_seq) user_emb_from_seq self.seq_encoder(seq_emb) # 如果有冷启动向量可以拼接或加权融合这里用门控融合 if cold_start_vector is not None: gate torch.sigmoid(self.fusion_gate(torch.cat([user_emb_from_seq, cold_start_vector], dim-1))) combined gate * user_emb_from_seq (1 - gate) * self.cold_start_proj(cold_start_vector) else: combined user_emb_from_seq else: # 新用户完全依赖冷启动向量 combined self.cold_start_proj(cold_start_vector) final_user_emb self.mlp(combined) return final_user_emb4.2 集成到排序模型排序模型如 DeepFM、DIN通常有丰富的用户特征域。我们可以将 SG-URInit 生成的向量作为一个新的、高维的连续特征Dense Feature加入到特征工程中。特征拼接直接将v_u_init拼接到排序模型的输入层之后与其他特征一起输入后续的深度网络。作为注意力查询在像 DINDeep Interest Network这样的模型中用户兴趣是从历史行为中动态聚合的。对于新用户我们可以用v_u_init作为“虚拟”的兴趣查询向量去和候选物品计算注意力分数模拟出一个初始的兴趣分布。实操心得模型集成的训练技巧两阶段训练首先在拥有丰富行为数据的老用户上正常训练你的推荐模型保持 SG-URInit 相关参数如投影层、注意力网络固定或微调。然后在训练中模拟冷启动场景随机将一部分老用户的行为序列掩码mask掉强迫模型学习使用 SG-URInit 提供的初始向量。这样能让模型学会在“有数据”和“没数据”两种状态下平滑过渡。损失函数设计可以在损失函数中加入一项正则化鼓励模型对于行为稀疏的用户其输出的用户向量不要偏离 SG-URInit 初始向量太远。这相当于给模型一个基于内容的强先验。5. 效果评估、常见问题与调优指南上线一个新策略没有严谨的评估就是耍流氓。SG-URInit 的效果评估需要分场景、分阶段进行。5.1 离线评估指标在离线环境下我们无法获得真实的新用户反馈但可以通过模拟实验来评估。模拟冷启动实验从老用户数据中随机选取一部分用户隐藏他们早期如前10次的交互行为只用他们第一次交互的1-3个物品作为“种子”用 SG-URInit 生成初始向量。然后用这个向量去预测他们被隐藏的那些早期交互物品计算召回率RecallK、命中率Hit Rate或归一化折损累计增益NDCGK。与基线方法如随机初始化、全局平均向量初始化对比。向量质量分析簇内距离/簇间距离将用户按兴趣类别分组根据其长期行为定义用 SG-URInit 为每个用户生成初始向量。计算同一组内用户向量的平均距离应小不同组间用户向量的平均距离应大。与最终向量的相似度对于有后续行为的用户用其完整行为训练一个“最终”的用户向量。计算 SG-URInit 初始向量与这个“最终”向量的余弦相似度。相似度越高说明初始向量越准确指向了用户稳定的兴趣方向。5.2 线上A/B测试指标这是黄金标准。将新用户流量随机分为实验组使用 SG-URInit和对照组使用原有冷启动策略。核心指标初期互动率新用户首次会话的点击率CTR、播放完成率、点赞率等。留存率新用户的次日留存、7日留存。这是衡量冷启动成功与否的终极指标。探索效率用户在前N次交互中接触到的物品类目或标签的多样性。好的初始化应引导用户快速探索到其兴趣区而不是漫无目的。注意事项A/B测试需要跑够样本量通常需要数万新用户和足够长时间至少一周以观察留存效应同时要监控对老用户是否有负面影响理论上应该没有。5.3 常见问题排查与调优在实际部署中我们踩过不少坑这里总结一下问题现象可能原因排查与解决方案线上服务延迟高子图节点数过多hop过大或k过大相似度计算或图查询未缓存。限制hop1对子图节点数设置硬上限如200对高频种子物品组合的生成结果进行缓存。新用户推荐结果过于集中/发散聚合策略不当相似度阈值设置不合理。过于集中尝试mean聚合或降低权重衰减。过于发散尝试weighted聚合提高相似度阈值减少k。离线评估效果好线上效果差离线模拟与真实场景有gap线上特征与离线特征不一致物品特征更新不及时。检查线上特征提取管道是否与离线一致确保物品特征库是近实时更新的延迟小于1小时在A/B测试中做更细粒度的分析看哪些用户群有效。对某些小众兴趣捕捉很差小众物品在k-NN图中连接数少成为“孤岛”特征提取模型对该领域不敏感。在构建图时为小众物品适当增加k值或降低相似度阈值强制增加连接考虑引入领域知识图谱来补充语义关系尝试在该领域数据上微调特征提取模型但这会引入训练成本。内存占用过大物品特征向量全量存储在内存图结构存储冗余。对特征向量进行量化如PQ量化将不活跃的旧物品特征存入磁盘或数据库仅缓存热物使用更紧凑的图存储格式如CSR。5.4 高级调优方向当基础版本跑通后可以考虑以下方向进行优化动态权重聚合用户与不同种子物品的交互强度点击、购买、长停留不同。在聚合时应根据交互类型赋予种子节点不同的初始权重。会话感知用户的首次会话可能包含多个有逻辑关联的点击。可以利用会话内物品的顺序或共现关系构建一个更精细的会话子图再进行聚合。与元学习结合将 SG-URInit 看作一个为“新用户”这个任务生成先验知识的方法。可以结合 MAML 等元学习算法让下游推荐模型学会如何快速适应这个先验用极少的几次用户反馈就更新出更精准的用户表示。最后想说的是SG-URInit 的本质是利用物品内容本身的、丰富的语义结构来补偿用户行为数据的缺失。它不是一个取代深度推荐模型的银弹而是一个强大的“助推器”和“稳定器”尤其适用于用户生命周期的最初那段关键而脆弱的时期。在实际业务中落地这种方法不仅需要算法创新更需要工程上的精心设计和持续迭代。从特征管道的稳定性到图索引的构建效率再到线上服务的毫秒级响应每一个环节都关乎最终的用户体验。我们也是在经历了多次迭代和优化后才让这套系统真正稳定、高效地跑起来并看到了业务指标的切实提升。如果你也在为推荐系统的冷启动问题头疼不妨从构建一个高质量的物品语义图开始尝试这或许会为你打开一扇新的大门。