☰
基于项目聚类与时间衰减的协同过滤推荐算法:破解稀疏与兴趣漂移
2026/10/14 18:02:49 网站建设 项目流程

简介:这是一份面向推荐系统学习者与科研训练参与者的算法实践资料,围绕项目聚类与时间衰减两大策略改进传统协同过滤,解决用户兴趣随时间漂移、推荐多样性不足的问题。资源包共139个文件,约113.29MB,以67篇pdf文献、12个py脚本、6个dat数据集及doc实验记录为主,另含pptx汇报、xml配置与png图表,覆盖理论参考、代码实现与实验数据三类内容。其中Python代码涉及用户-项目交互矩阵构建、余弦相似度与皮尔逊相关系数计算、基于用户或项目的协同过滤建模,以及时间衰减因子的计算与应用;曲线拟合部分借助艾宾浩斯遗忘曲线模拟记忆规律,用于优化衰减参数。参考的硕士论文与期刊文献为项目聚类和时间衰减提供理论支撑。目前已有934人学习,适合希望从原理到代码完整复现改进型协同过滤、并理解时效性建模思路的读者参考。

1. 基于项目聚类与时间衰减的协同过滤推荐算法:为什么你的推荐列表总在炒冷饭

做过推荐系统的同行大概率遇到过这种场景:离线指标 AUC 涨了,线上 CTR 却纹丝不动,用户刷出来的还是那几样老面孔。问题往往不在模型容量,而在协同过滤本身的两个硬伤——项目侧相似度矩阵太稀疏,以及用户兴趣随时间漂移但算法还在用三年前的交互打分。基于项目聚类与时间衰减的协同过滤推荐算法,就是冲着这两个痛点去的:先用聚类把 Item 压成簇,缓解稀疏;再给每次交互乘一个随时间衰减的权重,让近期行为说话更响。它适合已经有基础 CF 管线、想在不换深度模型的前提下把召回质量往上抬一档的团队,也适合数据量不大、冷启动明显的中小场景。下面按「原理选型 → 聚类实现 → 时间衰减 → 融合打分 → 避坑 → 调参与验证」的顺序拆开讲。

2. 项目聚类怎么压稀疏:从相似度矩阵到簇级近邻

协同过滤在项目侧的核心是算 Item-Item 相似度,但真实交互矩阵里,两个 Item 被同一批用户共同交互过的概率极低,导致相似度矩阵大面积为零。项目聚类的思路是:先把 Item 按特征或交互模式聚成若干簇,相似度计算和近邻搜索在簇级别先做一轮粗筛,再在簇内做细排。这样既降低了计算量,也让那些单独看很稀疏、但同簇的 Item 能互相借力。

2.1 为什么选聚类而不是直接上矩阵分解

矩阵分解(MF)确实能降维,但它把 Item 映射到隐空间后,可解释性差,且冷启动 Item 因为没有交互,隐向量基本是随机初始化的。聚类不一样:只要 Item 有内容特征(类目、标签、文本 embedding),就能先归簇,冷启动 Item 至少能落到一个簇里,拿到簇内平均相似度作为兜底。常见做法是「内容特征 + 交互共现」拼成一个混合向量再聚类,纯内容聚类在行为稀疏时更稳,纯共现聚类在行为丰富时更准。我一般会按数据稀疏度选:交互密度低于千分之一,优先内容特征;高于千分之五,可以混入共现信号。

2.2 用 KMeans 对 Item 向量做聚类的可复现步骤

下面这段代码演示从 Item 特征矩阵到簇标签的完整流程,特征可以是类目 one-hot、TF-IDF 文本向量或预训练 embedding,这里用后者举例。

import numpy as np from sklearn.cluster import KMeans from sklearn.preprocessing import normalize # item_feats: shape (n_items, dim),每行是一个 Item 的向量 # 先做 L2 归一化,避免模长差异主导距离 item_feats = normalize(item_feats, norm='l2') # n_clusters 是核心参数,经验值 sqrt(n_items/2),再按轮廓系数微调 n_clusters = int(np.sqrt(item_feats.shape[0] / 2)) kmeans = KMeans( n_clusters=n_clusters, init='k-means++', # 比随机初始化更稳,避免空簇 n_init=10, # 多次重启取最优,防止局部最优 max_iter=300, random_state=42 ) cluster_labels = kmeans.fit_predict(item_feats) # 输出每个簇的 Item 数量,检查是否有超小簇 unique, counts = np.unique(cluster_labels, return_counts=True) for c, n in zip(unique, counts): print(f"cluster {c}: {n} items")

