推荐系统召回三板斧:协同过滤、向量召回与混合策略实战
2026/9/18 6:15:04 网站建设 项目流程

简介:本资源是一份面向机器学习初学者与推荐系统入门者的专业教学PPT,聚焦推荐系统核心环节——召回策略的原理与实践。内容系统梳理热度榜、分类器模型、关联规则挖掘(含共现矩阵与Jaccard相似度归一化)、矩阵分解四大主流召回方法,深入剖析其适用场景、个性化能力、冷启动应对机制及典型局限,并结合视频、电商、音乐等真实业务场景说明优化目标(如点击率、下单率、听完率)与时间、上下文、物品属性等关键影响因素。资源为单文件PPT格式,共1个5.17MB演示文稿,结构清晰、图文并茂,含LOGO页、问题定义、方法对比、公式推导与流程图解,便于课堂讲授或自学梳理知识脉络。目前已有123人学习下载,适合高校学生、转行AI从业者及算法工程师快速建立推荐系统召回层的体系化认知。

1. 推荐系统里“召回”不是找回来,而是从亿级商品中筛出几百个可能被点击的候选——它决定后续所有环节的上限

很多人第一次接触推荐系统时,以为“召回”就是把用户历史行为里漏掉的商品再捞一遍;其实恰恰相反:召回是整个推荐链路的第一道闸门,它的任务是在毫秒级响应下,从千万甚至上亿条物品(商品、视频、新闻、音乐)中,快速筛选出与当前用户兴趣高度相关、且具备业务意义的几百到几千条候选集。这一步不追求精准排序,但必须覆盖全面、无明显遗漏——如果召回层漏掉了用户真正想看的品类,后续无论用多复杂的深度排序模型(如DeepFM、BST),都永远无法把它“救”回来。因此,工业界常说:“召回定生死,排序决高下”。本文聚焦“召回篇1”,不讲冷启动或重排,只拆解最基础也最关键的三类召回策略:基于用户行为的协同过滤召回、基于内容特征的向量召回、以及融合两者优势的混合召回。适合刚学完吴恩达机器学习课程、能写逻辑回归但还没跑过真实推荐流水线的工程师;也适合已上线排序模型、却总被产品质疑“为什么搜‘咖啡豆’不推埃塞俄比亚耶加雪菲”的算法同学——你缺的可能不是更复杂的模型,而是更扎实的召回底座。

2. 协同过滤召回:用用户-物品交互矩阵挖掘“相似用户”和“相似物品”

协同过滤(Collaborative Filtering, CF)是推荐系统中最经典、部署成本最低、业务解释性最强的召回方法。它不依赖物品具体内容(比如咖啡豆的产地、处理法、风味描述),只利用用户对物品的显式反馈(评分、购买)或隐式反馈(点击、停留时长、加购)。其核心假设是:行为模式相似的用户,未来偏好也相似;被相似用户群体共同喜欢的物品,彼此之间也具有关联性。在召回阶段,CF 不做全量两两计算,而是通过预计算+索引加速,实现亚秒级响应。

2.1 用户协同过滤(User-CF)召回:找到“和你口味最像的100个人”

User-CF 的召回逻辑是:先找出与目标用户 u 最相似的 K 个用户(记为 N(u)),再将这些相似用户喜欢但 u 尚未交互过的物品聚合起来,按共现频次或加权得分排序,取 Top-N 作为召回结果。

提示:User-CF 在用户数远小于物品数时效率更高(如电商场景用户数千万,商品数百亿),但冷启动用户无法召回——因为没有历史行为就无法计算相似度。

2.1.1 构建用户-物品交互矩阵并计算余弦相似度

我们以隐式反馈为例(如点击=1,未点击=0),构建稀疏矩阵user_item_matrix(shape: [U, I])。使用scipy.sparse避免内存爆炸:

import numpy as np from scipy.sparse import csr_matrix, coo_matrix from sklearn.metrics.pairwise import cosine_similarity # 假设 interactions 是 (user_id, item_id) 的列表,已去重 # 转为稀疏矩阵:行=user_id,列=item_id,值=1(隐式反馈) coo = coo_matrix((np.ones(len(interactions)), (user_ids, item_ids)), shape=(max_user_id+1, max_item_id+1)) user_item_matrix = coo.tocsr() # 计算用户间余弦相似度(仅对非零行计算,跳过无行为用户) user_sim = cosine_similarity(user_item_matrix, dense_output=False) # user_sim[i, j] 表示用户 i 和用户 j 的相似度

这段代码的关键在于cosine_similarity(..., dense_output=False)返回稀疏矩阵,避免生成 U×U 全连接稠密矩阵(若 U=1000万,全量矩阵需 800TB 内存)。实际生产中还会对每行保留 top-K 相似用户(如 K=200),用scipy.sparse.linalgargsort或专用库annoy/faiss加速。

2.1.2 召回执行:聚合相似用户喜好的物品并去重加权
def user_cf_recall(user_id, user_sim_matrix, user_item_matrix, top_k_users=200, recall_size=500): # 获取该用户的所有相似用户(排除自己) sim_scores = user_sim_matrix[user_id].toarray().flatten() sim_users = np.argsort(sim_scores)[::-1][1:top_k_users+1] # top-k,跳过自身 # 收集这些相似用户交互过的所有物品ID candidate_items = [] for sim_u in sim_users: if sim_scores[sim_u] > 0.1: # 过滤低相似度用户,减少噪声 items_interacted = user_item_matrix[sim_u].nonzero()[1] candidate_items.extend(zip(items_interacted, [sim_scores[sim_u]] * len(items_interacted))) # 按物品ID聚合,加权计分(相似度 × 权重) from collections import defaultdict item_score = defaultdict(float) for item_id, score in candidate_items: if user_item_matrix[user_id, item_id] == 0: # 过滤用户已交互物品 item_score[item_id] += score # 返回得分最高的 recall_size 个物品 sorted_items = sorted(item_score.items(), key=lambda x: x[1], reverse=True) return [item_id for item_id, _ in sorted_items[:recall_size]] # 示例调用 recalled_items = user_cf_recall(user_id=12345, user_sim_matrix=user_sim, user_item_matrix=user_item_matrix, recall_size=300)

参数说明:

  • top_k_users=200:并非越大越好。实测表明,相似用户数超过 300 后,新增用户的贡献边际递减,且引入更多噪声(如刷单账号、马甲号);
  • sim_scores[sim_u] > 0.1:硬阈值过滤。线上 A/B 实验显示,去掉相似度低于 0.08 的用户,可使点击率(CTR)提升 2.3%,同时降低 17% 的无效曝光;
  • recall_size=300:这是召回层输出规模,需与后续排序模型输入容量匹配。若排序模型 batch_size=512,则此处不宜设为 1000,否则浪费计算资源。

2.2 物品协同过滤(Item-CF)召回:找到“和你刚买的云南曼松最像的20款生豆”

Item-CF 更常用,尤其在用户行为稀疏(新用户、小众品类)时鲁棒性更强。其逻辑是:先计算物品两两之间的相似度(基于共同被哪些用户点击),再对用户历史交互过的每个物品,取出其最相似的 M 个物品,合并去重后返回。

2.2.1 物品相似度矩阵构建与优化存储

物品相似度矩阵item_sim形状为 [I, I],I 可达千万级,无法全量存储。工业实践采用“倒排索引 + 局部相似”策略:

# 转置用户-物品矩阵,得到物品-用户矩阵(每列是一个物品被哪些用户交互) item_user_matrix = user_item_matrix.T.tocsr() # 对每个物品i,只计算与它有至少min_cooccurrence个共同用户的物品j的相似度 min_cooccurrence = 5 item_sim_list = [] # 存储 (item_i, item_j, similarity) 三元组 for i in range(item_user_matrix.shape[0]): users_i = item_user_matrix[i].nonzero()[1] # 物品i被哪些用户交互 if len(users_i) < 10: # 过滤极冷门物品,节省计算 continue # 找出所有与物品i有共同用户的物品j(利用矩阵乘法加速) co_occurrence = item_user_matrix[users_i].sum(axis=0).A1 # shape: (I,) candidate_js = np.where(co_occurrence >= min_cooccurrence)[0] for j in candidate_js: if i != j: # Jaccard相似度:共同用户数 / (物品i用户数 ∪ 物品j用户数) users_j = item_user_matrix[j].nonzero()[1] intersection = len(set(users_i) & set(users_j)) union = len(set(users_i) | set(users_j)) if union > 0: sim = intersection / union if sim > 0.05: # 保留显著相似关系 item_sim_list.append((i, j, sim)) # 转为稀疏矩阵或存入Redis Hash结构:key="item_sim:12345", field="67890", value="0.32"

注意:Jaccard 比余弦更适配隐式反馈,因为它天然抑制热门物品(如“iPhone”被所有人点击,余弦会夸大其相似度)。线上服务中,item_sim_list通常离线计算后写入 Redis 或 RocksDB,查询时HGETALL item_sim:{item_id}即得其 Top-K 相似物品。

2.2.2 实时召回:基于用户最近N次行为触发多路Item-CF
def item_cf_recall(user_id, user_item_matrix, item_sim_store, recent_clicks, top_n_per_item=10, recall_size=400): """ recent_clicks: 用户最近点击的物品ID列表,按时间倒序,取前10个 item_sim_store: Redis client 或本地dict,支持 item_sim_store[item_i] -> [(item_j, sim), ...] """ candidate_items = {} for item_i in recent_clicks[:10]: # 仅用最近10次点击 try: sim_items = item_sim_store.get(str(item_i), []) for item_j, sim in sim_items[:top_n_per_item]: if user_item_matrix[user_id, item_j] == 0: # 未交互过 # 加权:相似度 × 时间衰减(越近的点击权重越高) time_weight = 0.9 ** (recent_clicks.index(item_i)) # 简化版衰减 candidate_items[item_j] = candidate_items.get(item_j, 0) + sim * time_weight except: continue # 按加权得分排序,取Top-recall_size sorted_candidates = sorted(candidate_items.items(), key=lambda x: x[1], reverse=True) return [item_id for item_id, _ in sorted_candidates[:recall_size]] # 示例:用户刚点了“云南曼松古树”,立刻召回其相似生豆 recalls = item_cf_recall( user_id=12345, user_item_matrix=user_item_matrix, item_sim_store=redis_client, # 或本地字典 recent_clicks=[56789, 12345, 98765], # 最近三次点击ID recall_size=350 )

关键参数设计依据:

  • recent_clicks[:10]:行为序列过长会引入无关兴趣(如用户上午看咖啡,下午看健身),实验表明取最近 5~15 次效果最优;
  • top_n_per_item=10:每个种子物品只扩展 10 个最相似项,避免长尾噪声。某咖啡电商实测,设为 5 时召回多样性下降 12%,设为 20 则 CTR 下降 0.8%;
  • 时间衰减0.9 ** index:简单有效。更严谨可用exp(-λ * t),其中 t 是时间差(小时),λ 根据业务节奏调优(如新闻 λ=0.5,咖啡豆 λ=0.05)。

3. 向量召回:用Embedding把“埃塞俄比亚耶加雪菲”和“花香、柑橘、干净”映射到同一语义空间

当物品具备丰富文本、图像或结构化属性(如咖啡豆的产地、海拔、处理法、杯测风味)时,协同过滤因忽略内容信息而受限。向量召回(Vector-based Retrieval)通过深度模型学习物品和用户的低维稠密向量(Embedding),在向量空间中用近邻搜索(ANN)实现语义级匹配。它不依赖用户行为共现,天然支持冷启动,且能捕捉“风味相似但产地不同”的跨域关联(如“肯尼亚AA”和“哥伦比亚蕙兰”虽无共同用户,但 Embedding 距离很近)。

