推荐系统学习笔记1:推荐系统基础

张开发
2026/4/14 22:45:29 15 分钟阅读

分享文章

推荐系统学习笔记1:推荐系统基础
评价推荐系统的指标消费指标包括点击率、曝光率、收藏率、转发率、阅读完成率等这些都是短期指标更重要的是北极星指标North Star Metric。北极星指标包括日活用户数DAU、月活用户数MAU、人均使用推荐的时长、人均阅读笔记的数量、发布渗透率、人均发布量等。其中发布渗透率指的是发布过内容的人数与总活跃人数的比值用于衡量用户从“围观者”转变为“创作者”的比例。实验流程首先进行离线实验收集历史数据在历史数据上做训练、测试快速迭代模型参数。通过 AUC、准确率、召回率等数学指标筛选出表现最好的几个算法版本。算法没有部署到产品中没有跟用户交互。随后进行小流量 A/B 测试将小流量池分为 A 组原算法和 B 组新算法观察两者在点击率、留存、转化等指标上的差异。最后进行全流量上线在确认小流量实验效果显著且稳定后将新算法推广给全站用户。推荐系统的链路推荐系统的目标是从几亿个物品中选出几十个物品展示给用户推荐系统的链路可分为四个环节召回Recall快速从几亿篇笔记中快速召回用户感兴趣的几千篇笔记常见的召回通道包括协同过滤、双塔模型等。粗排Pre-Ranking使用小规模神经网络进行打分将召回的几千篇笔记缩小到几百篇。精排Ranking使用大规模神经网络输入更加丰富的特征对几百篇笔记进行综合打分。重排Re-ranking从精排出的几百篇中选出最终展示的几十篇为了防止用户刷到的全是同类内容系统进行多样性打分打散相似内容同时在这里插入广告、运营推广内容根据规则调整顺序。A/B 测试A/B 测试的基本流程所有对模型和策略的改进都需要通过 A/B 测试用实验数据来验证模型和策略是否有效。例如观察新的召回通道对线上指标的影响、GNN 深度的取值选择。随机分桶利用哈希函数将用户 ID 映射成整数然后均匀随机分成b bb个桶例如 10 个桶每个桶占 10% 用户。这里的分桶使用哈希函数而不是直接进行抽样是因为如果简单地给用户抽样系统必须维护一张巨大的表格来记录“用户 A 在 1 号桶用户 B 在 2 号桶…”这在亿级用户面前极难维护的。对于哈希函数而言只要用户 ID 不变算出来的哈希值永远一致。同层互斥与不同层正交同层互斥在同一层中100% 的流量被划分为多个不重叠的桶如果一个用户进入了实验 A他就绝对不会进入实验 B。假设现在需要测试两个互相冲突的策略实验 A 是把按钮改红实验 B 是把同一个按钮改绿。一个用户不能同时看到红绿按钮所以它们必须在同一层中互斥。不同层正交流量在进入每一层时都会被重新洗牌。第一层桶 0 的用户在进入第二层时会均匀分布在第二层的 0-99 个桶中。假设现在需要测试不相关的策略时第一层测试搜索算法第二层测试底栏图标颜色。通过正交可以让搜索实验和 UI 实验同时进行且互不干扰。对于同类的策略而言如精排模型的两种结构它们天然互斥对于一个用户只能用其中一种再如添加两条召回通道效果会增强或相互抵消互斥可以避免同类策略相互干扰。不同类型的策略如添加召回通道和优化粗排模型它们通常不会相互干扰可以作为正交的两层。仿真代码importmmh3importpandasaspdimportnumpyasnpimportuuidimportrandomclassTrafficSharding:def__init__(self,num_buckets1000,layers3):self.num_bucketsnum_buckets# 为每一层生成独立的 Salt确保正交self.layer_salts[]foriinrange(layers):# 生成一个 0 到 10000 之间的随机数增加随机性random_numrandom.randint(0,10000)# 把层数编号 (i) 和 随机数 组合成一个盐值这样不同层的哈希输入不同# 比如第一层可能是 layer_0_842第二层是 layer_1_5531salt_labelflayer_{i}_{random_num}self.layer_salts.append(salt_label)defget_bucket(self,user_id,layer_index0):核心哈希分桶函数# 选择对应层的盐值saltself.layer_salts[layer_index]# 用户user_id与盐值组合成哈希输入keystr(user_id)salt# 计算哈希值hash_valmmh3.hash(key,signedFalse)# 取模得到桶编号returnhash_val%self.num_bucketsdefrun_simulation(num_users100000):print(f 开始仿真模拟{num_users}名用户进入正交分层系统...)sharderTrafficSharding(num_buckets100,layers2)# 100个桶2层实验user_ids[]# 存放所有用户的IDforiinrange(num_users):random_identityuuid.uuid4()# 随机数生成器user_id_stringstr(random_identity)# 转换为字符串user_ids.append(user_id_string)results[]foruinuser_ids:# 模拟用户在两个独立实验层中的分桶bucket_l1sharder.get_bucket(u,layer_index0)bucket_l2sharder.get_bucket(u,layer_index1)results.append({user_id:u,layer_1_bucket:bucket_l1,layer_2_bucket:bucket_l2})dfpd.DataFrame(results)print(\n✅ 分层分桶结果预览)print(df.head(5))# --- 1. 均匀性校验 ---print(\n 1. 均匀性校验 (Layer 1):)column_datadf[layer_1_bucket]# 取出 Layer 1 的分桶结果raw_countscolumn_data.value_counts()# 计算每个桶的用户数total_userslen(df)# 总用户数proportionsraw_counts/total_users# 计算每个桶的占比countsproportions.head(5)print(f前5个桶的流量占比:\n{counts})# --- 2. 正交性校验 ---# 原理如果在 Layer 1 处于桶 0 的用户在 Layer 2 均匀分布在 0-99 桶中则说明正交print(\n 2. 正交性校验:)is_in_bucket_0df[layer_1_bucket]0# 筛选出 Layer 1 在桶 0 的用户subset_l1_bucket0df[is_in_bucket_0]# 取出这些用户l2_column_for_these_peoplesubset_l1_bucket0[layer_2_bucket]# 取出他们在 Layer 2 的分桶counts_in_l2l2_column_for_these_people.value_counts()# 统计这些用户在 Layer 2 各桶的数量total_people_in_subsetlen(subset_l1_bucket0)proportions_in_l2counts_in_l2/total_people_in_subset# 计算占比l2_dist_in_l1_zeroproportions_in_l2.head(5)print(f当 Layer 1 固定在桶 0 时这些用户在 Layer 2 的分布:\n{l2_dist_in_l1_zero})# --- 3. 模拟实验效应计算 (含简单的 CUPED 思想) ---# 假设 Layer 1 正在做一个策略前 50 个桶是对照组后 50 个是实验组df[group]np.where(df[layer_1_bucket]50,control,treatment)# 模拟用户指标 (Baseline 实验提升 随机噪音)df[metric_pre]np.random.normal(100,20,num_users)# 实验前数据lift2.0# 假设实验组真有 2.0 的提升df[metric_post]df[metric_pre]np.where(df[group]treatment,lift,0)np.random.normal(0,5,num_users)print(\n 3. 实验结果初步观测:)summarydf.groupby(group)[metric_post].mean()print(summary)print(f观测到的提升:{summary[treatment]-summary[control]:.4f})if__name____main__:run_simulation()输出结果 开始仿真模拟100000名用户进入正交分层系统...✅ 分层分桶结果预览 user_id layer_1_bucket layer_2_bucket09ff5d564-3c2d-4c81-8b7a-0ecc1c9825a42024135bd6cf2-2eda-4b8c-8848-ac7abce1cfd180292b1086495-3ed3-4816-a1bb-885064aed80c3632327bffaf2-4403-4b81-86fb-ea49568674ca313544776cecd-b414-4fe9-b975-c7ba0d459c9e26701.均匀性校验(Layer1):前5个桶的流量占比:layer_1_bucket40.01074180.01063320.01062820.01055650.01052Name:count,dtype:float64 2.正交性校验:当 Layer1固定在桶0时这些用户在 Layer2的分布:layer_2_bucket780.022845210.020768950.019730570.016615500.015576Name:count,dtype:float64 3.实验结果初步观测:group control100.114321treatment101.810012Name:metric_post,dtype:float64 观测到的提升:1.6957Holdout 机制在推荐系统的全链路中每个小实验都会说有提升。但由于实验之间存在正交重叠这些提升不能简单相加。为了看清整个部门的成果将全站流量划分为两个绝对互斥的部分10% 的 Holdout 桶这部分用户被“冷冻”在上一周期最稳健的版本不参与本周期的任何新实验。90% 的实验桶这部分用户参与召回、排序等所有链路的新策略迭代。公司考核的是推荐系统对业务的净增贡献。计算公式通常如下部门总体收益 (Lift) 实验桶指标 − Holdout 桶指标 Holdout 桶指标 × 100 % \text{部门总体收益 (Lift)} \frac{\text{实验桶指标} - \text{Holdout 桶指标}}{\text{Holdout 桶指标}} \times 100\%部门总体收益(Lift)Holdout桶指标实验桶指标−Holdout桶指标​×100%由于实验桶和 Holdout 桶的人数不对等不能比绝对值因此需要归一化。在周期开始时两组指标的 Diff 应该趋于 0。此时的任何差异都是随机误差随着实验上线Diff 的拉大才是真正的技术红利。为了公平考核并避免“老用户效应”即长期不更新策略可能导致用户流失或行为偏差系统会按周期如每季度或半年进行重启推全与释放周期结束证明有效的策略全部固化应用到 100% 的用户身上。此时旧的 Holdout 桶消失。重新洗牌利用哈希算法重新打散用户随机抽取新的 10% 作为新一轮的 Holdout。基准对齐新周期第一天由于 100% 的用户用的都是同样的策略Diff 回归到 0 附近。复利增长随着新一轮召回、排序实验的滚动上线实验桶的指标开始再次爬坡形成新的 Diff 曲线。实验推全与反转实验在小流量 A/B 测试中观察到显著的 diff 增长则可以推全。例如先在 20% 的用户上测试后如果有效就把这个实验关闭将桶空出来之后再新开一层。在推全后因为作用的用户从 20% 提升到了 90%因此 diff 也会扩大。推全后有的指标会马上发生变化比如点击、交互等指标会立刻受到新策略影响但留存率等指标有滞后性需要长期观测。实验观测到显著收益后尽快推全新策略。目的是腾出桶供其他实验使用或需要基于新策略做后续的开发。用反转实验解决上述矛盾既可以尽快推全也可以长期观测实验指标。具体做法是在推全的新层中开一个旧策略的桶长期观测实验指标。参考资料[1] 推荐系统公开课——王树森[2] 王树森推荐系统学习笔记1概要[3] 深度解析A/B测试中的哈希分桶策略从原理到实战的流量分层方案

更多文章