逻辑说明:归一化是为了让余弦距离和欧氏距离在聚类时表现一致;n_init=10是防止 KMeans 陷入局部最优的后悔药,尤其当 Item 向量分布不均匀时。参数上,n_clusters不要拍脑袋,先用sqrt(n/2)起步,再画轮廓系数曲线,选下降拐点。如果出现某个簇只有个位数 Item,说明该簇是离群点集合,可以考虑合并到最近的大簇,或者直接标记为长尾簇单独处理。

2.3 簇级相似度与簇内细排的两级检索

聚类完成后,相似度计算分两级:第一级算目标 Item 所在簇与其他簇的簇心相似度,取 Top-N 簇;第二级只在候选簇内算 Item-Item 余弦相似度。这样把 O(n²) 的相似度矩阵压缩到 O(k² + m²),k 是簇数,m 是簇内平均 Item 数。实际落地时,簇心相似度可以离线算好缓存,簇内相似度按需实时算,召回延迟能从几百毫秒降到几十毫秒。注意簇内细排时仍要用原始 Item 向量,不要用簇心代替,否则推荐结果会趋同,用户刷到的全是「簇代表」,多样性直接崩掉。

3. 时间衰减怎么加:让三年前的点击不再和昨天等价

时间衰减要解决的是兴趣漂移。用户半年前买过婴儿奶粉,不代表现在还需要;上周刚搜过机械键盘,权重就该拉高。做法是给每次交互一个衰减权重,权重随「当前时间与交互时间之差」指数下降,再把这个权重乘进相似度或预测分里。

3.1 指数衰减与半衰期的参数选择

最常用的是指数衰减:w = exp(-λ * Δt),其中 Δt 是距今天数,λ 控制衰减速度。λ 越大衰减越快。更直观的参数是半衰期 T:经过 T 天后权重降到 0.5,此时 λ = ln2 / T。半衰期怎么定?电商类目通常 30 到 90 天,内容资讯类 3 到 7 天,工具类可以拉到 180 天。我一般会按业务复购周期来:复购周期中位数是多少天,半衰期就设成它的 1 到 2 倍。

import numpy as np from datetime import datetime def time_decay_weight(event_time, now_time, half_life_days=30): """ event_time / now_time: datetime 对象 half_life_days: 半衰期,单位天 返回 0~1 之间的衰减权重 """ delta_days = (now_time - event_time).total_seconds() / 86400.0 # 防止未来时间导致权重 >1 delta_days = max(delta_days, 0.0) lam = np.log(2) / half_life_days return np.exp(-lam * delta_days) # 示例:30 天前的一次点击,半衰期 30 天 w = time_decay_weight( datetime(2024, 5, 1), datetime(2024, 5, 31), half_life_days=30 ) print(round(w, 4)) # 约 0.5

逻辑说明:max(delta_days, 0.0)是防止数据里混入未来时间戳导致权重异常放大,这是线上常见的脏数据坑。半衰期参数不要全局一刀切,不同类目可以配不同值,比如快消品 15 天、耐用品 120 天,用配置表管理,方便 A/B 实验时按类目调。

3.2 把衰减权重乘进相似度矩阵

拿到每次交互的权重后,Item-Item 相似度不再是简单的共现计数,而是加权共现。具体做法:对每个用户,把他交互过的 Item 两两组合,共现次数加 1 乘以两个交互权重的乘积(或几何平均)。这样近期共同被交互的 Item 对,相似度会明显高于很久以前的组合。

from collections import defaultdict import math def build_weighted_item_sim(user_items, item_sim, half_life_days=30): """ user_items: {user_id: [(item_id, event_time), ...]} item_sim: 预分配的 defaultdict(float),存加权共现 """ now = datetime.now() for uid, records in user_items.items(): # 先算每条记录的权重 weighted = [] for iid, et in records: w = time_decay_weight(et, now, half_life_days) weighted.append((iid, w)) # 两两组合累加 for i in range(len(weighted)): for j in range(i + 1, len(weighted)): a, wa = weighted[i] b, wb = weighted[j] if a == b: continue # 用几何平均,避免单个高权重主导 pair_w = math.sqrt(wa * wb) item_sim[(a, b)] += pair_w item_sim[(b, a)] += pair_w return item_sim

逻辑说明:用几何平均而不是直接相乘,是为了防止某次刚发生的交互权重接近 1,把历史信号完全淹没。如果业务更看重近期,可以改成wa * wb;如果更看重长期稳定偏好,可以改成min(wa, wb)。这个选择没有标准答案,建议做成可配置项,用线上指标说话。

3.3 预测分融合:相似度、簇归属与衰减的三路加权

