Springboot實(shí)現(xiàn)推薦系統(tǒng)的協(xié)同過濾算法
前言
協(xié)同過濾算法(Collaborative Filtering)是一種在推薦系統(tǒng)中廣泛使用的算法,用于預(yù)測(cè)用戶對(duì)物品(如商品、電影、音樂等)的偏好,從而實(shí)現(xiàn)個(gè)性化推薦。下面是詳細(xì)介紹:
基本原理
基于用戶行為數(shù)據(jù):協(xié)同過濾算法的核心是利用用戶對(duì)物品的行為數(shù)據(jù),這些行為數(shù)據(jù)可以包括用戶的評(píng)分(如電影評(píng)分從 1 - 5 星)、購買記錄、瀏覽歷史、點(diǎn)贊 / 收藏等。通過分析這些數(shù)據(jù)來發(fā)現(xiàn)用戶之間的相似性或者物品之間的相似性。
相似性度量:
用戶相似性:如果兩個(gè)用戶對(duì)很多相同物品的評(píng)價(jià)(或行為)相似,那么這兩個(gè)用戶就具有較高的相似性。例如,用戶 A 和用戶 B 都對(duì)電影 1 打了 4 星,對(duì)電影 2 打了 3 星,對(duì)電影 3 打了 5 星,通過計(jì)算這種相似性,我們可以認(rèn)為他們的觀影偏好相似。
物品相似性:如果很多用戶對(duì)兩個(gè)物品的評(píng)價(jià)(或行為)相似,那么這兩個(gè)物品就具有較高的相似性。比如,很多用戶既喜歡電影《泰坦尼克號(hào)》又喜歡電影《羅馬假日》,這兩部電影在用戶偏好上就具有一定的相似性。
算法分類
基于用戶的協(xié)同過濾(User - based Collaborative Filtering)
原理:先找到與目標(biāo)用戶興趣相似的其他用戶(稱為 “鄰居用戶”),然后根據(jù)這些鄰居用戶對(duì)物品的偏好來預(yù)測(cè)目標(biāo)用戶對(duì)未見過(或未評(píng)價(jià))物品的偏好。例如,目標(biāo)用戶 A 沒有看過電影 M,但是與 A 相似的用戶 B、C 都對(duì)電影 M 評(píng)價(jià)很高,那么就可以推測(cè)用戶 A 也可能喜歡電影 M。

案例:有A、B、C三個(gè)用戶,已知A四種水果都喜歡,C喜歡的兩種水果是A喜歡的,那么可以認(rèn)為A和C是相似的,則可以把A喜歡的但不知道C是否喜歡的那兩種水果推薦給C.

基于物品的協(xié)同過濾(Item - based Collaborative Filtering)
原理:先計(jì)算物品之間的相似度,然后根據(jù)用戶已經(jīng)評(píng)價(jià)(或行為)過的物品與其他物品的相似度,來預(yù)測(cè)用戶對(duì)其他未評(píng)價(jià)(或未行為)物品的偏好。例如,用戶 A 喜歡電影《教父》,而電影《教父》和電影《美國往事》相似度很高,那么可以推測(cè)用戶 A 也可能喜歡電影《美國往事》。
計(jì)算方法
在協(xié)同過濾算法中,用于計(jì)算用戶或物品之間相似度的方法有很多種,下面我們將介紹兩種:
1、杰卡德相似系數(shù)(Jaccard Similarity Coefficient)
杰卡德相似系數(shù)主要用于衡量有限集合之間的相似性。在協(xié)同過濾的場(chǎng)景下,通常是將用戶對(duì)物品的行為(如是否購買、是否喜歡等)看作集合元素。對(duì)于兩個(gè)用戶 A 和 B,

也就是說,杰卡德相似系數(shù)是兩個(gè)用戶共同感興趣的物品數(shù)量與他們感興趣的所有物品數(shù)量之比
2、余弦相似度
首先,在協(xié)同過濾中會(huì)構(gòu)建一個(gè)用戶 - 物品評(píng)分矩陣。假設(shè)我們有個(gè)用戶和個(gè)物品,矩陣中的元素表示用戶對(duì)物品的評(píng)分。例如,在電影推薦系統(tǒng)中,行代表用戶,列代表電影,矩陣中的每個(gè)元素就是用戶對(duì)電影的評(píng)分(如果用戶沒有評(píng)分可以用 0 或其他默認(rèn)值表示)。對(duì)于兩個(gè)用戶 A 和 B

