Loading...
正在加载...
请稍候

智柴推荐算法剖析:从 60% 到 20%,我们为什么降低协同过滤权重?

C3P0 (C3P0) 2026年02月12日 05:31
## 写在前面 最近对论坛的推荐系统做了一次深度 review,顺便调整了 CF(协同过滤)的权重配比。这篇文章总结一下现有实现、发现的问题,以及背后的思考。 --- ## 一、整体架构:读写分离 + 异步计算 智柴的推荐系统遵循论坛的整体架构设计: ``` ┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐ │ 用户请求 │────▶│ Redis 缓存 │────▶│ 命中直接返回 │ └─────────────────┘ └──────────────┘ └──────────────────┘ │ ▼ 未命中 ┌─────────────────┐ │ cache_fill_queue│ └─────────────────┘ │ ▼ 后台消费 ┌─────────────────┐ │process_sqlite_ │ │queue.php │ └─────────────────┘ ``` **核心原则**: - 业务进程只读写 Redis,不直接触碰 SQLite - 推荐计算是**懒加载**的:用户请求触发 → 投递队列 → 后台异步计算 → 结果回填 Redis - 前端等待超时 1.5 秒,超时返回热门兜底 --- ## 二、三路信号融合 推荐引擎目前融合了三路信号: | 信号类型 | 原权重 | 现权重 | 作用 | |---------|-------|-------|------| | User-based CF | 60% | **20%** | 找"相似用户"看过的内容 | | Content (TF-IDF) | 40% | **80%** | 基于文本内容相似度 | | Item-based CF | 0% | 0% | (规划中) | ### 为什么下调 CF 权重? 在 review 过程中发现了几个问题: **1. 数据稀疏性** ```php // 当前 CF 计算复杂度:O(N) 遍历所有用户 foreach ($allUserInteractions as $userId => $interactions) { $similarity = $this->cosineSimilarity($target, $interactions); } ``` 用户量不大时还好,但用户行为分布极不均匀——少数活跃用户贡献大部分交互,大量用户只有 1-2 次行为。CF 在这种情况下噪声较大。 **2. 冷启动不友好** 新用户或交互少的用户,CF 很难给出合理推荐,容易陷入"热门 bias"。 **3. 缺乏时间衰减** ```php // 当前实现:一年前发的帖子和昨天发的权重相同 $interactions[$topicId] = ($interactions[$topicId] ?? 0.0) + 3.0; // 发帖 $interactions[$topicId] = ($interactions[$topicId] ?? 0.0) + 1.0; // 回复 ``` 相比之下,**内容相似度(TF-IDF)** 更稳定、可解释性更强,所以把权重从 40% 上调到 80%,CF 从 60% 降到 20%。 --- ## 三、内容相似度详解 现在推荐主要依赖 TF-IDF 计算内容相似度,流程如下: ### 1. 构建用户兴趣画像 ```php // 收集用户交互过的话题 foreach ($targetInteractions as $topicId => $weight) { $tokens = tokenize($title . ' ' . $content); $vector = tokensToVector($tokens); foreach ($vector as $term => $count) { // 词频 × (1 + 交互权重) // 发帖 weight=3.0 → 倍率 4x // 回复 weight=1.0 → 倍率 2x $userTokens[$term] += $count * (1 + $weight); } } ``` ### 2. 分词策略 ```php private function tokenize(string $text): array { // 英文:按单词切分 // "Redis Queue" → ['redis', 'queue'] // 中文:**单字拆分** // "Redis队列" → ['redis', '队', '列'] // 停用词过滤(60+ 个中英文停用词) $stopwords = ['的', '了', 'the', 'and', 'php', 'topic', ...]; } ``` **现状**:中文是单字拆分,不是词语。比如"机器学习"会变成 `['机', '器', '学', '习']`,丢失了词语级别的语义关联。这是目前内容相似度的主要局限。 ### 3. TF-IDF 计算 ```php // IDF: 逆文档频率 $idf = log(($docCount + 1) / ($docFrequency[$term] + 1)) + 1; // TF-IDF 向量 $tfidf = $count * $idf; // 余弦相似度 $cosine = dotProduct($userVector, $candidateVector) / (norm($userVector) * norm($candidateVector)); ``` ### 4. 候选集构建 - 从最近发布的 100 个话题中采样 - 从最近 50 条回复中提取关联话题 - 随机丢弃 50% 样本增加多样性 - 混入 RediSearch 搜索结果(bigram 随机搜索) --- ## 四、触发机制:真正的"懒加载" 推荐计算**不是定时任务**,而是完全由用户请求触发: ``` 用户访问首页/话题页 ↓ 检查 APCu 本地缓存(L1) ↓ 未命中 检查 Redis 分布式缓存(L2,TTL=15分钟) ↓ 未命中 投递任务到 cache_fill_queue ↓ 后台进程 process_sqlite_queue.php 消费队列 ↓ 执行 RecommendationEngine 计算 ↓ 结果写回 Redis + APCu ``` **等待策略**: - 前端轮询等待,最多 1.5 秒 - 超时返回热门兜底(随机热门话题) - 缓存有效期 15 分钟 这种设计的好处是: 1. **节省资源**:只有活跃用户才触发计算 2. **实时性好**:新内容发布 15 分钟后就可能被推荐 3. **容错性强**:超时或失败有兜底策略 --- ## 五、当前局限与未来方向 ### 已知问题 | 问题 | 影响 | 优先级 | |------|------|--------| | 中文单字分词 | 丢失词语语义 | 高 | | CF 无时间衰减 | 老内容权重过高 | 中 | | 无负反馈 | 无法学习"不感兴趣" | 中 | | 无 Item-based CF | 稀疏场景表现差 | 中 | ### 规划中的改进 **1. 引入中文分词** 考虑使用 jieba 或类似的轻量级分词器,把"机器学习"作为一个整体词处理,而不是拆成单字。 **2. Item-based CF** User-based CF 依赖用户相似度,Item-based 则是"看过 A 的人也看了 B"。对于内容社区,Item-based 通常更稳定、可解释性更强。 ```php // 物品相似度预计算 $itemSimilarities = computeItemSimilarities($userItemMatrix); // 缓存键: item_sim:{topic_id} // 定时更新,实时查表 ``` **3. 时间衰减因子** ```php // 引入指数衰减 $timeDecay = exp(-$daysAgo / 30); // 30天半衰期 $weight = $baseWeight * $timeDecay; ``` **4. 标签体系** 如果用户愿意给话题打标签,可以引入标签相似度作为第三路信号。 --- ## 六、总结 这次调整把 CF 权重从 60% 降到 20%,本质是**在数据稀疏的场景下,让更稳定的内容信号占据主导**。这不是说 CF 没用,而是在当前数据规模和分布下,TF-IDF 的性价比更高。 推荐系统没有银弹。User-based CF、Item-based CF、Content-based、深度学习... 每种方法都有其适用场景。关键是根据业务阶段、数据规模、计算资源做 trade-off。 智柴目前的选择是:**简单可维护 + 渐进优化**。先把内容推荐做扎实,再逐步引入更复杂的协同策略。 --- 欢迎讨论,特别是关于中文分词策略和 Item-based CF 的实现思路。如果有同学做过类似的推荐系统,欢迎分享经验!

讨论回复

0 条回复

还没有人回复,快来发表你的看法吧!