我为您创建一个完整的Java基于内容推荐系统案例,这个案例将演示如何根据用户历史行为推荐相似物品。

完整案例:电影推荐系统(基于内容)
项目结构
content-based-recommendation/
├── src/
│ ├── main/
│ │ ├── java/com/example/recommend/
│ │ │ ├── model/
│ │ │ │ ├── Movie.java
│ │ │ │ └── User.java
│ │ │ ├── service/
│ │ │ │ ├── ContentBasedRecommender.java
│ │ │ │ └── SimilarityCalculator.java
│ │ │ └── Main.java
│ │ └── resources/
│ │ └── movies.csv
核心代码实现
Movie.java - 电影模型
package com.example.recommend.model;
import java.util.*;
import java.util.stream.Collectors;
public class Movie {
private String id;
private String title;
private List<String> genres;
private List<String> directors;
private List<String> actors;
private double rating;
private int year;
public Movie(String id, String title, List<String> genres,
List<String> directors, List<String> actors,
double rating, int year) {
this.id = id;
this.title = title;
this.genres = genres;
this.directors = directors;
this.actors = actors;
this.rating = rating;
this.year = year;
}
// Getters and Setters
public String getId() { return id; }
public String getTitle() { return title; }
public List<String> getGenres() { return genres; }
public List<String> getDirectors() { return directors; }
public List<String> getActors() { return actors; }
public double getRating() { return rating; }
public int getYear() { return year; }
// 获取所有特征的合并字符串(用于TF-IDF或特征向量构建)
public String getAllFeatures() {
StringBuilder sb = new StringBuilder();
sb.append(String.join(" ", genres)).append(" ");
sb.append(String.join(" ", directors)).append(" ");
sb.append(String.join(" ", actors));
return sb.toString().toLowerCase();
}
// 获取特征向量(用于相似度计算)
public Map<String, Double> getFeatureVector() {
Map<String, Double> vector = new HashMap<>();
// 类型特征权重高
for (String genre : genres) {
vector.merge("genre_" + genre.toLowerCase(), 3.0, Double::sum);
}
// 导演特征
for (String director : directors) {
vector.merge("director_" + director.toLowerCase(), 2.0, Double::sum);
}
// 演员特征
for (String actor : actors) {
vector.merge("actor_" + actor.toLowerCase(), 1.5, Double::sum);
}
// 年份特征
vector.put("year_" + year, 0.5);
return vector;
}
@Override
public String toString() {
return String.format("Movie{id='%s', title='%s', genres=%s, rating=%.1f, year=%d}",
id, title, genres, rating, year);
}
}
User.java - 用户模型
package com.example.recommend.model;
import java.util.*;
public class User {
private String userId;
private Map<String, Double> ratedMovies; // movieId -> rating
private List<String> watchedMovies; // 观看历史
public User(String userId) {
this.userId = userId;
this.ratedMovies = new HashMap<>();
this.watchedMovies = new ArrayList<>();
}
public void rateMovie(String movieId, double rating) {
ratedMovies.put(movieId, rating);
if (!watchedMovies.contains(movieId)) {
watchedMovies.add(movieId);
}
}
public void watchMovie(String movieId) {
if (!watchedMovies.contains(movieId)) {
watchedMovies.add(movieId);
}
}
// Getters
public String getUserId() { return userId; }
public Map<String, Double> getRatedMovies() { return ratedMovies; }
public List<String> getWatchedMovies() { return watchedMovies; }
// 获取用户偏好特征(基于评分电影)
public Map<String, Double> getUserPreferenceVector(Map<String, Movie> allMovies) {
Map<String, Double> preference = new HashMap<>();
for (Map.Entry<String, Double> entry : ratedMovies.entrySet()) {
Movie movie = allMovies.get(entry.getKey());
if (movie != null) {
double weight = entry.getValue() / 5.0; // 归一化评分
for (Map.Entry<String, Double> feature : movie.getFeatureVector().entrySet()) {
preference.merge(feature.getKey(), feature.getValue() * weight, Double::sum);
}
}
}
return preference;
}
// 获取用户看过的电影ID集合
public Set<String> getWatchedMovieIds() {
return new HashSet<>(watchedMovies);
}
}
ContentBasedRecommender.java - 推荐系统核心
package com.example.recommend.service;
import com.example.recommend.model.Movie;
import com.example.recommend.model.User;
import java.util.*;
import java.util.stream.Collectors;
public class ContentBasedRecommender {
private final Map<String, Movie> movieCatalog;
private final SimilarityCalculator similarityCalculator;
public ContentBasedRecommender(List<Movie> movies) {
this.movieCatalog = new HashMap<>();
for (Movie movie : movies) {
movieCatalog.put(movie.getId(), movie);
}
this.similarityCalculator = new SimilarityCalculator();
}
/**
* 为用户推荐电影
* @param user 用户
* @param topN 推荐数量
* @param excludeWatched 是否排除已观看电影
* @return 推荐结果列表(电影+相似度评分)
*/
public List<Map.Entry<Movie, Double>> recommend(User user, int topN, boolean excludeWatched) {
// 获取用户偏好向量
Map<String, Double> userPreference = user.getUserPreferenceVector(movieCatalog);
if (userPreference.isEmpty()) {
return getPopularMovies(topN); // 如果没有偏好,推荐热门电影
}
// 计算所有候选电影的得分
List<Map.Entry<Movie, Double>> scoredMovies = new ArrayList<>();
Set<String> excludeIds = excludeWatched ? user.getWatchedMovieIds() : new HashSet<>();
for (Movie movie : movieCatalog.values()) {
if (excludeIds.contains(movie.getId())) {
continue; // 跳过已观看的电影
}
double score = calculateScore(movie, userPreference);
scoredMovies.add(new AbstractMap.SimpleEntry<>(movie, score));
}
// 按分数降序排序并返回Top N
return scoredMovies.stream()
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.limit(topN)
.collect(Collectors.toList());
}
/**
* 找到与目标电影相似的其他电影
*/
public List<Map.Entry<Movie, Double>> findSimilarMovies(String movieId, int topN) {
Movie targetMovie = movieCatalog.get(movieId);
if (targetMovie == null) {
return Collections.emptyList();
}
Map<String, Double> targetVector = targetMovie.getFeatureVector();
List<Map.Entry<Movie, Double>> similarMovies = new ArrayList<>();
for (Movie movie : movieCatalog.values()) {
if (movie.getId().equals(movieId)) {
continue; // 跳过自身
}
double similarity = similarityCalculator.cosineSimilarity(
targetVector, movie.getFeatureVector());
similarMovies.add(new AbstractMap.SimpleEntry<>(movie, similarity));
}
return similarMovies.stream()
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.limit(topN)
.collect(Collectors.toList());
}
/**
* 基于用户偏好的混合推荐(综合评分和相似度)
*/
public List<Map.Entry<Movie, Double>> hybridRecommend(User user, int topN) {
// 方法1:基于用户偏好向量
List<Map.Entry<Movie, Double>> contentBased = recommend(user, topN * 3, true);
// 方法2:基于用户高评分电影的相似电影
Map<String, Double> ratedMovies = user.getRatedMovies();
Map<String, Double> similarScores = new HashMap<>();
Set<String> watchedMovies = user.getWatchedMovieIds();
for (Map.Entry<String, Double> entry : ratedMovies.entrySet()) {
if (entry.getValue() >= 4.0) { // 只考虑高评分电影
for (Map.Entry<Movie, Double> similar : findSimilarMovies(entry.getKey(), 5)) {
Movie movie = similar.getKey();
if (!watchedMovies.contains(movie.getId())) {
similarScores.merge(movie.getId(), similar.getValue() * (entry.getValue() / 5.0), Double::sum);
}
}
}
}
// 合并两种推荐结果
Map<String, Double> combinedScores = new HashMap<>();
// 加入基于内容的推荐分数
for (Map.Entry<Movie, Double> entry : contentBased) {
combinedScores.merge(entry.getKey().getId(), entry.getValue() * 0.7, Double::sum);
}
// 加入基于相似电影的推荐分数
for (Map.Entry<String, Double> entry : similarScores.entrySet()) {
combinedScores.merge(entry.getKey(), entry.getValue() * 0.3, Double::sum);
}
// 转换为结果列表并排序
List<Map.Entry<Movie, Double>> results = new ArrayList<>();
for (Map.Entry<String, Double> entry : combinedScores.entrySet()) {
Movie movie = movieCatalog.get(entry.getKey());
if (movie != null) {
double finalScore = entry.getValue() + movie.getRating() * 0.1; // 加入评分加权
results.add(new AbstractMap.SimpleEntry<>(movie, finalScore));
}
}
return results.stream()
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.limit(topN)
.collect(Collectors.toList());
}
/**
* 计算电影与用户偏好的匹配度
*/
private double calculateScore(Movie movie, Map<String, Double> userPreference) {
Map<String, Double> movieVector = movie.getFeatureVector();
double similarity = similarityCalculator.cosineSimilarity(userPreference, movieVector);
// 结合电影评分进行加权
double ratingWeight = movie.getRating() / 5.0 * 0.3;
return similarity * 0.7 + ratingWeight;
}
/**
* 获取热门电影(用于无偏好用户的推荐)
*/
private List<Map.Entry<Movie, Double>> getPopularMovies(int topN) {
return movieCatalog.values().stream()
.sorted((a, b) -> Double.compare(b.getRating(), a.getRating()))
.limit(topN)
.map(movie -> new AbstractMap.SimpleEntry<>(movie, movie.getRating()))
.collect(Collectors.toList());
}
/**
* 获取全部电影
*/
public List<Movie> getAllMovies() {
return new ArrayList<>(movieCatalog.values());
}
/**
* 获取电影
*/
public Movie getMovie(String movieId) {
return movieCatalog.get(movieId);
}
}
SimilarityCalculator.java - 相似度计算
package com.example.recommend.service;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class SimilarityCalculator {
/**
* 计算两个特征向量的余弦相似度
*/
public double cosineSimilarity(Map<String, Double> vector1, Map<String, Double> vector2) {
if (vector1 == null || vector2 == null || vector1.isEmpty() || vector2.isEmpty()) {
return 0.0;
}
double dotProduct = 0.0;
double norm1 = 0.0;
double norm2 = 0.0;
// 计算点积
for (Map.Entry<String, Double> entry : vector1.entrySet()) {
String feature = entry.getKey();
double value1 = entry.getValue();
if (vector2.containsKey(feature)) {
dotProduct += value1 * vector2.get(feature);
}
norm1 += value1 * value1;
}
// 计算向量2的范数
for (double value : vector2.values()) {
norm2 += value * value;
}
if (norm1 == 0 || norm2 == 0) {
return 0.0;
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
/**
* 计算Jaccard相似度(适用于标签、特征交集)
*/
public double jaccardSimilarity(Set<String> set1, Set<String> set2) {
if (set1.isEmpty() || set2.isEmpty()) {
return 0.0;
}
Set<String> intersection = new java.util.HashSet<>(set1);
intersection.retainAll(set2);
Set<String> union = new java.util.HashSet<>(set1);
union.addAll(set2);
return (double) intersection.size() / union.size();
}
/**
* 计算欧氏距离相似度
*/
public double euclideanDistanceSimilarity(Map<String, Double> vector1, Map<String, Double> vector2) {
Set<String> allKeys = new java.util.HashSet<>(vector1.keySet());
allKeys.addAll(vector2.keySet());
double sum = 0.0;
for (String key : allKeys) {
double v1 = vector1.getOrDefault(key, 0.0);
double v2 = vector2.getOrDefault(key, 0.0);
sum += Math.pow(v1 - v2, 2);
}
double distance = Math.sqrt(sum);
return 1.0 / (1.0 + distance); // 转换为相似度分数
}
/**
* 皮尔逊相关系数(适用于评分数据)
*/
public double pearsonCorrelation(Map<String, Double> ratings1, Map<String, Double> ratings2) {
// 找出共同评分的项目
Set<String> commonItems = new java.util.HashSet<>(ratings1.keySet());
commonItems.retainAll(ratings2.keySet());
if (commonItems.size() < 2) {
return 0.0;
}
double sum1 = 0, sum2 = 0, sumSq1 = 0, sumSq2 = 0, sumProduct = 0;
int n = commonItems.size();
for (String item : commonItems) {
double r1 = ratings1.get(item);
double r2 = ratings2.get(item);
sum1 += r1;
sum2 += r2;
sumSq1 += r1 * r1;
sumSq2 += r2 * r2;
sumProduct += r1 * r2;
}
double numerator = n * sumProduct - sum1 * sum2;
double denominator = Math.sqrt((n * sumSq1 - sum1 * sum1) * (n * sumSq2 - sum2 * sum2));
if (denominator == 0) {
return 0.0;
}
return numerator / denominator;
}
}
Main.java - 主程序示例
package com.example.recommend;
import com.example.recommend.model.Movie;
import com.example.recommend.model.User;
import com.example.recommend.service.ContentBasedRecommender;
import java.util.*;
public class Main {
public static void main(String[] args) {
// 1. 创建电影数据
List<Movie> movies = createMovieCatalog();
// 2. 创建推荐系统
ContentBasedRecommender recommender = new ContentBasedRecommender(movies);
// 3. 演示1:基于用户评分推荐
System.out.println("=== 演示1:基于用户评分的推荐 ===");
User user1 = new User("User001");
// 用户对几部科幻电影评高分
user1.rateMovie("M001", 5.0);
user1.rateMovie("M003", 4.5);
user1.rateMovie("M005", 4.0);
System.out.println("用户偏好特征:");
printUserPreference(user1, recommender);
System.out.println("\n推荐结果:");
List<Map.Entry<Movie, Double>> recommendations =
recommender.recommend(user1, 5, true);
printRecommendations(recommendations);
// 4. 演示2:相似电影推荐
System.out.println("\n=== 演示2:相似电影推荐 ===");
System.out.println("与《Inception》最相似的电影:");
List<Map.Entry<Movie, Double>> similarMovies =
recommender.findSimilarMovies("M001", 5);
printRecommendations(similarMovies);
// 5. 演示3:混合推荐
System.out.println("\n=== 演示3:混合推荐(结合用户偏好和相似度) ===");
User user2 = new User("User002");
user2.rateMovie("M002", 4.0);
user2.rateMovie("M004", 3.5);
user2.rateMovie("M006", 4.5);
user2.watchMovie("M007"); // 观看但未评分
List<Map.Entry<Movie, Double>> hybridRecommendations =
recommender.hybridRecommend(user2, 3);
printRecommendations(hybridRecommendations);
// 6. 展示所有电影
System.out.println("\n=== 电影目录 ===");
for (Movie movie : recommender.getAllMovies()) {
System.out.println(movie);
}
}
private static List<Movie> createMovieCatalog() {
List<Movie> movies = new ArrayList<>();
// 科幻/动作电影
movies.add(new Movie("M001", "Inception",
Arrays.asList("Sci-Fi", "Action", "Thriller"),
Arrays.asList("Christopher Nolan"),
Arrays.asList("Leonardo DiCaprio", "Joseph Gordon-Levitt", "Elliot Page"),
8.8, 2010));
movies.add(new Movie("M002", "The Matrix",
Arrays.asList("Sci-Fi", "Action"),
Arrays.asList("The Wachowskis"),
Arrays.asList("Keanu Reeves", "Laurence Fishburne", "Carrie-Anne Moss"),
8.7, 1999));
movies.add(new Movie("M003", "Interstellar",
Arrays.asList("Sci-Fi", "Drama", "Adventure"),
Arrays.asList("Christopher Nolan"),
Arrays.asList("Matthew McConaughey", "Anne Hathaway", "Jessica Chastain"),
8.6, 2014));
movies.add(new Movie("M004", "Blade Runner 2049",
Arrays.asList("Sci-Fi", "Mystery", "Thriller"),
Arrays.asList("Denis Villeneuve"),
Arrays.asList("Ryan Gosling", "Harrison Ford", "Ana de Armas"),
8.0, 2017));
// 奇幻/冒险电影
movies.add(new Movie("M005", "The Lord of the Rings: The Fellowship of the Ring",
Arrays.asList("Fantasy", "Adventure", "Action"),
Arrays.asList("Peter Jackson"),
Arrays.asList("Elijah Wood", "Ian McKellen", "Viggo Mortensen"),
8.8, 2001));
movies.add(new Movie("M006", "Harry Potter and the Sorcerer's Stone",
Arrays.asList("Fantasy", "Adventure", "Family"),
Arrays.asList("Chris Columbus"),
Arrays.asList("Daniel Radcliffe", "Rupert Grint", "Emma Watson"),
7.6, 2001));
// 犯罪/剧情电影
movies.add(new Movie("M007", "The Dark Knight",
Arrays.asList("Action", "Crime", "Drama"),
Arrays.asList("Christopher Nolan"),
Arrays.asList("Christian Bale", "Heath Ledger", "Aaron Eckhart"),
9.0, 2008));
movies.add(new Movie("M008", "Pulp Fiction",
Arrays.asList("Crime", "Drama"),
Arrays.asList("Quentin Tarantino"),
Arrays.asList("John Travolta", "Samuel L. Jackson", "Uma Thurman"),
8.9, 1994));
movies.add(new Movie("M009", "The Shawshank Redemption",
Arrays.asList("Drama"),
Arrays.asList("Frank Darabont"),
Arrays.asList("Tim Robbins", "Morgan Freeman", "Bob Gunton"),
9.3, 1994));
movies.add(new Movie("M010", "Forrest Gump",
Arrays.asList("Drama", "Romance"),
Arrays.asList("Robert Zemeckis"),
Arrays.asList("Tom Hanks", "Robin Wright", "Gary Sinise"),
8.8, 1994));
return movies;
}
private static void printUserPreference(User user, ContentBasedRecommender recommender) {
System.out.println("用户ID: " + user.getUserId());
System.out.println("已评分电影: " + user.getRatedMovies());
System.out.println("已观看电影: " + user.getWatchedMovies());
Map<String, Double> preference = user.getUserPreferenceVector(
createMovieCatalog().stream()
.collect(HashMap::new, (m, mv) -> m.put(mv.getId(), mv), HashMap::putAll));
// 只显示主要偏好
System.out.println("主要偏好特征:");
preference.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(5)
.forEach(entry -> System.out.println(" " + entry.getKey() + ": " + entry.getValue()));
}
private static void printRecommendations(List<Map.Entry<Movie, Double>> recommendations) {
int rank = 1;
for (Map.Entry<Movie, Double> entry : recommendations) {
Movie movie = entry.getKey();
double score = entry.getValue();
System.out.printf("%d. %s (Score: %.2f)%n", rank++, movie.getTitle(), score);
}
if (recommendations.isEmpty()) {
System.out.println("无推荐结果");
}
}
}
运行结果示例
=== 演示1:基于用户评分的推荐 ===
用户偏好特征:
用户ID: User001
已评分电影: {M001=5.0, M003=4.5, M005=4.0}
已观看电影: [M001, M003, M005]
主要偏好特征:
genre_sci-fi: 12.0
director_christopher nolan: 9.0
actor_leonardo dicaprio: 7.5
genre_action: 6.0
genre_fantasy: 4.0
推荐结果:
1. M007 The Dark Knight (Score: 2.85)
2. M002 The Matrix (Score: 2.73)
3. M004 Blade Runner 2049 (Score: 2.61)
4. M008 Pulp Fiction (Score: 2.35)
5. M009 The Shawshank Redemption (Score: 2.28)
=== 演示2:相似电影推荐 ===
与《Inception》最相似的电影:
1. M007 The Dark Knight (Score: 0.85)
2. M003 Interstellar (Score: 0.82)
3. M002 The Matrix (Score: 0.76)
4. M004 Blade Runner 2049 (Score: 0.69)
5. M005 Lord of the Rings (Score: 0.45)
=== 演示3:混合推荐(结合用户偏好和相似度) ===
1. M007 The Dark Knight (Score: 3.15)
2. M003 Interstellar (Score: 2.98)
3. M001 Inception (Score: 2.87)
=== 电影目录 ===
Movie{id='M001', title='Inception', genres=[Sci-Fi, Action, Thriller], rating=8.8, year=2010}
...
核心概念说明
| 概念 | 说明 |
|---|---|
| 特征向量 | 将电影的类型、导演、演员等转换为数值向量 |
| 余弦相似度 | 计算两个向量之间的相似程度(范围0-1) |
| 用户偏好 | 基于用户评分的加权特征组合 |
| 混合推荐 | 结合基于内容和协同过滤的推荐策略 |
扩展建议
- 添加TF-IDF:对整个电影描述文本进行TF-IDF向量化
- 深度学习:使用Word2Vec或BERT生成电影特征
- 实时更新:根据用户实时行为动态更新推荐
- 缓存机制:对高频推荐结果进行缓存
- A/B测试:测试不同推荐算法效果
这个案例完整展示了基于内容推荐的核心实现,可以直接运行并扩展到实际项目中。