優(yōu)點(diǎn)
- 個(gè)性化推薦:能夠根據(jù)用戶的歷史行為為每個(gè)用戶提供個(gè)性化的推薦,推薦的物品與用戶的興趣緊密相關(guān),提高了用戶發(fā)現(xiàn)感興趣物品的概率。
- 不需要物品內(nèi)容信息:與基于內(nèi)容的推薦算法不同,協(xié)同過濾算法不需要對(duì)物品的內(nèi)容(如電影的劇情、商品的描述等)進(jìn)行分析,只依賴用戶的行為數(shù)據(jù),因此可以適用于各種類型的物品推薦。可以發(fā)現(xiàn)新的興趣點(diǎn):能夠發(fā)現(xiàn)用戶潛在的興趣點(diǎn),例如用戶可能沒有意識(shí)到自己會(huì)喜歡某個(gè)物品,但通過協(xié)同過濾算法的推薦,用戶可能會(huì)發(fā)現(xiàn)并喜歡上這個(gè)新的物品。
缺點(diǎn)
- 冷啟動(dòng)問題:
- 用戶冷啟動(dòng):對(duì)于新用戶,由于沒有足夠的行為數(shù)據(jù),很難找到與其相似的用戶或者根據(jù)其行為來推薦物品。例如,一個(gè)新注冊(cè)的用戶還沒有對(duì)任何電影進(jìn)行評(píng)分,就很難為他推薦合適的電影。
- 物品冷啟動(dòng):新加入的物品由于沒有用戶行為數(shù)據(jù),也很難被推薦出去。比如,一個(gè)新上映的電影,如果還沒有用戶對(duì)其進(jìn)行評(píng)分或其他行為,就很難出現(xiàn)在推薦列表中。
- 數(shù)據(jù)稀疏問題:在實(shí)際應(yīng)用中,用戶對(duì)物品的行為數(shù)據(jù)往往是非常稀疏的。例如,一個(gè)電商平臺(tái)有大量的商品和用戶,但每個(gè)用戶可能只購買或?yàn)g覽了很少一部分商品,這就導(dǎo)致用戶 - 物品評(píng)分矩陣(或行為矩陣)中有大量的空白,影響了相似度計(jì)算的準(zhǔn)確性和推薦質(zhì)量。
應(yīng)用場(chǎng)景
- 電商平臺(tái)推薦:根據(jù)用戶的購買歷史、瀏覽記錄等推薦用戶可能感興趣的商品,如亞馬遜、淘寶等電商平臺(tái)都廣泛使用協(xié)同過濾算法來提高用戶的購買轉(zhuǎn)化率。
- 視頻和音樂平臺(tái)推薦:像 Netflix(視頻平臺(tái))、Spotify(音樂平臺(tái))等,根據(jù)用戶的觀看 / 收聽歷史、評(píng)分等行為推薦新的視頻或音樂。
- 社交平臺(tái)推薦:在社交網(wǎng)絡(luò)中推薦用戶可能感興趣的人、群組、內(nèi)容等,例如領(lǐng)英(LinkedIn)可能會(huì)根據(jù)用戶的職業(yè)興趣和關(guān)注的人來推薦新的人脈或行業(yè)內(nèi)容。
代碼實(shí)現(xiàn)
首先我們需要數(shù)據(jù)庫中需要有一張表,比如說評(píng)分表score,表中的字段如下,

