1. 引言

本教程将带你深入理解 Slope One 算法 的 Java 实现。我们将通过一个完整的 协同过滤(CF) 案例展开,这是推荐系统常用的机器学习技术。

该技术可用于预测用户对特定物品的兴趣偏好

2. 协同过滤原理

Slope One 是一种 基于物品的协同过滤算法,完全依赖用户-物品评分数据。计算物品相似度时,我们只关注评分历史而非物品内容本身。这种相似度最终用于预测数据集中未出现的用户-物品评分组合

下图展示了协同过滤的完整流程:

协同过滤流程

  1. 用户对系统中的物品进行评分
  2. 算法计算物品间相似度
  3. 系统预测用户未评分物品的评分

关于协同过滤的更多细节可参考 维基百科

3. Slope One 算法详解

Slope One 因其简洁性而得名,是最简单的非平凡物品协同过滤算法。它同时考虑:

  • 所有评分同一物品的用户数据
  • 同一用户评分的其他物品数据

3.1 Java 模型设计

我们先构建核心领域模型:

// 物品类
private String itemName;

// 用户类
private String username;

通过 InputData 类初始化测试数据。假设商店有五种商品:

List<Item> items = Arrays.asList(
  new Item("糖果"), 
  new Item("饮料"), 
  new Item("苏打水"), 
  new Item("爆米花"), 
  new Item("零食")
);

创建三个用户,使用 0.0-1.0 评分(0=无兴趣,0.5=一般兴趣,1.0=非常感兴趣)。初始化后得到用户评分数据结构:

Map<User, HashMap<Item, Double>> data;

3.2 差异矩阵与频率矩阵

基于现有数据计算物品间关系及出现频次:

for (HashMap<Item, Double> user : data.values()) {
    for (Entry<Item, Double> e : user.entrySet()) {
        // 处理每个评分项
    }
}

关键步骤:

  1. 检查物品是否存在于矩阵,首次出现则创建新条目:

    if (!diff.containsKey(e.getKey())) {
        diff.put(e.getKey(), new HashMap<Item, Double>());
        freq.put(e.getKey(), new HashMap<Item, Integer>());
    }
    
  2. diff 矩阵存储评分差异(可为负值),freq 矩阵存储频次

  3. 遍历所有物品评分对计算差异:

    for (Entry<Item, Double> e2 : user.entrySet()) {
        int oldCount = freq.get(e.getKey()).getOrDefault(e2.getKey(), 0);
        double oldDiff = diff.get(e.getKey()).getOrDefault(e2.getKey(), 0.0);
        
        double observedDiff = e.getValue() - e2.getValue();
        freq.get(e.getKey()).put(e2.getKey(), oldCount + 1);
        diff.get(e.getKey()).put(e2.getKey(), oldDiff + observedDiff);
    }
    

    ⚠️ 注意:这里存储的是差异累计值而非平均值

  4. 计算最终相似度分数:

    for (Item j : diff.keySet()) {
        for (Item i : diff.get(j).keySet()) {
            double oldValue = diff.get(j).get(i);
            int count = freq.get(j).get(i);
            diff.get(j).put(i, oldValue / count);  // 计算平均差异
        }
    }
    

3.3 评分预测

算法核心步骤:预测缺失评分

for (Entry<User, HashMap<Item, Double>> e : data.entrySet()) {
    for (Item j : e.getValue().keySet()) {
        for (Item k : diff.keySet()) {
            double predictedValue = 
              diff.get(k).get(j) + e.getValue().get(j);
            double finalValue = predictedValue * freq.get(k).get(j);
            uPred.put(k, uPred.getOrDefault(k, 0.0) + finalValue);
            uFreq.put(k, uFreq.getOrDefault(k, 0) + freq.get(k).get(j));
        }
    }
    // 后续处理...
}

生成"干净"的预测结果:

HashMap<Item, Double> clean = new HashMap<>();
for (Item j : uPred.keySet()) {
    if (uFreq.get(j) > 0) {
        clean.put(j, uPred.get(j) / uFreq.get(j));
    }
}

for (Item j : InputData.items) {
    if (e.getValue().containsKey(j)) {
        clean.put(j, e.getValue().get(j));  // 保留原始评分
    } else if (!clean.containsKey(j)) {
        clean.put(j, -1.0);  // 无法预测的标记为-1
    }
}

验证要点:算法必须满足:

  • 预测用户未评分的物品
  • 用户已评分物品的预测值应与原始值一致
  • 若不一致,说明实现存在 bug

3.4 实战技巧

影响 Slope One 效果的关键因素:

优化方向 具体措施
数据获取 ✅ 大数据集建议在数据库端完成用户-物品评分查询
时间维度 ✅ 设置评分时间窗口(用户兴趣会随时间变化)
✅ 减少数据处理时间
数据分片 ✅ 无需每日计算所有用户预测
✅ 根据用户交互动态调整处理队列

4. 总结

本教程完整实现了 Slope One 算法,并展示了其在物品推荐系统中的协同过滤应用。

完整代码实现可在 GitHub 项目 中获取。


原始标题:A Collaborative Filtering Recommendation System in Java