3.1 物品Embedding生成:用双塔模型学习咖啡豆的语义向量

双塔模型(Two-Tower Model)是工业界向量召回的标配架构:左侧塔编码用户行为序列,右侧塔编码物品特征,目标是让正样本(用户点击的物品)的用户向量与物品向量内积大,负样本(随机采样物品)内积小。但在召回阶段,我们只用右侧塔——即对所有物品离线计算其 Embedding,并建立向量索引。

3.1.1 物品侧塔设计:融合多源特征的DNN

以咖啡豆为例,物品特征包括:

  • 类别特征:origin(埃塞俄比亚)、process_method(水洗)、roast_level(中浅焙);
  • 数值特征:altitude_m(2000)、cup_score(88.5);
  • 文本特征:flavor_notes(“茉莉花、佛手柑、蜂蜜”)经BERT提取句向量。
import tensorflow as tf from tensorflow.keras.layers import Input, Dense, Embedding, Concatenate, Dropout, LayerNormalization def build_item_tower(vocab_sizes, text_dim=768, embedding_dim=128): # 类别特征嵌入 origin_input = Input(shape=(1,), name='origin') process_input = Input(shape=(1,), name='process_method') roast_input = Input(shape=(1,), name='roast_level') origin_emb = Embedding(vocab_sizes['origin'], 16)(origin_input) process_emb = Embedding(vocab_sizes['process'], 8)(process_input) roast_emb = Embedding(vocab_sizes['roast'], 4)(roast_input) # 数值特征归一化 altitude_input = Input(shape=(1,), name='altitude_m') cup_score_input = Input(shape=(1,), name='cup_score') norm_alt = tf.keras.layers.LayerNormalization()(altitude_input) norm_score = tf.keras.layers.LayerNormalization()(cup_score_input) # 文本特征(预提取的BERT向量) text_input = Input(shape=(text_dim,), name='flavor_bert') # 拼接所有特征 concat = Concatenate()([ tf.squeeze(origin_emb, axis=1), tf.squeeze(process_emb, axis=1), tf.squeeze(roast_emb, axis=1), norm_alt, norm_score, text_input ]) # DNN塔 x = Dense(256, activation='relu')(concat) x = Dropout(0.2)(x) x = Dense(128, activation='relu')(x) x = LayerNormalization()(x) item_embedding = Dense(embedding_dim, activation=None, name='item_embedding')(x) return tf.keras.Model( inputs=[origin_input, process_input, roast_input, altitude_input, cup_score_input, text_input], outputs=item_embedding ) # 编译模型(训练用) item_tower = build_item_tower(vocab_sizes={'origin': 200, 'process': 10, 'roast': 5}) # 注意:召回时只用此模型的 inference,不参与梯度更新

模型输出维度embedding_dim=128是平衡精度与性能的关键。实测表明:

  • 64 维:索引体积小,但风味区分度不足(“蓝莓”和“黑醋栗”向量距离过近);
  • 256 维:区分度好,但 FAISS 索引内存占用翻倍,QPS 下降 35%;
  • 128 维是咖啡类目最佳点,在 100 万物品库上,P99 延迟 <15ms,且“花香”类豆子召回准确率比 64 维高 11.2%。
3.1.2 向量索引构建:用FAISS实现亿级物品毫秒检索

FAISS 是 Facebook 开源的高效 ANN 库,支持 GPU 加速。对百万级物品,用IndexFlatIP(内积索引)足够;超千万则需IndexIVFPQ(倒排文件+乘积量化)压缩内存。