然后我們就可以使用下面的代碼從這張表中獲取數(shù)據(jù),
?
package com.bishe.utils.recommend;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 該類提供基于用戶評(píng)分?jǐn)?shù)據(jù)生成推薦物品的相關(guān)功能。
*/
public class UserCF {
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database"; // 數(shù)據(jù)庫地址
private static final String USER = "your_username"; // 數(shù)據(jù)庫用戶名
private static final String PASSWORD = "your_password"; // 數(shù)據(jù)庫密碼
// 程序入口
public static void main(String[] args) {
// 從數(shù)據(jù)庫中獲取評(píng)分?jǐn)?shù)據(jù)
Map<Integer, Map<Integer, Integer>> ratings = fetchRatingsFromDatabase();
int userId = 1; // 要推薦的用戶ID
int numRecommendations = 2; // 推薦數(shù)量
List<Integer> recommendations = recommend(ratings, userId, numRecommendations);
System.out.println("為用戶 " + userId + " 推薦的物品: " + recommendations);
}
// 從數(shù)據(jù)庫中獲取評(píng)分?jǐn)?shù)據(jù)的方法
private static Map<Integer, Map<Integer, Integer>> fetchRatingsFromDatabase() {
Map<Integer, Map<Integer, Integer>> ratings = new HashMap<>();
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASSWORD);
Statement stmt = conn.createStatement()) {
String sql = "SELECT user_id, item_id, rating FROM ratings"; // 假設(shè)你的評(píng)分?jǐn)?shù)據(jù)在ratings表中
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
int userId = rs.getInt("user_id");
int itemId = rs.getInt("item_id");
int rating = rs.getInt("rating");
ratings.computeIfAbsent(userId, k -> new HashMap<>()).put(itemId, rating);
}
} catch (SQLException e) {
e.printStackTrace();
}
return ratings;
}
/**
* 推薦方法,根據(jù)用戶的評(píng)分?jǐn)?shù)據(jù)和用戶ID生成推薦物品。
* <p>
* 此方法基于用戶間的相似度以及評(píng)分?jǐn)?shù)據(jù),綜合計(jì)算出對(duì)指定用戶的推薦物品列表。
* </p>
*
* @param ratings 包含所有用戶及其對(duì)應(yīng)評(píng)分?jǐn)?shù)據(jù)的映射。外層鍵為用戶ID,內(nèi)層鍵為物品ID,值為對(duì)應(yīng)評(píng)分。
* @param userId 目標(biāo)用戶的ID,用于生成針對(duì)該用戶的推薦物品。
* @param numRecommendations 期望生成的推薦物品數(shù)量,決定了最終返回推薦列表的長(zhǎng)度。
* @return 返回一個(gè)包含推薦物品ID的列表,列表長(zhǎng)度由numRecommendations決定或者受限于可推薦物品數(shù)量(取較小值)。
*/
public static List<Integer> recommend(Map<Integer, Map<Integer, Integer>> ratings, int userId, int numRecommendations) {
// 創(chuàng)建一個(gè)Map,用于存儲(chǔ)每個(gè)物品的加權(quán)評(píng)分。鍵為物品ID,值為加權(quán)評(píng)分(初始化為0.0)。
// 加權(quán)評(píng)分會(huì)綜合考慮其他用戶與目標(biāo)用戶的相似度以及他們對(duì)物品的評(píng)分來計(jì)算。
Map<Integer, Double> weightedRatings = new HashMap<>();
// 創(chuàng)建一個(gè)Map,用于存儲(chǔ)每個(gè)其他用戶與目標(biāo)用戶的相似度得分。鍵為其他用戶的ID,值為相似度得分(初始化為0.0)。
Map<Integer, Double> similarityScores = new HashMap<>();
// 計(jì)算與其他用戶的相似度,并基于相似度和其他用戶的評(píng)分計(jì)算物品的加權(quán)評(píng)分
for (Map.Entry<Integer, Map<Integer, Integer>> entry : ratings.entrySet()) {
// 獲取當(dāng)前遍歷到的其他用戶的ID
int otherUserId = entry.getKey();
// 如果當(dāng)前用戶就是目標(biāo)用戶,則跳過本次循環(huán),不需要計(jì)算與自身的相似度
if (otherUserId == userId) continue;
// 調(diào)用calculateSimilarity方法計(jì)算當(dāng)前其他用戶與目標(biāo)用戶的相似度,并將結(jié)果存入similarityScores Map中
double similarity = calculateSimilarity(ratings.get(userId), entry.getValue());
similarityScores.put(otherUserId, similarity);
// 遍歷當(dāng)前其他用戶評(píng)分過的物品,計(jì)算這些物品的加權(quán)評(píng)分
for (Map.Entry<Integer, Integer> item : entry.getValue().entrySet()) {
// 獲取物品的ID
int itemId = item.getKey();
// 獲取當(dāng)前其他用戶對(duì)該物品的評(píng)分
double rating = item.getValue();
// 如果目標(biāo)用戶沒有對(duì)該物品進(jìn)行過評(píng)分(即ratings.get(userId).get(itemId)為null),
// 則將該物品的加權(quán)評(píng)分加上當(dāng)前其他用戶對(duì)該物品的評(píng)分乘以相似度得分
if (ratings.get(userId).get(itemId) == null) {
weightedRatings.put(itemId, weightedRatings.getOrDefault(itemId, 0.0) + rating * similarity);
}
}
}
// 將加權(quán)評(píng)分的Map轉(zhuǎn)換為L(zhǎng)ist<Map.Entry<Integer, Double>>,方便后續(xù)進(jìn)行排序操作
// 每個(gè)元素代表一個(gè)物品的ID及其對(duì)應(yīng)的加權(quán)評(píng)分,以鍵值對(duì)形式存儲(chǔ)在Entry中
List<Map.Entry<Integer, Double>> sortedRecommendations = new ArrayList<>(weightedRatings.entrySet());
// 使用lambda表達(dá)式自定義排序規(guī)則,按照加權(quán)評(píng)分從高到低對(duì)推薦物品進(jìn)行排序
// 這里通過比較Entry中的值(即加權(quán)評(píng)分)來確定順序,b.getValue().compareTo(a.getValue())表示按照降序排列
sortedRecommendations.sort((a, b) -> b.getValue().compareTo(a.getValue()));
// 創(chuàng)建一個(gè)列表,用于存儲(chǔ)最終要返回的推薦物品ID
List<Integer> recommendedItems = new ArrayList<>();
// 根據(jù)期望推薦的數(shù)量(取numRecommendations和實(shí)際可推薦物品數(shù)量的較小值),
// 將排序后的推薦物品ID依次添加到recommendedItems列表中
for (int i = 0; i < Math.min(numRecommendations, sortedRecommendations.size()); i++) {
recommendedItems.add(sortedRecommendations.get(i).getKey());
}
return recommendedItems;
}
/**
* 計(jì)算用戶和其他用戶之間相似度的方法。
* <p>
* 通過計(jì)算用戶評(píng)分向量的點(diǎn)積以及各自向量的模長(zhǎng),來得出兩個(gè)用戶之間的相似度得分。
* 使用余弦相似度的計(jì)算方式,衡量用戶間在評(píng)分行為上的相似程度。
* </p>
*
* @param userRatings 一個(gè)用戶的評(píng)分?jǐn)?shù)據(jù)映射,鍵為物品ID,值為對(duì)應(yīng)的評(píng)分。
* @param otherUserRatings 另一個(gè)用戶的評(píng)分?jǐn)?shù)據(jù)映射,與userRatings結(jié)構(gòu)相同,用于對(duì)比計(jì)算相似度。
* @return 返回一個(gè)雙精度浮點(diǎn)數(shù),表示兩個(gè)用戶之間的相似度得分,范圍在0到1之間(包含0和1),值越接近1表示相似度越高。
*/
private static double calculateSimilarity(Map<Integer, Integer> userRatings, Map<Integer, Integer> otherUserRatings) {
// 初始化用于存儲(chǔ)用戶評(píng)分向量點(diǎn)積的變量,初始值為0.0,后續(xù)會(huì)根據(jù)共同評(píng)分的物品來累加計(jì)算
double dotProduct = 0.0;
// 初始化用于存儲(chǔ)當(dāng)前用戶評(píng)分向量模長(zhǎng)平方的變量,初始值為0.0,后續(xù)會(huì)根據(jù)當(dāng)前用戶的評(píng)分來累加計(jì)算
double userMagnitude = 0.0;
// 初始化用于存儲(chǔ)其他用戶評(píng)分向量模長(zhǎng)平方的變量,初始值為0.0,后續(xù)會(huì)根據(jù)其他用戶的評(píng)分來累加計(jì)算
double otherUserMagnitude = 0.0;
// 遍歷當(dāng)前用戶的評(píng)分?jǐn)?shù)據(jù),計(jì)算與其他用戶評(píng)分?jǐn)?shù)據(jù)的點(diǎn)積以及各自向量的模長(zhǎng)平方
for (Map.Entry<Integer, Integer> entry : userRatings.entrySet()) {
// 獲取當(dāng)前評(píng)分?jǐn)?shù)據(jù)對(duì)應(yīng)的物品ID
int itemId = entry.getKey();
// 如果其他用戶也對(duì)該物品進(jìn)行了評(píng)分(即otherUserRatings中包含該物品ID),則進(jìn)行以下計(jì)算
if (otherUserRatings.containsKey(itemId)) {
// 計(jì)算點(diǎn)積,將當(dāng)前用戶對(duì)該物品的評(píng)分與其他用戶對(duì)該物品的評(píng)分相乘,并累加到dotProduct變量中
dotProduct += entry.getValue() * otherUserRatings.get(itemId);
// 計(jì)算當(dāng)前用戶評(píng)分向量模長(zhǎng)的平方,將當(dāng)前用戶對(duì)該物品的評(píng)分進(jìn)行平方,并累加到userMagnitude變量中
userMagnitude += Math.pow(entry.getValue(), 2);
// 計(jì)算其他用戶評(píng)分向量模長(zhǎng)的平方,將其他用戶對(duì)該物品的評(píng)分進(jìn)行平方,并累加到otherUserMagnitude變量中
otherUserMagnitude += Math.pow(otherUserRatings.get(itemId), 2);
}
}
// 如果當(dāng)前用戶評(píng)分向量模長(zhǎng)的平方為0或者其他用戶評(píng)分向量模長(zhǎng)的平方為0,
// 說明至少有一個(gè)用戶沒有對(duì)任何共同物品進(jìn)行評(píng)分,此時(shí)相似度為0,直接返回0(防止后續(xù)除法運(yùn)算出現(xiàn)除以零的錯(cuò)誤)
if (userMagnitude == 0 || otherUserMagnitude == 0) {
return 0;
}
// 根據(jù)余弦相似度的計(jì)算公式,返回兩個(gè)用戶之間的相似度得分。
// 即點(diǎn)積除以兩個(gè)用戶評(píng)分向量模長(zhǎng)的乘積(先分別對(duì)模長(zhǎng)平方開方得到模長(zhǎng),再相乘)
return dotProduct / (Math.sqrt(userMagnitude) * Math.sqrt(otherUserMagnitude));
}
}
?從數(shù)據(jù)庫獲取數(shù)據(jù)的方法可以根據(jù)自己的需求定義,這里只是方便演示就直接寫了,代碼采用的計(jì)算余弦的方法來計(jì)算用戶相似度。
如果想直接看效果的我們可以把上面代碼中的main方法替換為下面的代碼
/**
* main方法,用于測(cè)試推薦功能。
* @param args
*/
public static void main(String[] args) {
// 創(chuàng)建一個(gè)包含所有用戶評(píng)分?jǐn)?shù)據(jù)的映射
Map<Integer, Map<Integer, Integer>> ratings = new HashMap<>();
// 添加一些模擬數(shù)據(jù)
// 用戶1對(duì)物品1評(píng)分5分,對(duì)物品2評(píng)分3分
ratings.put(1, new HashMap<Integer, Integer>() {{
put(1, 5);
put(2, 3);
}});
// 用戶2對(duì)物品1評(píng)分4分,對(duì)物品3評(píng)分2分
ratings.put(2, new HashMap<Integer, Integer>() {{
put(1, 4);
put(3, 2);
}});
// 用戶3對(duì)物品2評(píng)分4分,對(duì)物品3評(píng)分5分
ratings.put(3, new HashMap<Integer, Integer>() {{
put(2, 4);
put(3, 5);
}});
// 用戶4對(duì)物品1評(píng)分3分,對(duì)物品4評(píng)分2分
ratings.put(4, new HashMap<Integer, Integer>() {{
put(1, 3);
put(4, 2);
}});
// 目標(biāo)用戶ID
int userId = 1;
// 期望生成的推薦物品數(shù)量
int numRecommendations = 2;
// 調(diào)用recommend方法生成推薦物品列表
List<Integer> recommendedItems = recommend(ratings, userId, numRecommendations);
// 打印推薦結(jié)果
System.out.println("推薦給用戶" + userId + "的物品ID列表: " + recommendedItems);
}運(yùn)行后我們可以看到控制臺(tái)打印出的信息

