引言
在信息爆炸的时代,推荐系统已经成为连接用户与内容的桥梁。从电商平台的”猜你喜欢”到视频网站的”为你推荐”,背后都离不开推荐算法的支撑。协同过滤(Collaborative Filtering)是推荐系统中最经典、最核心的算法范式之一,其核心思想简单而深刻:”与你兴趣相似的用户喜欢的物品,你可能也会喜欢”。本文将用 Java 完整实现基于用户的协同过滤算法(User-Based CF),涵盖用户相似度计算、邻居选取、评分预测与 Top-N 推荐等完整流程,并在一个电影评分数据集上进行演示。
协同过滤的核心思想
协同过滤不依赖物品本身的特征描述,而是完全基于用户的历史行为数据(评分、购买、浏览等)进行推荐。它基于两个基本假设:
- 用户相似性假设:如果两个用户在过去对很多物品的评价一致,那么他们对其他物品的评价也可能一致
- 物品相似性假设:如果两个物品被很多相同的用户喜欢,那么它们可能具有相似的属性
基于用户的协同过滤(User-Based CF)是协同过滤最早的形式,其推荐流程为:
– 计算目标用户与其他用户的相似度
– 选取最相似的 K 个邻居用户
– 根据邻居的评分预测目标用户对未评分物品的评分
– 推荐预测评分最高的 N 个物品
相似度度量方法
余弦相似度
将用户的评分向量看作高维空间中的向量,计算它们夹角的余弦值:
sim(u, v) = (Σ r_u,i × r_v,i) / (√Σ r_u,i² × √Σ r_v,i²)
余弦相似度取值范围为 [-1, 1],值越大表示用户越相似。
皮尔逊相关系数
考虑了用户评分偏差的差异,对评分尺度不同的用户更公平:
sim(u, v) = Σ(r_u,i - r̄_u)(r_v,i - r̄_v) / (√Σ(r_u,i - r̄_u)² × √Σ(r_v,i - r̄_v)²)
Java 实现
数据结构定义
import java.util.*;
/**
* 用户-物品评分数据模型
*/
public class Rating {
public final int userId;
public final int itemId;
public final double score;
public Rating(int userId, int itemId, double score) {
this.userId = userId;
this.itemId = itemId;
this.score = score;
}
}
/**
* 基于用户的协同过滤推荐系统
*/
public class UserBasedCF {
// userId -> {itemId -> score}
private Map<Integer, Map<Integer, Double>> userRatings;
// itemId -> {userId -> score}
private Map<Integer, Map<Integer, Double>> itemRatings;
// userId -> 平均评分
private Map<Integer, Double> userMeanRatings;
private int kNeighbors; // 邻居数量 K
public UserBasedCF(int k) {
this.kNeighbors = k;
this.userRatings = new HashMap<>();
this.itemRatings = new HashMap<>();
this.userMeanRatings = new HashMap<>();
}
数据加载与预处理
“`java /*
* 加载评分数据
* @param ratings 评分列表
/
public void loadData(List
for (Rating r : ratings) {
// 构建用户-物品评分矩阵
userRatings.computeIfAbsent(r.userId, k -> new HashMap<>())
.put(r.itemId, r.score);
// 构建物品-用户评分矩阵(用于快速查找共同评分物品)
itemRatings.computeIfAbsent(r.itemId, k -> new HashMap<>())
.put(r.userId, r.score);
}
// 计算每个用户的平均评分
for (Map.Entry<Integer, Map<Integer, Double>> entry : userRatings.entrySet()) {
double sum = 0;
for (double score : entry.getValue().values()) {
sum += score;
}
userMeanRatings.put(entry.getKey(), sum / entry.getValue().size());
}
}
### 余弦相似度计算
```java /**
* 计算两个用户的余弦相似度
* @param userA 用户A的ID
* @param userB 用户B的ID
* @return 余弦相似度,范围 [-1, 1]
*/
public double cosineSimilarity(int userA, int userB) {
Map<Integer, Double> ratingsA = userRatings.get(userA);
Map<Integer, Double> ratingsB = userRatings.get(userB);
if (ratingsA == null || ratingsB == null) return 0;
// 找到两个用户共同评分的物品
double dotProduct = 0;
double normA = 0;
double normB = 0;
boolean hasCommon = false;
for (Map.Entry<Integer, Double> entry : ratingsA.entrySet()) {
int itemId = entry.getKey();
double scoreA = entry.getValue();
normA += scoreA * scoreA;
Double scoreB = ratingsB.get(itemId);
if (scoreB != null) {
dotProduct += scoreA * scoreB;
hasCommon = true;
}
}
for (double scoreB : ratingsB.values()) {
normB += scoreB * scoreB;
}
if (!hasCommon || normA == 0 || normB == 0) return 0;
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
皮尔逊相关系数
“`java /*
* 计算两个用户的皮尔逊相关系数
* 考虑了用户评分的均值偏差,对评分尺度不同的用户更公平
/
public double pearsonCorrelation(int userA, int userB) {
Map
Map
if (ratingsA == null || ratingsB == null) return 0;
double meanA = userMeanRatings.get(userA);
double meanB = userMeanRatings.get(userB);
double numerator = 0;
double denomA = 0;
double denomB = 0;
boolean hasCommon = false;
for (Map.Entry<Integer, Double> entry : ratingsA.entrySet()) {
int itemId = entry.getKey();
Double scoreB = ratingsB.get(itemId);
if (scoreB == null) continue;
double devA = entry.getValue() - meanA;
double devB = scoreB - meanB;
numerator += devA * devB;
denomA += devA * devA;
denomB += devB * devB;
hasCommon = true;
}
if (!hasCommon || denomA == 0 || denomB == 0) return 0;
return numerator / Math.sqrt(denomA * denomB);
}
### 评分预测与推荐生成
```java /**
* 预测目标用户对指定物品的评分
* 使用加权平均:邻居的相似度作为权重
* @param targetUser 目标用户ID
* @param targetItem 目标物品ID
* @param usePearson 是否使用皮尔逊相关系数(否则用余弦相似度)
* @return 预测评分
*/
public double predictRating(int targetUser, int targetItem, boolean usePearson) {
// 如果用户已经评分,直接返回
Map<Integer, Double> targetRatings = userRatings.get(targetUser);
if (targetRatings != null && targetRatings.containsKey(targetItem)) {
return targetRatings.get(targetItem);
}
// 找到对目标物品评分的所有用户
Map<Integer, Double> itemRaters = itemRatings.get(targetItem);
if (itemRaters == null || itemRaters.isEmpty()) {
return userMeanRatings.getOrDefault(targetUser, 3.0);
}
// 计算目标用户与所有评分用户的相似度
List<Neighbor> neighbors = new ArrayList<>();
for (int otherUser : itemRaters.keySet()) {
if (otherUser == targetUser) continue;
double sim = usePearson
? pearsonCorrelation(targetUser, otherUser)
: cosineSimilarity(targetUser, otherUser);
if (sim > 0) { // 只考虑正相似度的邻居
neighbors.add(new Neighbor(otherUser, sim, itemRaters.get(otherUser)));
}
}
// 按相似度降序排序,取前 K 个邻居
neighbors.sort((a, b) -> Double.compare(b.similarity, a.similarity));
if (neighbors.size() > kNeighbors) {
neighbors = neighbors.subList(0, kNeighbors);
}
// 加权平均预测评分
double weightedSum = 0;
double simSum = 0;
for (Neighbor n : neighbors) {
weightedSum += n.similarity * n.rating;
simSum += Math.abs(n.similarity);
}
if (simSum == 0) {
return userMeanRatings.getOrDefault(targetUser, 3.0);
}
return weightedSum / simSum;
}
/**
* 为指定用户生成 Top-N 推荐
* @param targetUser 目标用户ID
* @param topN 推荐数量
* @param usePearson 是否使用皮尔逊相关系数
* @return 推荐物品ID列表及预测评分
*/
public List<Map.Entry<Integer, Double>> recommend(int targetUser, int topN, boolean usePearson) {
Set<Integer> ratedItems = userRatings.getOrDefault(targetUser, Collections.emptyMap()).keySet();
// 获取所有物品
Set<Integer> allItems = itemRatings.keySet();
// 预测用户对未评分物品的兴趣
Map<Integer, Double> predictions = new HashMap<>();
for (int itemId : allItems) {
if (!ratedItems.contains(itemId)) {
double pred = predictRating(targetUser, itemId, usePearson);
predictions.put(itemId, pred);
}
}
// 按预测评分降序排序,取 Top-N
List<Map.Entry<Integer, Double>> result = new ArrayList<>(predictions.entrySet());
result.sort((a, b) -> Double.compare(b.getValue(), a.getValue()));
if (result.size() > topN) {
return result.subList(0, topN);
}
return result;
}
/**
* 邻居辅助类
*/
private static class Neighbor {
int userId;
double similarity;
double rating;
Neighbor(int userId, double similarity, double rating) {
this.userId = userId;
this.similarity = similarity;
this.rating = rating;
}
}
}
完整测试:电影推荐
public class CollaborativeFilteringDemo {
public static void main(String[] args) {
// 模拟电影评分数据 (userId, movieId, rating)
List<Rating> ratings = Arrays.asList(
new Rating(1, 101, 5.0), new Rating(1, 102, 3.0), new Rating(1, 103, 4.0),
new Rating(1, 104, 2.0), new Rating(1, 105, 4.5),
new Rating(2, 101, 4.5), new Rating(2, 102, 2.5), new Rating(2, 103, 5.0),
new Rating(2, 106, 3.5), new Rating(2, 107, 4.0),
new Rating(3, 102, 4.0), new Rating(3, 103, 3.5), new Rating(3, 104, 5.0),
new Rating(3, 105, 3.0), new Rating(3, 108, 4.5),
new Rating(4, 101, 5.0), new Rating(4, 103, 4.5), new Rating(4, 105, 4.0),
new Rating(4, 106, 2.0), new Rating(4, 109, 3.5),
new Rating(5, 102, 3.0), new Rating(5, 104, 4.5), new Rating(5, 106, 5.0),
new Rating(5, 107, 2.5), new Rating(5, 108, 4.0)
);
// 物品名称映射
Map<Integer, String> movieNames = new HashMap<>();
movieNames.put(101, "星际穿越");
movieNames.put(102, "盗梦空间");
movieNames.put(103, "黑客帝国");
movieNames.put(104, "肖申克的救赎");
movieNames.put(105, "阿甘正传");
movieNames.put(106, "泰坦尼克号");
movieNames.put(107, "千与千寻");
movieNames.put(108, "霸王别姬");
movieNames.put(109, "楚门的世界");
UserBasedCF cf = new UserBasedCF(3); // K=3 个邻居
cf.loadData(ratings);
// 测试相似度计算
System.out.println("===== 用户相似度(余弦) =====");
for (int u = 1; u <= 5; u++) {
if (u == 1) continue;
System.out.printf("用户1与用户%d 的余弦相似度: %.4f%n",
u, cf.cosineSimilarity(1, u));
}
System.out.println("\n===== 用户相似度(皮尔逊) =====");
for (int u = 1; u <= 5; u++) {
if (u == 1) continue;
System.out.printf("用户1与用户%d 的皮尔逊相关系数: %.4f%n",
u, cf.pearsonCorrelation(1, u));
}
// 为每个用户推荐
System.out.println("\n===== Top-3 电影推荐(余弦相似度) =====");
for (int user = 1; user <= 5; user++) {
System.out.println("\n用户 " + user + " 的推荐:");
List<Map.Entry<Integer, Double>> recs = cf.recommend(user, 3, false);
for (Map.Entry<Integer, Double> rec : recs) {
System.out.printf(" %s (预测评分: %.2f)%n",
movieNames.get(rec.getKey()), rec.getValue());
}
}
// 评分预测示例
System.out.println("\n===== 评分预测示例 =====");
System.out.printf("预测用户1对《泰坦尼克号》(106)的评分: %.2f%n",
cf.predictRating(1, 106, false));
System.out.printf("预测用户2对《阿甘正传》(105)的评分: %.2f%n",
cf.predictRating(2, 105, false));
}
}
算法复杂度分析
| 阶段 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 数据加载 | O(R) | O(R) |
| 相似度计算(单次) | O(I_uv) | O(1) |
| 预测评分 | O(U × I_uv + K log U) | O(U) |
| Top-N 推荐 | O(U × I + I log I) | O(I) |
其中 R 为总评分数,U 为用户数,I 为物品数,I_uv 为用户 u 和 v 的共同评分物品数,K 为邻居数。
协同过滤的优缺点
| 优点 | 缺点 |
|---|---|
| 无需理解物品内容,通用性强 | 冷启动问题:新用户/新物品缺乏历史数据 |
| 能发现用户潜在兴趣(惊喜推荐) | 数据稀疏性:用户-物品矩阵通常非常稀疏 |
| 可解释性好(”和你相似的人喜欢”) | 扩展性差:用户/物品增多时计算量剧增 |
| 实现简单,效果直观 | 流行度偏差:热门物品更容易被推荐 |
总结
协同过滤是推荐系统领域的奠基性算法,其”物以类聚、人以群分”的思想朴素而强大。基于用户的协同过滤通过余弦相似度或皮尔逊相关系数量化用户间的口味相似度,再借助邻居用户的评分进行加权预测,完成了从”找相似的人”到”推合适的物”的完整闭环。尽管现代推荐系统已经发展到深度学习和图神经网络阶段,协同过滤的思想仍深深嵌入其中——无论是矩阵分解还是神经协同过滤,本质上都是在更高效地学习用户与物品之间的隐式关联。掌握协同过滤,就掌握了推荐系统最原始也最重要的算法直觉。