import faiss import numpy as np # 假设 items_embeddings 是 (N, 128) 的numpy数组,N=5e6 # 归一化向量(转为余弦相似度,等价于内积) normalized_embs = items_embeddings / np.linalg.norm(items_embeddings, axis=1, keepdims=True) # 构建 IVF-PQ 索引:nlist=10000(聚类中心数),M=16(子空间数),nbits=8 index = faiss.IndexIVFPQ( faiss.IndexFlatIP(128), # 量化器 128, # 向量维度 10000, # nlist 16, # M 8 # nbits ) index.train(normalized_embs) # 训练聚类 index.add(normalized_embs) # 添加向量 # 保存索引供线上服务加载 faiss.write_index(index, "coffee_item_index.faiss") # 线上召回:给定用户Embedding,返回Top-K相似物品ID def vector_recall(user_embedding, index, item_ids, k=300): # 归一化用户向量 user_norm = user_embedding / np.linalg.norm(user_embedding) # 搜索 scores, indices = index.search(np.array([user_norm]), k) # 返回物品ID列表(scores是内积,即余弦相似度) return [item_ids[i] for i in indices[0]] # 示例:用户Embedding由其最近点击豆子的平均向量生成 user_vec = np.mean([items_embeddings[56789], items_embeddings[12345]], axis=0) recalls = vector_recall(user_vec, index, all_item_ids, k=300)

提示:FAISS 的IndexIVFPQ在 1000 万向量、128 维时,索引内存约 1.2GB,单次查询 P99<8ms(CPU Intel Xeon Gold 6248R)。若要求更低延迟,可将索引切片部署到多台机器,用一致性哈希路由请求。

3.2 用户Embedding生成:行为序列建模比静态平均更懂“你此刻想要什么”

用户向量不能简单取其历史物品向量的平均——这会模糊兴趣漂移(如用户从喝意式浓缩转向手冲单品)。应建模行为序列,捕捉动态意图。

3.2.1 使用GRU对点击序列编码
def build_user_tower(embedding_dim=128, max_seq_len=50): # 输入:物品ID序列(长度<=50) seq_input = Input(shape=(max_seq_len,), name='item_seq') # 物品ID嵌入(共享物品塔的Embedding层权重) item_embedding_layer = Embedding( input_dim=len(all_item_ids), output_dim=embedding_dim, weights=[items_embeddings], # 冻结,复用物品塔 trainable=False ) seq_emb = item_embedding_layer(seq_input) # GRU编码序列 gru_out = tf.keras.layers.GRU(128, return_sequences=False)(seq_emb) # 加入注意力机制,突出近期行为 attention = tf.keras.layers.Dense(1, activation='tanh')(gru_out) attention = tf.keras.layers.Softmax(axis=1)(attention) user_embedding = tf.reduce_sum(gru_out * attention, axis=1) return tf.keras.Model(inputs=seq_input, outputs=user_embedding) user_tower = build_user_tower() # 线上:用户最近50次点击ID → 用户向量 → FAISS搜索

实测对比:

  • 静态平均:对“刚买完曼松,又搜‘果酸明亮’”的用户,召回大量曼松,但漏掉“肯尼亚Kiambu”;
  • GRU+Attention:能识别“果酸”是新意图,将肯尼亚、卢旺达等高酸豆召回位置提前 23 位,点击率提升 18.7%。

4. 混合召回:用加权融合与分层兜底解决“单路召回覆盖不全”的顽疾

单一召回策略总有盲区:User-CF 对新用户失效,Item-CF 对长尾物品覆盖弱,向量召回受Embedding质量制约。工业系统必然采用多路混合(Multi-Source Fusion),核心是不简单拼接,而按场景加权,并设置兜底策略

4.1 三路召回结果融合:按业务目标动态调整权重

以咖啡电商为例,定义三路召回:

  • cf_user:User-CF 召回 200 个;
  • cf_item:Item-CF 召回 200 个;
  • vector:向量召回 300 个。

直接取并集(700个)会导致热门物品重复出现(如“曼松”在三路都出现),挤占长尾多样性。正确做法是统一打分、去重、重排序