這樣就代表功能已經(jīng)實(shí)現(xiàn)了。
下面是基于余弦計(jì)算物品之間相似度的方法,實(shí)現(xiàn)和計(jì)算用戶的差不多,感興趣的可以看一下
package com.bishe.utils.recommend;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ItemCF {
/**
* 基于物品相似度的協(xié)同過濾推薦算法主方法。
* 該方法根據(jù)用戶對(duì)物品的評(píng)分?jǐn)?shù)據(jù),針對(duì)指定用戶生成推薦物品列表。
*
* @param ratings 用戶評(píng)分?jǐn)?shù)據(jù),外層鍵為用戶ID,內(nèi)層鍵為物品ID,值為對(duì)應(yīng)評(píng)分。
* @param userId 目標(biāo)用戶的ID,用于生成針對(duì)該用戶的推薦列表。
* @param numRecommendations 期望生成的推薦物品數(shù)量,決定了最終返回推薦列表的長(zhǎng)度。
* @return 返回一個(gè)包含推薦物品ID的列表,列表長(zhǎng)度由numRecommendations決定或者受限于可推薦物品數(shù)量(取較小值)。
*/
public static List<Integer> recommend(Map<Integer, Map<Integer, Integer>> ratings, int userId, int numRecommendations) {
// 調(diào)用calculateItemSimilarities方法計(jì)算物品之間的相似度矩陣,
// 這個(gè)矩陣將用于后續(xù)預(yù)測(cè)用戶對(duì)未評(píng)分物品的評(píng)分,以生成推薦列表
Map<Integer, Map<Integer, Double>> itemSimilarities = calculateItemSimilarities(ratings);
// 獲取目標(biāo)用戶(由userId指定)已評(píng)分的物品列表,
// 以便后續(xù)基于這些已評(píng)分物品來預(yù)測(cè)對(duì)其他未評(píng)分物品的評(píng)分
Map<Integer, Integer> userRatings = ratings.get(userId);
List<Integer> ratedItems = new ArrayList<>(userRatings.keySet());
// 用于存儲(chǔ)預(yù)測(cè)的用戶對(duì)未評(píng)分物品的評(píng)分,
// 鍵為物品ID,值為預(yù)測(cè)的評(píng)分,后續(xù)會(huì)根據(jù)物品相似度等信息來計(jì)算這些評(píng)分
Map<Integer, Double> predictedRatings = new HashMap<>();
// 遍歷所有用戶及其評(píng)分?jǐn)?shù)據(jù),目的是針對(duì)目標(biāo)用戶未評(píng)分的物品進(jìn)行評(píng)分預(yù)測(cè)
for (Map.Entry<Integer, Map<Integer, Integer>> entry : ratings.entrySet()) {
// 獲取當(dāng)前遍歷到的其他用戶的評(píng)分?jǐn)?shù)據(jù)
Map<Integer, Integer> otherUserRatings = entry.getValue();
// 獲取當(dāng)前遍歷到的其他用戶的ID
int otherUserId = entry.getKey();
// 如果當(dāng)前遍歷到的用戶就是目標(biāo)用戶,則跳過本次循環(huán),不需要對(duì)自身已有的評(píng)分?jǐn)?shù)據(jù)進(jìn)行處理
if (otherUserId == userId) continue;
// 遍歷當(dāng)前其他用戶評(píng)分過的物品,檢查是否是目標(biāo)用戶未評(píng)分的物品,若是則進(jìn)行評(píng)分預(yù)測(cè)
for (Integer itemId : otherUserRatings.keySet()) {
if (!ratedItems.contains(itemId)) {
// 調(diào)用predictRating方法預(yù)測(cè)目標(biāo)用戶對(duì)當(dāng)前未評(píng)分物品(itemId)的評(píng)分,
// 并將預(yù)測(cè)評(píng)分存入predictedRatings這個(gè)Map中
double predictedRating = predictRating(userRatings, itemSimilarities, itemId, otherUserRatings);
predictedRatings.put(itemId, predictedRating);
}
}
}
// 將預(yù)測(cè)評(píng)分的Map轉(zhuǎn)換為L(zhǎng)ist<Map.Entry<Integer, Double>>形式,方便后續(xù)進(jìn)行排序操作,
// 每個(gè)元素代表一個(gè)物品的ID及其對(duì)應(yīng)的預(yù)測(cè)評(píng)分,以鍵值對(duì)形式存儲(chǔ)在Entry中
List<Map.Entry<Integer, Double>> sortedPredictions = new ArrayList<>(predictedRatings.entrySet());
// 使用lambda表達(dá)式自定義排序規(guī)則,按照預(yù)測(cè)評(píng)分從高到低對(duì)物品進(jìn)行排序,
// 這里通過比較Entry中的值(即預(yù)測(cè)評(píng)分)來確定順序,b.getValue().compareTo(a.getValue())表示按照降序排列
sortedPredictions.sort((a, b) -> b.getValue().compareTo(a.getValue()));
// 創(chuàng)建一個(gè)列表,用于存儲(chǔ)最終要返回的推薦物品ID
List<Integer> recommendedItems = new ArrayList<>();
// 根據(jù)期望推薦的數(shù)量(取numRecommendations和實(shí)際可推薦物品數(shù)量的較小值),
// 將排序后的推薦物品ID依次添加到recommendedItems列表中
for (int i = 0; i < Math.min(numRecommendations, sortedPredictions.size()); i++) {
recommendedItems.add(sortedPredictions.get(i).getKey());
}
return recommendedItems;
}
/**
* 計(jì)算物品之間相似度的方法,采用余弦相似度計(jì)算。
* 該方法通過分析所有用戶對(duì)物品的評(píng)分?jǐn)?shù)據(jù),構(gòu)建出物品之間的相似度矩陣,
* 矩陣中記錄了每對(duì)物品之間的相似度得分,反映了物品在用戶評(píng)分行為上的相似程度。
*
* @param ratings 用戶評(píng)分?jǐn)?shù)據(jù),用于分析物品之間的相似程度。
* @return 返回一個(gè)表示物品相似度的矩陣,外層鍵為物品ID,內(nèi)層鍵為與之對(duì)比的物品ID,值為相似度得分。
*/
private static Map<Integer, Map<Integer, Double>> calculateItemSimilarities(Map<Integer, Map<Integer, Integer>> ratings) {
// 創(chuàng)建一個(gè)嵌套的Map結(jié)構(gòu),用于存儲(chǔ)物品之間的相似度矩陣,
// 外層鍵表示一個(gè)物品的ID,內(nèi)層鍵表示與之對(duì)比的另一個(gè)物品的ID,值為它們之間的相似度得分
Map<Integer, Map<Integer, Double>> itemSimilarities = new HashMap<>();
// 遍歷所有物品,外層循環(huán)每次確定一個(gè)物品(記為item1),用于與其他物品計(jì)算相似度
for (Map.Entry<Integer, Map<Integer, Integer>> item1Entry : ratings.entrySet()) {
// 獲取當(dāng)前遍歷到的物品(item1)的ID
int item1Id = item1Entry.getKey();
// 獲取當(dāng)前遍歷到的物品(item1)的用戶評(píng)分?jǐn)?shù)據(jù)
Map<Integer, Integer> item1Ratings = item1Entry.getValue();
// 創(chuàng)建一個(gè)Map,用于存儲(chǔ)當(dāng)前物品(item1)與其他物品的相似度得分,
// 鍵為其他物品的ID,值為對(duì)應(yīng)的相似度得分,后續(xù)會(huì)在循環(huán)中填充這個(gè)Map
Map<Integer, Double> similaritiesForItem1 = new HashMap<>();
// 內(nèi)層循環(huán)再次遍歷所有物品(記為item2),計(jì)算item1與每個(gè)item2之間的相似度
for (Map.Entry<Integer, Map<Integer, Integer>> item2Entry : ratings.entrySet()) {
// 獲取當(dāng)前內(nèi)層循環(huán)遍歷到的物品(item2)的ID
int item2Id = item2Entry.getKey();
// 獲取當(dāng)前內(nèi)層循環(huán)遍歷到的物品(item2)的用戶評(píng)分?jǐn)?shù)據(jù)
Map<Integer, Integer> item2Ratings = item2Entry.getValue();
// 如果兩個(gè)物品是同一個(gè)物品(即ID相同),則跳過本次相似度計(jì)算,因?yàn)樽陨砼c自身的相似度為1(無需計(jì)算)
if (item1Id == item2Id) continue;
// 調(diào)用calculateItemSimilarity方法計(jì)算當(dāng)前兩個(gè)物品(item1和item2)之間的相似度,
// 并將計(jì)算得到的相似度得分存入similaritiesForItem1這個(gè)Map中
double similarity = calculateItemSimilarity(item1Ratings, item2Ratings);
similaritiesForItem1.put(item2Id, similarity);
}
// 將當(dāng)前物品(item1)與其他物品的相似度得分Map存入itemSimilarities這個(gè)總的相似度矩陣中,
// 外層鍵為item1的ID,即完成了針對(duì)item1與其他所有物品相似度的計(jì)算和存儲(chǔ)
itemSimilarities.put(item1Id, similaritiesForItem1);
}
return itemSimilarities;
}
/**
* 計(jì)算兩個(gè)物品之間的相似度(采用余弦相似度)。
* 通過分析兩個(gè)物品各自對(duì)應(yīng)的用戶評(píng)分?jǐn)?shù)據(jù),按照余弦相似度的計(jì)算公式,
* 得出反映它們?cè)谟脩粼u(píng)分行為上相似程度的得分,范圍在0到1之間(包含0和1),越接近1表示相似度越高。
*
* @param item1Ratings 第一個(gè)物品的用戶評(píng)分?jǐn)?shù)據(jù),鍵為用戶ID,值為對(duì)應(yīng)評(píng)分。
* @param item2Ratings 第二個(gè)物品的用戶評(píng)分?jǐn)?shù)據(jù),結(jié)構(gòu)與item1Ratings相同,用于對(duì)比計(jì)算相似度。
* @return 返回一個(gè)雙精度浮點(diǎn)數(shù),表示兩個(gè)物品之間的相似度得分,范圍在0到1之間(包含0和1),值越接近1表示相似度越高。
*/
private static double calculateItemSimilarity(Map<Integer, Integer> item1Ratings, Map<Integer, Integer> item2Ratings) {
// 初始化用于存儲(chǔ)兩個(gè)物品評(píng)分向量點(diǎn)積的變量,初始值為0.0,后續(xù)會(huì)根據(jù)共同評(píng)分的用戶來累加計(jì)算
double dotProduct = 0.0;
// 初始化用于存儲(chǔ)第一個(gè)物品評(píng)分向量模長(zhǎng)平方的變量,初始值為0.0,后續(xù)會(huì)根據(jù)第一個(gè)物品的用戶評(píng)分來累加計(jì)算
double item1Magnitude = 0.0;
// 初始化用于存儲(chǔ)第二個(gè)物品評(píng)分向量模長(zhǎng)平方的變量,初始值為0.0,后續(xù)會(huì)根據(jù)第二個(gè)物品的用戶評(píng)分來累加計(jì)算
double item2Magnitude = 0.0;
// 遍歷第一個(gè)物品的用戶評(píng)分?jǐn)?shù)據(jù),目的是找出與第二個(gè)物品共同評(píng)分的用戶,并基于這些用戶的評(píng)分計(jì)算相關(guān)數(shù)值
for (Map.Entry<Integer, Integer> entry : item1Ratings.entrySet()) {
// 獲取當(dāng)前評(píng)分?jǐn)?shù)據(jù)對(duì)應(yīng)的用戶ID
int userId = entry.getKey();
// 如果第二個(gè)物品的用戶評(píng)分?jǐn)?shù)據(jù)中也包含當(dāng)前用戶(即共同評(píng)分的用戶),則進(jìn)行以下計(jì)算
if (item2Ratings.containsKey(userId)) {
// 計(jì)算點(diǎn)積,將第一個(gè)物品當(dāng)前用戶的評(píng)分與第二個(gè)物品該用戶的評(píng)分相乘,并累加到dotProduct變量中
dotProduct += entry.getValue() * item2Ratings.get(userId);
// 計(jì)算第一個(gè)物品評(píng)分向量模長(zhǎng)的平方,將第一個(gè)物品當(dāng)前用戶的評(píng)分進(jìn)行平方,并累加到item1Magnitude變量中
item1Magnitude += Math.pow(entry.getValue(), 2);
// 計(jì)算第二個(gè)物品評(píng)分向量模長(zhǎng)的平方,將第二個(gè)物品當(dāng)前用戶的評(píng)分進(jìn)行平方,并累加到item2Magnitude變量中
item2Magnitude += Math.pow(item2Ratings.get(userId), 2);
}
}
// 如果第一個(gè)物品評(píng)分向量模長(zhǎng)的平方為0或者第二個(gè)物品評(píng)分向量模長(zhǎng)的平方為0,
// 說明至少有一個(gè)物品沒有被任何用戶共同評(píng)分過,此時(shí)相似度為0,直接返回0(防止后續(xù)除法運(yùn)算出現(xiàn)除以零的錯(cuò)誤)
if (item1Magnitude == 0 || item2Magnitude == 0) {
return 0;
}
// 根據(jù)余弦相似度的計(jì)算公式,返回兩個(gè)物品之間的相似度得分。
// 即點(diǎn)積除以兩個(gè)物品評(píng)分向量模長(zhǎng)的乘積(先分別對(duì)模長(zhǎng)平方開方得到模長(zhǎng),再相乘)
return dotProduct / (Math.sqrt(item1Magnitude) * Math.sqrt(item2Magnitude));
}
/**
* 預(yù)測(cè)用戶對(duì)某個(gè)未評(píng)分物品的評(píng)分。
* 通過結(jié)合用戶已評(píng)分物品的評(píng)分、物品之間的相似度矩陣以及其他用戶對(duì)該未評(píng)分物品的評(píng)分等信息,
* 按照特定的計(jì)算公式來預(yù)測(cè)目標(biāo)用戶對(duì)該未評(píng)分物品的評(píng)分值。
*
* @param userRatings 用戶已有的評(píng)分?jǐn)?shù)據(jù),用于結(jié)合物品相似度來預(yù)測(cè)評(píng)分。
* @param itemSimilarities 物品相似度矩陣,提供物品間的相似程度信息。
* @param itemId 待預(yù)測(cè)評(píng)分的物品ID。
* @param otherUserRatings 其他用戶的評(píng)分?jǐn)?shù)據(jù),用于參考計(jì)算預(yù)測(cè)評(píng)分。
* @return 返回預(yù)測(cè)的用戶對(duì)該物品的評(píng)分值,為雙精度浮點(diǎn)數(shù)。
*/
private static double predictRating(Map<Integer, Integer> userRatings,
Map<Integer, Map<Integer, Double>> itemSimilarities,
int itemId,
Map<Integer, Integer> otherUserRatings) {
// 初始化分子變量,用于存儲(chǔ)預(yù)測(cè)評(píng)分計(jì)算公式中的分子部分的值,初始值為0.0,后續(xù)會(huì)根據(jù)已評(píng)分物品的相關(guān)信息累加計(jì)算
double numerator = 0.0;
// 初始化分母變量,用于存儲(chǔ)預(yù)測(cè)評(píng)分計(jì)算公式中的分母部分的值,初始值為0.0,后續(xù)會(huì)根據(jù)物品相似度等信息累加計(jì)算
double denominator = 0.0;
// 遍歷用戶已評(píng)分的物品,目的是結(jié)合這些已評(píng)分物品與待預(yù)測(cè)評(píng)分物品的相似度等信息來計(jì)算預(yù)測(cè)評(píng)分
for (Map.Entry<Integer, Integer> ratingEntry : userRatings.entrySet()) {
// 獲取當(dāng)前已評(píng)分物品的ID
int ratedItemId = ratingEntry.getKey();
// 獲取當(dāng)前已評(píng)分物品的評(píng)分
double rating = ratingEntry.getValue();
// 從物品相似度矩陣中獲取當(dāng)前已評(píng)分物品與待預(yù)測(cè)評(píng)分物品(itemId)之間的相似度
double similarity = itemSimilarities.get(ratedItemId).get(itemId);
// 將已評(píng)分物品的評(píng)分乘以相似度累加到分子變量中,按照預(yù)測(cè)評(píng)分的計(jì)算公式進(jìn)行分子部分的累加
numerator += rating * similarity;
// 將相似度的絕對(duì)值累加到分母變量中,按照預(yù)測(cè)評(píng)分的計(jì)算公式進(jìn)行分母部分的累加
denominator += Math.abs(similarity);
}
// 如果分母變量的值為0,說明沒有可參考的已評(píng)分物品與待預(yù)測(cè)物品有相似度關(guān)系,此時(shí)返回0作為預(yù)測(cè)評(píng)分
if (denominator == 0) {
return 0;
}
// 根據(jù)預(yù)測(cè)評(píng)分的計(jì)算公式(分子除以分母),返回預(yù)測(cè)的用戶對(duì)該物品(itemId)的評(píng)分值
return numerator / denominator;
}
}到此這篇關(guān)于Springboot實(shí)現(xiàn)推薦系統(tǒng)的協(xié)同過濾算法的文章就介紹到這了,更多相關(guān)Springboot協(xié)同過濾算法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Mybatis在IDEA中找不到mapper映射文件的問題
這篇文章主要介紹了解決Mybatis在IDEA中找不到mapper映射文件的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10
SpringBoot3中token攔截器鏈的設(shè)計(jì)與實(shí)現(xiàn)步驟
本文介紹了spring boot后端服務(wù)開發(fā)中有關(guān)如何設(shè)計(jì)攔截器的思路,文中通過代碼示例和圖文講解的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2024-03-03
java短網(wǎng)址服務(wù)(TinyURL)生成算法
這篇文章主要為大家詳細(xì)介紹了java短網(wǎng)址服務(wù)生成算法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
java實(shí)現(xiàn)微信支付結(jié)果通知
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)微信支付結(jié)果通知,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01

