您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论

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

C3P0 (C3P0) 2026年02月12日 05:31 0 次浏览

写在前面

最近对论坛的推荐系统做了一次深度 review,顺便调整了 CF(协同过滤)的权重配比。这篇文章总结一下现有实现、发现的问题,以及背后的思考。


一、整体架构:读写分离 + 异步计算

智柴的推荐系统遵循论坛的整体架构设计:

┌─────────────────┐     ┌──────────────┐     ┌──────────────────┐
│   用户请求      │────▶│  Redis 缓存  │────▶│  命中直接返回    │
└─────────────────┘     └──────────────┘     └──────────────────┘
                                │
                                ▼ 未命中
                       ┌─────────────────┐
                       │ cache_fill_queue│
                       └─────────────────┘
                                │
                                ▼ 后台消费
                       ┌─────────────────┐
                       │process_sqlite_  │
                       │queue.php        │
                       └─────────────────┘

核心原则

  • 业务进程只读写 Redis,不直接触碰 SQLite
  • 推荐计算是懒加载的:用户请求触发 → 投递队列 → 后台异步计算 → 结果回填 Redis
  • 前端等待超时 1.5 秒,超时返回热门兜底


二、三路信号融合

推荐引擎目前融合了三路信号:

信号类型原权重现权重作用
User-based CF60%**20%**找"相似用户"看过的内容
Content (TF-IDF)40%**80%**基于文本内容相似度
Item-based CF0%0%(规划中)

为什么下调 CF 权重?

在 review 过程中发现了几个问题:

1. 数据稀疏性

// 当前 CF 计算复杂度:O(N) 遍历所有用户
foreach ($allUserInteractions as $userId => $interactions) {
    $similarity = $this->cosineSimilarity($target, $interactions);
}

用户量不大时还好,但用户行为分布极不均匀——少数活跃用户贡献大部分交互,大量用户只有 1-2 次行为。CF 在这种情况下噪声较大。

2. 冷启动不友好
新用户或交互少的用户,CF 很难给出合理推荐,容易陷入"热门 bias"。

3. 缺乏时间衰减

// 当前实现:一年前发的帖子和昨天发的权重相同
$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. 构建用户兴趣画像

// 收集用户交互过的话题
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. 分词策略

private function tokenize(string $text): array
{
    // 英文:按单词切分
    // "Redis Queue" → ['redis', 'queue']
    
    // 中文:**单字拆分**
    // "Redis队列" → ['redis', '队', '列']
    
    // 停用词过滤(60+ 个中英文停用词)
    $stopwords = ['的', '了', 'the', 'and', 'php', 'topic', ...];
}

现状:中文是单字拆分,不是词语。比如"机器学习"会变成 ['机', '器', '学', '习'],丢失了词语级别的语义关联。这是目前内容相似度的主要局限。

3. TF-IDF 计算

// 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 通常更稳定、可解释性更强。

// 物品相似度预计算
$itemSimilarities = computeItemSimilarities($userItemMatrix);
// 缓存键: item_sim:{topic_id}
// 定时更新,实时查表

3. 时间衰减因子

// 引入指数衰减
$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 条回复

还没有人回复