ARTICLE DETAIL

资讯详情

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

推荐算法实战精讲:从协同过滤到FM的选型与落地

推荐算法实战精讲:从协同过滤到FM的选型与落地 聊推荐算法绕不开一个很现实的问题你接触到的第一个推荐需求大概率不是让你从零训练一个大模型而是让你把一段“没带脑子”的内容流列表改成有点个性识别度的东西。我刚接到这类需求时第一反应是去翻各种前沿论文结果被一堆模型架构和训练 trick 绕晕了。后来踩了几年坑回头看发现真正在业务里能打的核心永远是那几类“经典算法”协同过滤UserCF、ItemCF、矩阵分解、逻辑回归排序、以及后来的 FM 等浅层模型。它们看着不惊艳但能解决绝大多数中小产品的推荐诉求深度学习模型也大多是站在它们肩膀上演进的。这篇文章不讲花活就按“原理 计算过程 适用场景 实操坑点”把几种常用推荐算法串一遍尽量让没深入做过推荐系统的人也能看懂用得上。文中涉及的示例代码都是可跑通的最小实现场景也是业务里出现频率最高的——你拿去本地跑一遍再映射到自己的数据上思路会顺很多。1. 先想清楚再动手推荐算法怎么选在动手写第一行代码之前我更建议你先花半天时间想清楚一个框架问题推荐系统在业务里到底解决什么绝大多数推荐场景本质是解决“信息过载下的人货匹配效率”。用户来了你得在他没有明确表达需求时把高概率能让他产生停留、点击、购买、观看等行为的内容推到他面前。因此算法选型的核心依据不是“哪个模型听起来高级”而是“你的业务数据形态和阶段适合哪种计算方式”。我把推荐系统在工程上的实现路径拆成三层来理解这个分层直接决定了算法的分工召回层从百万甚至亿级候选里快速圈定几百条用户可能感兴趣的物品。常用手段是 ItemCF、UserCF、向量检索、规则召回等要求是快、覆盖广。排序层把召回的候选集合进行精准打分排序常用的有逻辑回归、FM、GBDTLR以及各类深度排序模型要求是准。重排与策略层处理多样性、去重、商业规则、新鲜度、分页截断等常用的是规则和轻量策略模型。大多数推荐系统部署后出现的“算法效果不好”往上追根溯源往往不是排序模型不够厉害而是召回源太少或者召回内容本身质量差导致精排模型“巧妇难为无米之炊”。所以我理解算法的思路时从来不会只盯着某个模型本身而是先看它属于哪一层、解决的又是什么问题。还有个容易被忽视的点算法选型要匹配数据规模。冷启动产品一天只有几百个用户用深度学习排序意义不大因为样本量撑不起复杂模型的训练反过来百万用户亿级行为样本时你还用最基础的规则排序那可能连“猜猜你可能喜欢”的基本面都撑不住。定义一个合适的起点比一步到位重要得多。我在实际项目里最开始做的往往是“数据质量盘点 基础规则召回 逻辑回归排序”等数据积累到一定程度再逐步替换成矩阵分解和向量召回。这套演进路径看似保守但稳。2. 几种主流推荐算法的原理与实现细节进入正题这章我把业务里真正高频使用的几类算法逐一说透。每个算法我都按“一句话理解 - 计算逻辑 - 代码骨架 - 优缺点 - 适合场景”的结构讲方便你对照自己的业务去判断。2.1 基于流行度与统计规则的算法简单但永远有用很多人看不上基于统计的算法觉得没有“智能感”。但我在生产环境里见过太多基础版本冷启动阶段流行度推荐几乎是最稳妥的方案。它的核心逻辑很简单将一段时间内点击量、销量、播放量最高的物品直接推给所有人。不用训练不用特征工程一个 SQL 就能跑出来。# 伪代码按最近7天点击量取TopN item_scores df.groupby(item_id)[click_cnt].sum() top_items item_scores.sort_values(ascendingFalse).head(50).index.tolist()统计规则里再往前走一步可以引入关联规则推荐。核心是先找用户买了A之后后面容易买B的规律这种算法在电商结算页很常见典型的做法是计算物品之间的置信度和提升度。举个例子Apriori 算法里面花功夫最多的部分就是找频繁项集。在实际业务做推荐时候我不会对全量订单频繁挖全项集那样计算量太大多数场景只用“二元共现”就够了统计物品对在同一用户行为序列中出现的次数找出“购买X的人还常买Y”这样的关系。# 简化版统计物品共现次数 cooccurrence defaultdict(int) for user, group in user_behaviors.groupby(user_id): items group[item_id].tolist() for i in range(len(items) - 1): for j in range(i 1, len(items)): cooccurrence[(items[i], items[j])] 1统计类算法的最大价值是给用不到复杂模型的产品兜底尤其是新上线、数据稀疏的推荐位。它的缺点也很明显没有个性化头部效应严重相关性强的长尾内容几乎没有机会露头。所以这类算法在系统里的定位通常是“保底策略”而非常态主推。2.2 协同过滤推荐系统的经典灵魂协同过滤是理解推荐算法绕不开的基石也是面试题里的常客。一句话概括它物以类聚、人以群分。它分 UserCF基于用户的协同过滤和 ItemCF基于物品的协同过滤两派。UserCF 的逻辑是找一群和你品味相似的人把这些人喜欢的、但你还没见过的物品推荐给你。计算步骤拆开看就三步构建“用户-物品”行为矩阵用余弦相似度或皮尔逊相关系数计算用户之间的相似度拿相似用户喜欢的物品减去你已消费过的按加权评分倒排取TopN。ItemCF 的逻辑则反过来先判断物品之间的相似度再把你曾经喜欢的物品的相似物品推荐给你。比如你在电商平台看过一台单反系统给你推荐另一款镜头用的就是物品相似逻辑。def item_cf_recommend(user_items, item_sim, user_id, top_k10): user_items: dict, {user_id: set(item_ids)} item_sim: dict, {item_id: {item_id: sim_score}} interacted_items user_items[user_id] rank {} for item in interacted_items: for sim_item, score in item_sim[item].items(): if sim_item in interacted_items: continue rank.setdefault(sim_item, 0) rank[sim_item] score return sorted(rank.items(), keylambda x: x[1], reverseTrue)[:top_k]理解 ItemCF 和 UserCF 的关键差异对选型很关键。在电商、资讯场景用户兴趣变化快ItemCF 更实用因为物品相似关系相对稳定实时性更好解释性也强社交场景比如豆瓣小组、知乎关注人群圈层明显UserCF 可以发现“同类人带来的惊喜”有社交扩散的效果。ItemCF 在工程落地时有几个隐蔽坑点需要注意归一化问题一个热门物品可能和所有物品都有相似度像母舰一样把所有流量吸走。解决方法是做相似度归一化或者热门惩罚公式 IUF逆用户频率就是常用的降权手段。时间衰减物品相似度里要带上时间衰减因子否则三个月前的热门内容永远霸榜。我在实际实现时会给用户行为时间打一个半衰期权重公式类似weight exp(-alpha * days_diff)。矩阵稀疏冷门物品的行为很少算出来的相似度极不稳定噪声很大。落地时对稀疏物品做平滑或者直接过滤比强行计算好很多。2.3 矩阵分解把隐因子挖出来有了“用户-物品”行为矩阵你不会满足于只做近邻计算。用户和物品的关系太稀疏直接算相似度容易失真而且矩阵里往往有大量空位。矩阵分解的思路是把“用户-物品”高维矩阵拆成两个低维矩阵用隐因子向量来表示用户和物品。用公式表达就是用户对物品的预估评分 用户隐因子向量与物品隐因子向量的内积。[ \hat{r}_{ui} p_u^T q_i ]其中 (p_u) 是用户 u 的隐因子向量(q_i) 是物品 i 的隐因子向量。隐因子没有显式的业务含义但常常能学习到“题材风格、消费能力、观看时段、价格敏感度”等隐含模式。矩阵分解里最常用的两种实现FunkSVD只分解已有评分的项通过随机梯度下降优化 RMSE。ALS交替最小二乘法固定一个矩阵求解另一个矩阵交替进行直至收敛适合 Spark 分布式实现。下面给一个 FunkSVD 的最简可运行代码框架展示核心更新公式import numpy as np def train_funk_svd(R, k20, lr0.01, reg0.1, epochs50): n_users, n_items R.shape P np.random.normal(size(n_users, k)) Q np.random.normal(size(n_items, k)) for epoch in range(epochs): for u in range(n_users): for i in range(n_items): if R[u, i] 0: continue err R[u, i] - np.dot(P[u], Q[i]) P[u] lr * (err * Q[i] - reg * P[u]) Q[i] lr * (err * P[u] - reg * Q[i]) return P, Q这只是教学代码生产环境还需要负样本采样、偏置项、置信度权重但这些花里胡哨的优化都建立在对基础公式的理解之上。矩阵分解的优势是能做个性化而且隐因子表达能力强于近邻方法缺点是冷启动难新用户、新物品没有行为训练要定期更新且难以引入用户年龄、物品价格等边信息。后出现的 FM 模型正是在这个痛点上前进了一步——它把隐因子向量的思想推广到特征交叉场景能够把用户 ID、物品 ID、类别、上下文特征全部塞进一个统一模型。后来火过一阵的 GBDTLR以及现在更常用的深度兴趣网络它们的底层味道也没离开“先挖掘交叉特征再交给模型打分”这个框架。2.4 逻辑回归排序把特征工程发挥到极致召回层圈定了候选集下一步就是精细排序。逻辑回归在这个环节是经典中的经典十多年前各大互联网公司最常用的排序模型之一就是它。逻辑回归解决的是一个二分类问题用户看到某物品后是否会产生点击/购买/播放等目标行为。难点不在模型本身而在特征工程。这里我给出一个非常通用、且在各行业都能迁移使用的特征体系表格特征大类特征示例说明用户特征用户年龄段、性别、注册时长人口统计学属性多样性较低但稳定物品特征物品类目、价格带、发布时间刻画物品自身属性冷启动可用用户-物品交叉特征该类目下历史点击率、是否收藏过同类决定排序效果的关键上下文特征当前时间、设备类型、所在页面位置场景化权重简单但见效统计类特征物品近7天曝光/点击/转化率、用户近30天活跃度反馈快容易被低估但十分有效(上表只是示意实际落地时可以按业务再扩展。)比如在信息流场景“用户近7天对相同类目的点击率”这个特征常常比一堆 embedding 出来的向量特征更有解释力而且工程成本更低。这也是为什么我建议排序模型起步阶段先别急着上深度模型把逻辑回归配合扎实的特征工程跑起来很多场景已经能到七八十分水平。逻辑回归实现很简单sklearn 里一行代码就能跑但真正决定效果的是特征构造和样本处理。样本层面最大的坑是“正负样本比例失衡”展示点击率如果只有1%直接拿原始日志训练模型只会无脑输出负类。我常用的处理方式是负样本降采样 点击正样本加权或者直接预测点击率时对负样本按曝光占比做校正。2.5 FM让特征交叉自动一点逻辑回归强行做人工特征工程但人工交叉有三个问题工作量大、维度爆炸、特征组合稀疏时系数学不准。FMFactorization Machine就是为这个痛点而生的它通过隐因子向量让两两特征交叉的权重可以泛化到从未一起出现过的特征组合。FM 的公式[ \hat{y}(x) w_0 \sum_{i1}^{n} w_i x_i \sum_{i1}^{n}\sum_{ji1}^{n} \langle v_i, v_j \rangle x_i x_j ]前两项是线性部分第三项是交叉部分。核心思路是每个特征学一个隐向量 (v_i)交叉权重由向量内积计算。这样即使特征 A 和特征 B 的组合在训练集中从未出现也能通过它们各自的向量计算出合理的交叉权重。FM 实现不复杂libfm、xlearn 都可以直接拿来用稍新一点的 deepctr 库也能轻松跑 DeepFM。实际业务中FM 特别适合电商和广告推荐的 CTR 预估因为它的特征输入天然支持用户 ID、物品 ID、类目、价格等类别型特征的 one-hot 展开并能自动学习高阶交叉。举个最小化例子如果用 FM 预估“男性 数码类目 睡前时段”这种组合对点击的影响人工特征工程可能需要手动构造二阶甚至三阶交叉而 FM 在训练过程中会自动把这几个特征组合学出来。这种能力在特征维度高的场景特别省事。3. 手把手落地一个基于物品相似度的完整推荐流程这个章节我拿一个最常见的 ItemCF 推荐需求带你走一遍从原始行为数据到最终推荐结果的完整流程。这套流程我在多个项目里跑过结构可以作为通用模板替换成你自己的表名和字段就能用。3.1 数据准备与清洗原始数据一般是用户行为日志表核心字段是user_id、item_id、behavior_type、timestamp。我通常会做两件预处理过滤异常行为比如点击后10秒内又返回、时长小于2秒的曝光点击这类噪音行为会影响相似度计算。简单做法是设置行为阈值低于阈值的直接剔除。构造正反馈标签把“点击”作为弱正样本“加购”“下单”作为强正样本在相似度计算时赋予不同权重。比如点击记1分加购记3分下单记5分。这一步直接决定后续算法上限。数据脏模型再先进也没救。3.2 计算物品相似度矩阵传统 ItemCF 的相似度公式是[ sim(i, j) \frac{\sum_{u} w_{u,i} \cdot w_{u,j}}{\sqrt{\sum_{u} w_{u,i}^2} \cdot \sqrt{\sum_{u} w_{u,j}^2}} ]实践中会对热门物品做惩罚[ sim(i, j) \frac{\sum_{u} w_{u,i} \cdot w_{u,j} \cdot \frac{1}{\log(1 N_u)}}{\sqrt{\sum_{u} w_{u,i}^2} \cdot \sqrt{\sum_{u} w_{u,j}^2}} ]其中 (N_u) 是用户 u 交互过的物品总数。这个惩罚项叫 IUFInverse User Frequency作用是压制“什么类型都看”的泛兴趣用户带来的虚假共现。实现时不需要每次从零全量计算可以提前把用户行为序列切分好按物品对聚合。下面是一个非常精简的坐标式实现便于跑通全流程from collections import defaultdict import math def build_item_sim(user_items): item_users defaultdict(set) for user, items in user_items.items(): for item in items: item_users[item].add(user) item_sim defaultdict(lambda: defaultdict(float)) item_cnt {item: len(users) for item, users in item_users.items()} for item, users in item_users.items(): for u in users: for v_item in user_items[u]: if v_item item: continue item_sim[item][v_item] 1 / math.log(1 len(user_items[u])) for item, related_items in item_sim.items(): for v_item, cnt in related_items.items(): item_sim[item][v_item] cnt / math.sqrt(item_cnt[item] * item_cnt[v_item]) return item_sim这段代码有个小注意点外层循环是遍历物品访问用户行为序列来累计共现次数归一化放在最后做。对于百万级物品纯 Python 是不现实的生产上要用 Spark 或内存数据库但逻辑完全相同理解透这一版迁移到分布式只是改数据结构的事。3.3 召回与排序生成推荐列表有了物品相似度矩阵召回就是查表聚合。对于用户历史交互过的每个物品取它的 TopK 相似物品汇总候选物品对每个候选物品按“历史物品与它的相似度 * 行为权重”加权求和最后过滤掉已经交互过的物品按总分排序截断 TopN。def recommend_for_user(user_items_history, item_sim, top_k10, top_n20): scores defaultdict(float) for item in user_items_history: # 行为权重这里简化全部为1 for sim_item, sim_score in item_sim[item].items(): if sim_item in user_items_history: continue scores[sim_item] sim_score sorted_items sorted(scores.items(), keylambda x: x[1], reverseTrue) return [item for item, score in sorted_items[:top_n]]排序这一步还可以加多样性控制比如同一个二级类目的物品最多出现3个。否则会发现推荐列表里全是用户最近看过那件商品的同款变体信息量很低也没有探索性。多样性和相关性的平衡是推荐效果提升的一个重要工程优化点代码上可以简单做重排时打散效果提升往往比调模型参数明显。3.4 离线评估用指标判断模型好坏推荐模型效果好不好不能靠感觉要用离线指标先卡一道门槛。我常用的指标如下指标计算口径建议阈值召回率 RecallK测试集里用户真实交互的物品被推荐命中的比例不同场景差异大重点是横向对比精确率 PrecisionK推荐列表里被用户真实交互的比例同上覆盖率 Coverage推荐物品数占总可推荐物品数比例避免只推头部建议低于30%需要警惕多样性/类目熵推荐列表中类目分布的熵约多样越分散越高越好离线评估要特别注意“时间切分”问题。不能用未来数据训练、过去数据测试必须按时间排序前80%做训练、后20%做测试才能模拟真实业务中的线上表现。4. 推荐系统实战中绕不开的坎与排查技巧纸上谈兵容易上了生产环境才发现各种问题不按教科书出牌。这一章我把自己踩过的坑和摸索出的排查套路整理出来希望能帮你少走弯路。4.1 冷启动问题新用户和新物品怎么推冷启动是推荐工程师躲不开的话题。新用户没有历史行为协同过滤没法算相似度新物品没有交互物品相似度矩阵里几乎找不到它。我惯用的分级策略是用户冷启动只依赖用户基础属性性别、城市、设备、注册来源配以热门榜、新品榜、编辑精选等规则打底用户产生第一个点击后立刻用 ItemCF 做实时召回填充后续刷新。物品冷启动靠物品内容属性做基于内容的召回比如类目、标签、品牌、文本向量上线初期给一定探索流量观察点击表现再决定是否进入常规推荐池。系统冷启动整个产品还没数据先用规则配热门、编辑推荐同时尽快在UI上埋点把行为数据采集列完整。冷启动阶段的核心原则是不要指望推荐模型要让规则和人工先跑起来把初期的种子行为数据攒够再逐步切模型。4.2 数据稀疏与长尾问题用户行为大部分集中在少数头部内容长尾物品和冷门用户的行为矩阵极度稀疏。矩阵分解在这个环境下训练困难近邻计算也不稳定。我常用的三个应对方式行为加宽把“点击”“收藏”“加购”“购买”等多行为加权合并成用户对物品的偏好分别只用单一行为。内容特征补充当“用户-物品”的协同信号不够时用物品的内容向量比如文本 embedding补充为物品相似度的一部分。降维玩法对物品做类目聚合先计算类目偏好再在类目内做物品召回。这能极大缓解稀疏压力实现难度也不高。4.3 流行度偏差问题推荐系统天生容易马太效应——头部越来越热长尾越来越冷。如果你发现推荐列表里全是爆款用户点击率反而下降大概率是多样性和探索性没做好。在排序阶段我会加入一个探索因子也不是简单的随机曝光而是给每类物品设置曝光配额再在同类物品内部按预估分数排序。比如某页面 20 条推荐里固定至少 4 条来自中长尾这些中长尾内部再按“预估点击率 一定随机扰动”来排这样既不伤害整体效果也保住了生态多样性。4.4 实时性问题从离线到在线更新的时间差大多数入门项目一开始都是离线计算、定时更新比如每天凌晨跑一次全量相似度矩阵白天用户访问时只能拿前一天的推荐结果。这个方案在业务初期没啥问题但等用户行为量大了你会发现很多推荐已经“过期”了——用户昨天刚买完一台手机今天推荐列表里还是手机配件。演进路径一般是离线日级更新 - 小时级增量更新 - 分钟级实时特征 - 在线实时召回排序。如果团队资源和数据规模有限我建议至少做到“小时级增量更新相似度矩阵”或者“实时更新用户最近交互序列”这两个点性价比最高。用户最近交互序列一旦实时更新即使相似度矩阵是旧的推荐结果也能快速反映用户最新兴趣。5. 算法选型速查与个人经验小结最后给一张速查表方便你拿到一个推荐需求时直接对号入座业务阶段/场景推荐算法候选选型理由新系统冷启动热门统计、关联规则无历史数据快速撑起基础体验电商有行为数据ItemCF、矩阵分解行为稀疏可控可解释性强资讯/短视频海量内容向量召回、ItemCF、深度模型内容更新快需要实时性和多样性广告/精排 CTR 预估逻辑回归、FM、DeepFM特征多需要自动交叉社交/人群推荐UserCF、图算法圈层效应明显适合找相似人我的项目经历里大多数“推荐没效果”的案例失败原因都不是模型不够先进而是又偏又漏。比如把模型调参调了俩礼拜结果发现采集的行为埋点日志里商品 ID 和用户 ID 有上传丢失或者把全部精力放在排序模型上召回头部就200条排序再准也没用。所以我的建议很朴素先把数据、埋点、召回源头治理好再用简单模型建立基线拿到基线之后再考虑上更复杂的模型。这个顺序走顺了推荐系统的迭代节奏会非常健康。最后分享一个我个人一直在用的判断标准推荐算法好与不好不在于榜单指标涨了多少而在于用户是否真的觉得“这东西有点懂我”。多去看看真实的用户反馈、多去刷刷自己做的推荐位很多问题是离线指标根本暴露不出来的。
返回列表