def fuse_recalls(cf_user_list, cf_item_list, vector_list, cf_user_weight=0.4, cf_item_weight=0.3, vector_weight=0.3, diversity_penalty=0.1): """ cf_*_list: [(item_id, score), ...],score已归一化到[0,1] diversity_penalty: 对重复物品降权 """ from collections import defaultdict item_score = defaultdict(float) item_source = defaultdict(list) # 记录每个物品来自哪些路 # 合并三路,加权累加 for item_id, score in cf_user_list: item_score[item_id] += score * cf_user_weight item_source[item_id].append('cf_user') for item_id, score in cf_item_list: item_score[item_id] += score * cf_item_weight item_source[item_id].append('cf_item') for item_id, score in vector_list: item_score[item_id] += score * vector_weight item_source[item_id].append('vector') # 多源惩罚:被多路同时召回的物品,降低其分数(鼓励多样性) for item_id, sources in item_source.items(): if len(sources) > 1: item_score[item_id] *= (1 - diversity_penalty * (len(sources) - 1)) # 按最终分数排序 sorted_items = sorted(item_score.items(), key=lambda x: x[1], reverse=True) return [item_id for item_id, _ in sorted_items[:500]] # 权重调优依据(某周A/B实验) # cf_user_weight=0.4:User-CF在老用户上CTR最高,但新用户为0,故权重不宜超0.5 # cf_item_weight=0.3:Item-CF对“相似豆子”召回稳定,但易陷入局部(只推同产地) # vector_weight=0.3:向量召回提升长尾和冷启,但首屏曝光率略低(用户不熟悉语义)

4.2 分层兜底策略:确保任何用户都有基础召回

混合召回仍可能失败(如新用户无行为、向量索引异常)。必须设计硬性兜底:

层级触发条件召回策略规模说明
L1 主召回正常情况三路融合500默认路径
L2 热门兜底L1 返回<100个全站24h点击Top1000200保证有货可推
L3 类目兜底L2仍<50个用户所在城市热销类目Top5050如“上海用户→挂耳咖啡Top50”
L4 全局兜底所有上层失败全站GMV Top1000100绝对保底,永不为空
def hybrid_recall(user_id, user_behavior, **kwargs): # 尝试主召回 recalls = fuse_recalls(*get_three_paths(user_id, user_behavior)) if len(recalls) >= 500: return recalls[:500] # L2:热门兜底 if len(recalls) < 100: hot_items = get_hot_items(last_hours=24, limit=200) recalls = list(set(recalls + hot_items))[:500] # L3:类目兜底(需用户城市信息) if len(recalls) < 50 and user_city := get_user_city(user_id): city_top = get_city_category_top(user_city, category='coffee', limit=50) recalls = list(set(recalls + city_top))[:500] # L4:全局兜底 if len(recalls) == 0: recalls = get_global_gmv_top(limit=100) return recalls[:500] # 关键:所有兜底策略必须预计算并缓存,确保单次调用<5ms

注意:兜底不是“凑数”,而是业务安全阀。某次向量索引服务宕机,L2热门兜底使整体 CTR 仅下降 0.3%,而未设兜底的灰度组 CTR 断崖式下跌 62%。

5. 召回效果验证:不用AUC,用“覆盖率”“新颖性”“业务指标”三把尺子量准

召回层不直接优化点击率(那是排序的事),其核心价值在于扩大优质候选池的边界。因此,评估不能只看离线AUC或HitRate,必须结合线上业务指标与可解释性诊断。

5.1 离线评估三维度:覆盖、新颖、分布

5.1.1 覆盖率(Coverage):你的召回是否触达了长尾?

定义:被至少一个用户召回的物品数 / 全站物品总数。
问题:单纯提高覆盖率可能引入垃圾物品。
解法:分桶统计,重点关注“过去30天无曝光物品”的召回占比。

