资讯动态

基于项目聚类与时间衰减的协同过滤推荐算法优化实践

发布时间:2026/10/14 10:25:21 来源:尧图企业网站定制
简介这份资源是2018年夏季科研训练项目的完整实现包面向推荐系统初学者与算法研究者聚焦协同过滤中用户兴趣随时间漂移、推荐多样性不足两大痛点。项目将项目聚类与时间衰减两大策略融合通过聚类把相似物品归组以挖掘潜在兴趣、拓宽推荐范围借助艾宾浩斯遗忘曲线拟合时间衰减因子让近期行为获得更高权重从而提升推荐的时效性与人性化程度。压缩包共139个文件、约113.29MB以67篇pdf参考文献、12个Python源码、6份dat数据集、6个doc实验记录及若干xml、pptx、png等为主涵盖用户-项目交互矩阵构建、余弦相似度与皮尔逊相关系数计算、基于用户/项目的协同过滤建模、时间衰减因子应用及曲线拟合评估等关键环节。目前已有934人学习下载适合希望从理论到代码完整复现改进型协同过滤算法、并借鉴科研项目组织方式的读者参考。1. 当推荐系统开始「记性变差」项目聚类与时间衰减到底在解决什么做过推荐系统的同行大概都遇到过这种场景离线指标 AUC 涨了两个点信心满满上线结果首页点击率反而掉了。排查一圈发现用户三个月前点过的技术文章还在被反复推新上线的项目却迟迟进不了召回池。这不是模型不行是协同过滤的「记性」出了问题——它把用户所有历史行为一视同仁既记不住兴趣会漂移也分不清哪些项目本来就该归为一类。基于项目聚类与时间衰减的协同过滤推荐算法核心就干两件事一是用聚类把相似项目先归堆缓解稀疏矩阵里「邻居找不到」的尴尬二是给交互行为加时间衰减权重让近期行为说话更算数。它适合谁适合手里有中等规模行为日志、跑过基础 ItemCF 或矩阵分解、但被冷启动和兴趣漂移折磨的团队。这套思路不依赖深度模型单机就能复现是性价比很高的一次改造。2. 项目聚类怎么选把相似度矩阵从稀疏变稠密协同过滤最怕的就是用户-项目交互矩阵太稀疏。一个用户可能只碰过几百个项目而项目总量是几万甚至几十万算出来的项目相似度要么是零要么是噪声。项目聚类的价值在于先把项目按内容特征或共现关系聚成若干簇簇内项目共享相似度信息等于给稀疏矩阵做了一次「插值」。2.1 特征工程项目侧到底拿什么来聚类聚类效果好不好八成看特征。常见做法分两路一路用项目的内容特征比如文本类项目用 TF-IDF 或句向量商品类项目用类目、品牌、价格分桶另一路用行为共现特征把「同一批用户都交互过的项目」当作相似信号。我一般会两路都做然后拼接。纯内容特征的问题是「看起来像但用户不买账」纯共现特征的问题是冷门项目根本没有共现记录。拼接之后做归一化能兼顾语义和真实行为。import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.preprocessing import normalize # item_texts: 项目id - 文本描述标题标签拼接 # cooc_matrix: 项目共现矩阵shape(n_items, n_items)来自用户行为 tfidf TfidfVectorizer(max_features5000, min_df2) content_feat tfidf.fit_transform(item_texts) # 内容特征 content_feat normalize(content_feat) # L2归一化 # 共现矩阵做行归一化避免热门项目主导 cooc_norm normalize(cooc_matrix, norml1, axis1) # 拼接内容权重0.6共现权重0.4可调 combined np.hstack([ content_feat.toarray() * 0.6, cooc_norm * 0.4 ])这段代码的关键在权重分配。内容权重给太高聚类会偏向语义相似但行为不相关的项目共现权重给太高热门项目会把整个簇带偏。0.6/0.4 是我在几个中等规模数据集上试出来的起点实际项目里建议用轮廓系数扫一遍 0.3 到 0.7。2.2 聚类算法选型K-Means 不是唯一答案很多人一提聚类就上 K-Means但项目特征往往是高维稀疏的K-Means 的欧氏距离在这种空间里区分度很差。我的经验是项目数在 1 万以内、特征稠密K-Means 够用速度快特征稀疏、簇形状不规则用 MiniBatchKMeans 或 Birch内存友好想要层次结构、方便后续按粒度召回用层次聚类但要注意 O(n²) 的复杂度。K 值怎么定不要拍脑袋。用肘部法看畸变程度或者直接看业务如果你希望每个簇大概覆盖 50 到 200 个项目用总数除一下就有个初值再微调。from sklearn.cluster import MiniBatchKMeans from sklearn.metrics import silhouette_score best_k, best_score 0, -1 for k in [20, 40, 60, 80, 100]: km MiniBatchKMeans(n_clustersk, batch_size1024, random_state42) labels km.fit_predict(combined) # 稀疏高维下轮廓系数会偏低只做相对比较 score silhouette_score(combined, labels, sample_size2000) if score best_score: best_k, best_score k, score print(f选定簇数: {best_k}, 轮廓系数: {best_score:.4f})这里有个坑要提前说轮廓系数在高维稀疏数据上普遍偏低0.1 到 0.3 都算正常别看到 0.15 就以为聚类失败了。它只用来横向比较不同 K 值不是绝对标准。2.3 用簇中心做召回把「找邻居」变成「找邻居簇」聚类完成后召回逻辑要改。传统 ItemCF 是「找和目标项目最相似的 K 个项目」现在变成两步先定位目标项目所属的簇再在簇内找相似项目簇间相似度用簇中心距离近似。这样做的好处是召回池更稳定不会因为某个项目临时爆火就把整个推荐列表带偏。代价是损失了一部分跨簇的惊喜推荐所以实际系统里我一般保留 20% 的配额给全局相似度召回剩下 80% 走簇内。3. 时间衰减怎么设让近期行为拿到该有的权重时间衰减是这套算法的另一半。协同过滤默认所有历史行为等权但用户兴趣是有半衰期的——三个月前收藏的项目和昨天刚交互的项目重要性显然不一样。时间衰减就是给每个交互乘一个随时间递减的系数。3.1 指数衰减与半衰期参数怎么定最常用的是指数衰减weight exp(-λ * Δt)其中 Δt 是当前时间与交互时间的差值λ 控制衰减速度。更直观的写法是用半衰期 Tweight 0.5 ^ (Δt / T)半衰期 T 的含义是「经过 T 时间后行为权重降到一半」。T 怎么定看业务节奏业务类型建议半衰期理由新闻资讯13 天热点生命周期短短视频37 天兴趣漂移快电商商品1430 天购买决策周期长技术课程3090 天学习兴趣相对稳定这个表是起点不是标准答案。我一般会先按业务直觉定一个值然后用线上 A/B 的点击率反推调整。import math from datetime import datetime def time_decay_weight(interact_time, now_time, half_life_days): interact_time: 交互时间 datetime now_time: 当前时间 datetime half_life_days: 半衰期单位天 delta_days (now_time - interact_time).total_seconds() / 86400.0 if delta_days 0: delta_days 0 # 防止时间戳异常导致权重爆炸 weight math.pow(0.5, delta_days / half_life_days) return weight # 示例半衰期30天 now datetime(2024, 6, 1) w_1day time_decay_weight(datetime(2024, 5, 31), now, 30) # 约0.977 w_30day time_decay_weight(datetime(2024, 5, 2), now, 30) # 0.5 w_90day time_decay_weight(datetime(2024, 3, 3), now, 30) # 约0.125注意那个delta_days 0的保护。线上数据里时间戳错乱是家常便饭不加这行一个未来时间戳能让权重变成天文数字直接把推荐结果带飞。3.2 把衰减权重灌进相似度计算有了项目聚类和时间衰减相似度计算要同时用上两者。传统余弦相似度是sim(i, j) Σ(u_i * u_j) / (||u_i|| * ||u_j||)现在把每个交互乘上时间衰减权重 wsim(i, j) Σ(w_u * r_ui * w_u * r_uj) / (||W·u_i|| * ||W·u_j||)其中 W 是对角矩阵对角线是每个交互的时间权重。实现上不用真的构造对角矩阵逐元素乘就行。import numpy as np def weighted_cosine_sim(interaction_matrix, time_weights): interaction_matrix: shape(n_users, n_items)原始交互 time_weights: shape(n_users, n_items)每个交互的时间衰减权重 weighted interaction_matrix * time_weights # 逐元素加权 norms np.linalg.norm(weighted, axis0, keepdimsTrue) norms[norms 0] 1e-9 # 防止除零 normalized weighted / norms sim_matrix normalized.T normalized # 项目-项目相似度 np.fill_diagonal(sim_matrix, 0) # 自身相似度置零 return sim_matrix这段代码里norms 0的处理很关键。冷门项目可能所有交互权重都趋近于零归一化时直接除零报错。给个极小值兜底比加 epsilon 更稳。3.3 聚类与衰减的融合顺序先加权还是先聚类这是个容易被忽略但影响很大的问题。我的做法是先算时间衰减权重用加权后的交互矩阵去算共现特征再做聚类。理由是聚类应该反映「当前有效」的项目关系而不是被陈旧行为污染。如果反过来先聚类再加权簇结构里混入了大量过期信息衰减的效果会被稀释。这个顺序差异在离线评估里可能只差零点几个点但线上长期跑下来先加权的版本对新鲜内容的响应明显更快。4. 离线评估与线上验证指标怎么读才不骗自己算法改完最危险的动作就是只看一个 AUC 就上线。协同过滤的评估要分两层离线看排序质量线上看真实反馈。4.1 离线指标RecallK 比 AUC 更贴近召回场景AUC 衡量的是整体排序能力但推荐系统真正关心的是「Top K 里有没有用户喜欢的」。所以离线阶段我优先看 RecallK 和 NDCGK。def recall_at_k(recommended, ground_truth, k): recommended: 推荐列表按分数降序 ground_truth: 用户真实交互集合 top_k set(recommended[:k]) hit len(top_k set(ground_truth)) return hit / len(ground_truth) if ground_truth else 0.0 def ndcg_at_k(recommended, ground_truth, k): dcg 0.0 for idx, item in enumerate(recommended[:k]): if item in ground_truth: dcg 1.0 / math.log2(idx 2) idcg sum(1.0 / math.log2(i 2) for i in range(min(len(ground_truth), k))) return dcg / idcg if idcg 0 else 0.0评估时要注意时间切分用 T 时刻之前的行为训练预测 T 到 TΔ 之间的交互。随机切分会导致时间泄漏离线指标虚高上线就翻车。这是血泪经验别问我怎么知道的。4.2 线上 A/B关注长期指标而非单日点击线上验证至少跑两周。第一周看点击率、转化率这些短期指标第二周重点看留存和人均交互深度。时间衰减类算法的一个典型现象是短期点击可能持平甚至微降因为推荐结果变「保守」了但用户留存和长期活跃会改善。如果只跑三天就下结论很容易把一个正确的方向毙掉。我一般会设一个「探索配额」比如 10% 的流量走无衰减的基线用来对比长期效果。5. 避坑指南这套算法最容易翻车的五个地方5.1 现象聚类后推荐结果高度同质化原因K 值设得太小或者内容特征权重过高导致一个簇里全是语义相近的项目召回多样性丧失。解决把 K 值调大或者降低内容特征权重、提高共现权重。另外在召回阶段强制保留跨簇配额比如每个用户至少 20% 的推荐来自非目标簇。5.2 现象时间衰减开了之后新用户推荐质量暴跌原因新用户交互本来就少衰减一乘有效信号更稀疏相似度计算几乎全是零。解决对新用户做衰减豁免或者用全局热门做兜底召回。我的做法是交互数少于 5 条的用户时间权重统一设为 1.0不做衰减。5.3 现象离线指标涨了线上点击率没动原因离线评估用了随机切分时间泄漏导致指标虚高或者离线用的半衰期和线上实际业务节奏不匹配。解决改成时间切分重新评估同时用线上数据反推半衰期。具体做法是拿几组不同半衰期的配置跑小流量看哪组的次日留存最好。5.4 现象相似度矩阵计算越来越慢原因项目数增长后稠密相似度矩阵内存爆炸。1 万个项目的矩阵就是 1 亿个浮点数10 万项目直接没法存。解决不要存全量矩阵。用稀疏存储或者只保留每个项目的 Top 200 邻居。聚类之后簇内相似度可以实时算簇间用簇中心近似内存占用能降一个数量级。5.5 现象时间戳异常导致权重计算溢出原因日志里混入了未来时间戳或空值衰减公式里 Δt 为负指数函数直接爆炸。解决在权重函数入口做双重保护——空值填默认时间负值截断为零。这个保护必须放在最底层别指望上游数据清洗能百分百干净。6. 一个可复现的最小验证脚本与调参习惯如果你想把整套流程跑通我建议从一个最小闭环开始构造模拟交互数据跑通「加权 → 聚类 → 相似度 → 召回 → 评估」五步再往真实数据上迁。import numpy as np from sklearn.cluster import MiniBatchKMeans np.random.seed(42) # 1. 模拟200用户 x 500项目稀疏交互 n_users, n_items 200, 500 interaction (np.random.rand(n_users, n_items) 0.05).astype(float) # 2. 时间权重模拟每个交互的时间衰减 time_weights np.random.uniform(0.1, 1.0, size(n_users, n_items)) weighted interaction * time_weights # 3. 项目聚类用加权后的转置矩阵做特征 item_feat weighted.T # shape(n_items, n_users) km MiniBatchKMeans(n_clusters20, random_state42) item_labels km.fit_predict(item_feat) # 4. 簇内相似度 全局相似度混合召回 def recommend(user_idx, top_k10): scores weighted[user_idx] weighted.T # 简化版打分 # 簇内加权同簇项目分数乘1.2 user_cluster item_labels[np.argmax(weighted[user_idx])] for i in range(n_items): if item_labels[i] user_cluster: scores[i] * 1.2 return np.argsort(scores)[::-1][:top_k] # 5. 简单评估 recs recommend(0, top_k10) print(用户0的推荐列表:, recs) print(所属簇分布:, item_labels[recs])这个脚本的价值不在精度而在让你快速看到聚类和衰减如何影响召回结果。跑通之后把模拟数据换成真实日志把简化打分换成加权余弦相似度就是一套可上线的原型。调参上我有个习惯每次只动一个参数记录三组数据——离线 Recall20、线上点击率、次日留存。三个指标同向变化才认为改动有效只有一个涨就继续观察。这套习惯帮我避开了很多「离线涨线上跌」的玄学问题。最后说一句实在的项目聚类和时间衰减都不是银弹它们解决的是协同过滤在稀疏和漂移场景下的两个具体痛点。如果你的数据本身稠密、用户兴趣稳定上这套反而增加复杂度。判断标准很简单——先看你的推荐结果里过期内容和冷门项目占比高不高高就值得做不高就先别折腾。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