最终给用户算某个 Item 的预测分时,把三路信号融合:簇级相似度(粗)、Item 级加权相似度(细)、以及该 Item 在用户历史中的衰减权重(如果有直接交互)。常见公式是score = α * sim_item + β * sim_cluster + γ * decay_hist,α、β、γ 用网格搜索或贝叶斯优化在验证集上定。注意三路信号量纲不同,融合前都要做 min-max 或 z-score 归一化,否则数值大的那路会直接主导排序。

4. 避坑与常见问题排查:聚类和时间衰减最容易翻车的五个点

4.1 簇数量拍脑袋导致推荐结果趋同

现象:推荐列表里全是同一类目或同一风格的商品,多样性指标断崖下跌。原因:n_clusters设得太小,每个簇内 Item 差异大,簇内细排时相似度区分度不够,排出来都差不多。解决:先用轮廓系数和 Davies-Bouldin 指数扫一遍簇数,选指标拐点;再在簇内细排时引入随机扰动或多样性重排,比如 MMR(最大边际相关)做后处理。

4.2 半衰期全局统一,快消和耐用品互相拖累

现象:快消类目的推荐明显滞后,用户刚买完的东西还在推;耐用品类目又过度偏向近期,长期偏好被忽略。原因:半衰期写死在代码里,所有类目共用。解决:把半衰期做成类目维度的配置表,快消 7 到 15 天,耐用品 90 到 180 天,用配置中心下发,改参数不用发版。

4.3 未来时间戳导致权重爆炸

现象:某些 Item 的预测分异常高,排查发现交互时间戳比当前时间还晚。原因:数据同步延迟、时区处理错误或测试数据混入生产。解决:在衰减函数里加max(delta, 0)兜底,同时在数据接入层做时间戳校验,超过当前时间 5 分钟的记录直接丢弃并告警。

4.4 冷启动 Item 落不进任何簇

现象:新上架的 Item 没有交互,聚类时因为特征缺失被分到随机簇,推荐效果极差。原因:聚类只用了交互共现特征,新 Item 没有共现记录。解决:聚类特征里必须包含内容特征(类目、标题 embedding),新 Item 至少能按内容归簇;同时给新 Item 一个探索权重,在预测分里加一个小的随机项,保证有曝光机会。

4.5 加权共现矩阵内存溢出

现象:离线任务跑着跑着 OOM。原因:两两组合是 O(L²),L 是单用户交互序列长度,重度用户 L 可能上千,组合数爆炸。解决:对单用户交互序列做截断,只保留最近 N 条(比如 200 条);或者用稀疏矩阵存储,只累加共现次数超过阈值的 Item 对,低于阈值的直接丢弃。

5. 调参与线上验证:把离线指标翻译成业务收益

5.1 三个必调参数与搜索范围

参数含义建议范围调参方法
n_clustersItem 聚类簇数sqrt(n/2) 上下浮动 50%轮廓系数 + 线上多样性指标
half_life_days时间衰减半衰期类目复购周期的 1~2 倍按类目网格搜索
α / β / γ三路信号融合权重各 0~1,和为 1贝叶斯优化,目标线上 CTR

调参顺序建议:先定 half_life_days,因为衰减直接影响相似度矩阵的数值分布;再定 n_clusters,聚类质量决定召回上限;最后调融合权重,这一步对最终排序影响最直接,但也最容易过拟合验证集,建议用时间切分的验证集而不是随机切分。

5.2 离线评估指标与线上 A/B 的对应关系

离线看 Recall@K 和 NDCG@K,但这两个指标涨了线上不一定涨。我的血泪经验是:离线 Recall 涨但线上 CTR 不涨,通常是召回结果多样性下降,用户刷两下就没了新鲜感。所以离线阶段除了准确率指标,一定要加覆盖率(Coverage)和基尼系数(Gini)监控。线上 A/B 至少看三个指标:CTR、人均曝光 Item 数、以及次日留存。如果 CTR 涨但留存跌,说明推荐在透支用户兴趣,衰减参数可能设得太激进。

5.3 一个容易被忽略的验证技巧:时间穿越检查

做时间衰减的离线验证时,最容易犯的错是用未来数据算历史权重。比如用 6 月的数据训练,却用 5 月的交互时间算衰减,导致权重全错。正确做法是:验证集的每条样本,只允许使用该样本发生时间之前的数据来算相似度和权重,严格按时间切分。我一般会在数据管道里加一个断言,检查所有特征的时间戳都早于样本时间戳,不通过就直接报错,这个黑匣子检查能省掉大量排查时间。

这套方案我在几个中等数据量的场景里跑过,最深的教训是:聚类和时间衰减都不是银弹,它们解决的是特定问题——稀疏和漂移。如果数据本身很稠密、用户兴趣很稳定,硬加这两层反而引入噪声。上线前先做小流量 A/B,确认离线指标和线上收益方向一致再全量。希望帮到你。

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

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

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

立即咨询