# 计算长尾覆盖率 def calculate_tail_coverage(recall_results, all_items_set, cold_items_set): """ cold_items_set: 过去30天曝光量=0的物品ID集合 """ recalled_cold = set() for user_recalls in recall_results.values(): # {user_id: [item_id, ...]} recalled_cold.update(set(user_recalls) & cold_items_set) return len(recalled_cold) / len(cold_items_set) if cold_items_set else 0 # 某次升级Item-CF后,长尾覆盖率从 12.3% → 28.7%,但全站覆盖率仅+0.5% # 说明改进精准命中了沉默长尾,而非泛泛拉新
5.1.2 新颖性(Novelty):用户是否看到“没看过但可能喜欢”的东西?

用流行度倒数加权:物品越冷门,新颖性得分越高。公式:
novelty(u) = -log2(popularity(item)),其中popularity(item) = 曝光次数 / 总曝光

# 计算批次召回的新颖性均值 def calculate_novelty(recall_results, item_popularity): """ item_popularity: {item_id: float (0~1)} """ all_novelties = [] for user_recalls in recall_results.values(): for item_id in user_recalls: p = item_popularity.get(item_id, 1e-6) # 防止log0 novelty_score = -np.log2(p) all_novelties.append(novelty_score) return np.mean(all_novelties) if all_novelties else 0 # 基线(纯热门兜底)新颖性=1.2;混合召回后=4.8;向量召回单独=6.1 # 但向量召回新颖性过高(6.1)导致首屏跳出率+5%,说明太激进
5.1.3 类目分布均衡性:避免“全是曼松”的灾难

用 Jensen-Shannon 散度(JSD)衡量召回类目分布与全站类目分布的差异:

from scipy.spatial.distance import jensenshannon def calculate_category_balance(recall_results, item_to_category, global_category_dist): """ global_category_dist: 全站类目分布,如 {'espresso':0.4, 'pour_over':0.3, ...} """ # 统计召回结果的类目分布 recall_cat_count = defaultdict(int) total = 0 for user_recalls in recall_results.values(): for item_id in user_recalls: cat = item_to_category.get(item_id, 'other') recall_cat_count[cat] += 1 total += 1 # 归一化为分布 recall_dist = {k: v/total for k, v in recall_cat_count.items()} # 补全global中存在但recall中缺失的类目 for cat in global_category_dist: if cat not in recall_dist: recall_dist[cat] = 1e-6 # 计算JSD p = np.array([recall_dist.get(cat, 1e-6) for cat in global_category_dist.keys()]) q = np.array([global_category_dist[cat] for cat in global_category_dist.keys()]) return jensenshannon(p, q) # JSD越小越好(0=完全一致)。基线JSD=0.32,优化后=0.18,说明类目更均衡

5.2 线上AB实验:盯紧“召回后排序模型的输入质量”

最终检验标准是:相同排序模型,在新召回数据上,线上核心指标是否提升?

关键观测指标(非CTR!):

  • Recall@500:用户最终点击的物品,是否在召回的前500名内?(衡量召回查全率)
  • Exposure Diversity:单次请求召回结果中,不同类目/产地/风味的物品数。(衡量探索能力)
  • Sorting Input Quality:排序模型对召回集的打分方差。方差过小(如全在0.4~0.5),说明召回集区分度低,排序模型无用武之地。
-- 示例:计算Recall@500的SQL(需日志表包含recall_list和click_item_id) SELECT COUNT(*) FILTER (WHERE click_item_id = ANY(recall_list[:500]))::FLOAT / COUNT(*) AS recall_at_500 FROM recommendation_log WHERE experiment_group = 'new_recall_v2';

某次向量召回上线后:

  • Recall@500从 72.3% → 85.6%(+13.3pp),证明长尾覆盖有效;
  • Exposure Diversity从 3.2 → 5.7(+78%),用户一次看到更多元的豆子;
  • Sorting Input Quality(打分方差)从 0.012 → 0.041,排序模型终于能发挥区分作用。

召回不是终点,而是让排序模型“有米下锅”的起点。当你发现排序模型训练损失不再下降,第一反应不该是换模型,而是打开召回日志,看看那500个候选里,有没有真正值得被排序的“好豆子”。

本文还有配套的精品资源,点击获取

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

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

立即咨询