推荐系统是互联网产品的核心技术之一,从电商购物到视频流媒体,无处不在。本文用Java实现两种经典推荐算法——基于用户的协同过滤与矩阵分解(Funk SVD),讲解相似度计算、邻居选择与隐向量训练,帮助读者理解推荐系统背后的算法原理与工程实现。
一、问题建模
推荐系统的核心任务可以抽象为:给定一个用户-物品评分矩阵R(m个用户×n个物品,其中大部分元素为空),预测用户对未评分物品的偏好程度,并推荐得分最高的物品。
import java.util.*;
/**
* 推荐系统核心算法实现
* 包含用户协同过滤与矩阵分解(SVD)
*/
public class RecommenderSystem {
// 用户数量
private int numUsers;
// 物品数量
private int numItems;
// 评分矩阵(稀疏表示:user -> {item -> rating})
private Map<Integer, Map<Integer, Double>> ratings;
// 用户平均评分
private Map<Integer, Double> userAvgRatings;
// 物品平均评分
private Map<Integer, Double> itemAvgRatings;
public RecommenderSystem(int numUsers, int numItems) {
this.numUsers = numUsers;
this.numItems = numItems;
this.ratings = new HashMap<>();
this.userAvgRatings = new HashMap<>();
this.itemAvgRatings = new HashMap<>();
}
/**
* 添加评分
*/
public void addRating(int user, int item, double rating) {
ratings.computeIfAbsent(user, k -> new HashMap<>()).put(item, rating);
}
/**
* 计算所有用户和物品的平均评分
*/
public void computeAverages() {
// 用户平均
for (Map.Entry<Integer, Map<Integer, Double>> entry : ratings.entrySet()) {
double sum = 0;
for (double r : entry.getValue().values()) {
sum += r;
}
userAvgRatings.put(entry.getKey(), sum / entry.getValue().size());
}
// 物品平均
Map<Integer, List<Double>> itemRatings = new HashMap<>();
for (Map<Integer, Double> userRatings : ratings.values()) {
for (Map.Entry<Integer, Double> e : userRatings.entrySet()) {
itemRatings.computeIfAbsent(e.getKey(), k -> new ArrayList<>()).add(e.getValue());
}
}
for (Map.Entry<Integer, List<Double>> entry : itemRatings.entrySet()) {
double sum = 0;
for (double r : entry.getValue()) {
sum += r;
}
itemAvgRatings.put(entry.getKey(), sum / entry.getValue().size());
}
}
二、基于用户的协同过滤
协同过滤的核心假设是”相似的用户有相似的偏好”。算法步骤为:找到与目标用户最相似的K个邻居,根据邻居的评分加权预测目标用户对未知物品的评分。
/**
* 计算两个用户之间的皮尔逊相关系数
*/
public double pearsonCorrelation(int user1, int user2) {
Map<Integer, Double> r1 = ratings.getOrDefault(user1, Collections.emptyMap());
Map<Integer, Double> r2 = ratings.getOrDefault(user2, Collections.emptyMap());
// 找到共同评分的物品
List<Double> common1 = new ArrayList<>();
List<Double> common2 = new ArrayList<>();
for (int item : r1.keySet()) {
if (r2.containsKey(item)) {
common1.add(r1.get(item));
common2.add(r2.get(item));
}
}
if (common1.size() < 2) {
return 0; // 共同评分太少,无法计算相关性
}
double avg1 = common1.stream().mapToDouble(Double::doubleValue).average().orElse(0);
double avg2 = common2.stream().mapToDouble(Double::doubleValue).average().orElse(0);
double numerator = 0;
double denom1 = 0;
double denom2 = 0;
for (int i = 0; i < common1.size(); i++) {
double diff1 = common1.get(i) - avg1;
double diff2 = common2.get(i) - avg2;
numerator += diff1 * diff2;
denom1 += diff1 * diff1;
denom2 += diff2 * diff2;
}
if (denom1 == 0 || denom2 == 0) {
return 0;
}
return numerator / Math.sqrt(denom1 * denom2);
}
/**
* 基于用户的协同过滤预测评分
* @param user 目标用户
* @param item 目标物品
* @param k 邻居数量
*/
public double userBasedCF(int user, int item, int k) {
if (!ratings.containsKey(user)) {
return itemAvgRatings.getOrDefault(item, 3.0);
}
// 计算该用户与其他所有用户的相似度
List<Neighbor> neighbors = new ArrayList<>();
for (int otherUser : ratings.keySet()) {
if (otherUser == user) continue;
Map<Integer, Double> otherRatings = ratings.get(otherUser);
if (!otherRatings.containsKey(item)) continue; // 邻居必须评分过该物品
double sim = pearsonCorrelation(user, otherUser);
if (sim > 0) { // 只考虑正相关用户
neighbors.add(new Neighbor(otherUser, sim, otherRatings.get(item)));
}
}
// 按相似度排序,取前K个
neighbors.sort((a, b) -> Double.compare(b.similarity, a.similarity));
if (neighbors.size() > k) {
neighbors = neighbors.subList(0, k);
}
if (neighbors.isEmpty()) {
return userAvgRatings.getOrDefault(user, itemAvgRatings.getOrDefault(item, 3.0));
}
// 加权预测
double numerator = 0;
double denominator = 0;
double userAvg = userAvgRatings.getOrDefault(user, 3.0);
for (Neighbor n : neighbors) {
double otherAvg = userAvgRatings.getOrDefault(n.userId, 3.0);
numerator += n.similarity * (n.rating - otherAvg);
denominator += Math.abs(n.similarity);
}
return userAvg + numerator / denominator;
}
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;
}
}
三、矩阵分解(Funk SVD)
矩阵分解将用户-物品评分矩阵R分解为两个低秩矩阵的乘积:R ≈ P × Q^T。其中P是用户隐向量矩阵,Q是物品隐向量矩阵。通过梯度下降优化,最小化预测评分与实际评分的误差。
// 隐向量维度
private int numFactors;
// 用户隐向量
private double[][] userFactors;
// 物品隐向量
private double[][] itemFactors;
// 用户偏置
private double[] userBias;
// 物品偏置
private double[] itemBias;
// 全局平均评分
private double globalAvg;
/**
* 初始化矩阵分解模型
* @param numFactors 隐向量维度
*/
public void initMatrixFactorization(int numFactors) {
this.numFactors = numFactors;
this.userFactors = new double[numUsers][numFactors];
this.itemFactors = new double[numItems][numFactors];
this.userBias = new double[numUsers];
this.itemBias = new double[numItems];
// 计算全局平均
double sum = 0;
int count = 0;
for (Map<Integer, Double> userRatings : ratings.values()) {
for (double r : userRatings.values()) {
sum += r;
count++;
}
}
this.globalAvg = count > 0 ? sum / count : 3.0;
// 随机初始化隐向量(标准正态分布)
Random rand = new Random(42);
for (int u = 0; u < numUsers; u++) {
for (int f = 0; f < numFactors; f++) {
userFactors[u][f] = rand.nextGaussian() * 0.01;
}
}
for (int i = 0; i < numItems; i++) {
for (int f = 0; f < numFactors; f++) {
itemFactors[i][f] = rand.nextGaussian() * 0.01;
}
}
}
/**
* 使用随机梯度下降训练矩阵分解模型
* @param learningRate 学习率
* @param regularization 正则化系数
* @param epochs 训练轮数
*/
public void trainSGD(double learningRate, double regularization, int epochs) {
// 将评分数据转换为列表,便于随机采样
List<RatingEntry> entries = new ArrayList<>();
for (Map.Entry<Integer, Map<Integer, Double>> e1 : ratings.entrySet()) {
for (Map.Entry<Integer, Double> e2 : e1.getValue().entrySet()) {
entries.add(new RatingEntry(e1.getKey(), e2.getKey(), e2.getValue()));
}
}
Random rand = new Random(42);
for (int epoch = 0; epoch < epochs; epoch++) {
// 随机打乱
Collections.shuffle(entries, rand);
double totalLoss = 0;
for (RatingEntry entry : entries) {
int u = entry.user;
int i = entry.item;
double r = entry.rating;
// 预测评分
double pred = predictMF(u, i);
double error = r - pred;
totalLoss += error * error;
// 更新偏置
userBias[u] += learningRate * (error - regularization * userBias[u]);
itemBias[i] += learningRate * (error - regularization * itemBias[i]);
// 更新隐向量
for (int f = 0; f < numFactors; f++) {
double uf = userFactors[u][f];
double itemf = itemFactors[i][f];
userFactors[u][f] += learningRate * (error * itemf - regularization * uf);
itemFactors[i][f] += learningRate * (error * uf - regularization * itemf);
}
}
if (epoch % 10 == 0) {
double rmse = Math.sqrt(totalLoss / entries.size());
System.out.printf("Epoch %d, RMSE: %.4f%n", epoch, rmse);
}
}
}
/**
* 矩阵分解预测评分
*/
public double predictMF(int user, int item) {
if (user < 0 || user >= numUsers || item < 0 || item >= numItems) {
return globalAvg;
}
double pred = globalAvg + userBias[user] + itemBias[item];
for (int f = 0; f < numFactors; f++) {
pred += userFactors[user][f] * itemFactors[item][f];
}
// 限制评分范围
return Math.max(1.0, Math.min(5.0, pred));
}
private static class RatingEntry {
int user, item;
double rating;
RatingEntry(int user, int item, double rating) {
this.user = user;
this.item = item;
this.rating = rating;
}
}
四、推荐生成与评估
/**
* 为用户生成Top-N推荐
* @param user 目标用户
* @param n 推荐数量
* @param useMF 是否使用矩阵分解(否则使用协同过滤)
*/
public List<Recommendation> recommend(int user, int n, boolean useMF) {
Set<Integer> ratedItems = ratings.getOrDefault(user, Collections.emptyMap()).keySet();
List<Recommendation> candidates = new ArrayList<>();
for (int item = 0; item < numItems; item++) {
if (ratedItems.contains(item)) continue; // 跳过已评分物品
double score;
if (useMF) {
score = predictMF(user, item);
} else {
score = userBasedCF(user, item, 10);
}
candidates.add(new Recommendation(item, score));
}
// 按预测评分降序排序
candidates.sort((a, b) -> Double.compare(b.score, a.score));
return candidates.size() > n ? candidates.subList(0, n) : candidates;
}
/**
* 计算RMSE评估模型
*/
public double evaluateRMSE(boolean useMF) {
double sumSquaredError = 0;
int count = 0;
for (Map.Entry<Integer, Map<Integer, Double>> e1 : ratings.entrySet()) {
for (Map.Entry<Integer, Double> e2 : e1.getValue().entrySet()) {
int user = e1.getKey();
int item = e2.getKey();
double actual = e2.getValue();
double predicted = useMF ? predictMF(user, item) : userBasedCF(user, item, 10);
sumSquaredError += (actual - predicted) * (actual - predicted);
count++;
}
}
return Math.sqrt(sumSquaredError / count);
}
public static class Recommendation {
public int itemId;
public double score;
public Recommendation(int itemId, double score) {
this.itemId = itemId;
this.score = score;
}
@Override
public String toString() {
return String.format("物品%d (预测评分: %.2f)", itemId, score);
}
}
public static void main(String[] args) {
// 构造测试数据:10个用户,20个物品
RecommenderSystem rs = new RecommenderSystem(10, 20);
Random rand = new Random(42);
// 随机生成评分(1-5分)
for (int u = 0; u < 10; u++) {
for (int i = 0; i < 20; i++) {
if (rand.nextDouble() < 0.3) { // 30%的填充率
double rating = 1 + rand.nextInt(5);
rs.addRating(u, i, rating);
}
}
}
rs.computeAverages();
// 测试协同过滤
System.out.println("=== 用户协同过滤 ===");
double cfRMSE = rs.evaluateRMSE(false);
System.out.printf("CF RMSE: %.4f%n", cfRMSE);
List<Recommendation> cfRecs = rs.recommend(0, 5, false);
System.out.println("为用户0推荐(协同过滤):");
for (Recommendation rec : cfRecs) {
System.out.println(" " + rec);
}
// 测试矩阵分解
System.out.println("\n=== 矩阵分解(SVD) ===");
rs.initMatrixFactorization(10);
rs.trainSGD(0.01, 0.02, 100);
double mfRMSE = rs.evaluateRMSE(true);
System.out.printf("MF RMSE: %.4f%n", mfRMSE);
List<Recommendation> mfRecs = rs.recommend(0, 5, true);
System.out.println("为用户0推荐(矩阵分解):");
for (Recommendation rec : mfRecs) {
System.out.println(" " + rec);
}
}
}
五、复杂度分析
- 协同过滤:
- 相似度计算:O(m × n) 预处理,每次预测O(m);
- 邻居搜索:O(m log m) 排序取Top-K;
-
适合用户/物品数量较小的场景。
-
矩阵分解:
- 训练复杂度:O(epochs × |R| × k),|R|为评分数量,k为隐向量维度;
- 预测复杂度:O(k),非常高效;
- 适合大规模推荐场景。
六、总结
本文实现了推荐系统的两种经典算法:
- 用户协同过滤:基于皮尔逊相关系数度量用户相似度,通过邻居加权预测未知评分,原理直观、可解释性强;
- 矩阵分解(Funk SVD):将高维评分矩阵分解为低维隐向量乘积,通过随机梯度下降训练,预测效率高且能捕捉深层语义关联。
读者可以在此基础上扩展更多功能,如加入时间衰减因子、融合物品内容特征、实现基于物品的协同过滤,或探索更先进的深度学习推荐模型(如Neural Collaborative Filtering)。