Java推薦系統-基於使用者的最近鄰協同過濾演算法
基於使用者的最近鄰演算法(User-Based Neighbor Algorithms),是一種非概率性的協同過濾演算法,也是推薦系統中最最古老,最著名的演算法,我們稱那些興趣相似的使用者為鄰居,如果使用者n相似於使用者u,我們就說n是u的一個鄰居。起初演算法,對於未知目標的預測是根據該使用者的相似使用者的評分作出預測的
本文中以電影推薦為例:
假設有7個人像向你分別推薦了電影,你是其中的小明,你自己也看過一些電影,你自己看過的不想看,只想看沒有看過的,且推薦度高,評分高的電影,示例資料如下:
{ "小明":{ "中國合夥人":5, "太平輪":3, "荒野獵人":4.5, "老炮兒":5, "我的少女時代":3, "肖洛特煩惱":4.5, "火星救援":5 }, "小紅":{ "小時代4":4, "荒野獵人":3, "我的少女時代":5, "肖洛特煩惱":5, "火星救援":3, "後會無期":3 }, "小陽":{ "小時代4":2, "中國合夥人":5, "我的少女時代":3, "老炮兒":5, "肖洛特煩惱":4.5, "速度與激情7":5 }, "小四":{ "小時代4":5, "中國合夥人":3, "我的少女時代":4, "匆匆那年":4, "速度與激情7":3.5, "火星救援":3.5, "後會無期":4.5 }, "六爺":{ "小時代4":2, "中國合夥人":4, "荒野獵人":4.5, "老炮兒":5, "我的少女時代":2 }, "小李":{ "荒野獵人":5, "盜夢空間":5, "我的少女時代":3, "速度與激情7":5, "蟻人":4.5, "老炮兒":4, "後會無期":3.5 }, "隔壁老王":{ "荒野獵人":5, "中國合夥人":4, "我的少女時代":1, "Phoenix":5, "甄嬛傳":4, "The Strokes":5 }, "鄰村小芳":{ "小時代4":4, "我的少女時代":4.5, "匆匆那年":4.5, "甄嬛傳":2.5, "The Strokes":3 } }
這裡我們需要用到一個公式:皮爾遜公式
假設有兩個變數X、Y,那麼兩變數間的皮爾遜相關係數可通過以下公式計算:
公式一:
皮爾遜相關係數計算公式
公式二:
皮爾遜相關係數計算公式
公式三:
皮爾遜相關係數計算公式
公式四:
皮爾遜相關係數計算公式
以上列出的四個公式等價,其中E是數學期望,cov表示協方差,N表示變數取值的個數
皮爾遜演算法過於複雜,如果要有點理解的話,可以使用把維數降到二維,這樣就跟餘弦定理有點相識了,相識度就是兩條直線的夾角,角度為0的時候,皮爾遜的值為1,就是最相識的,如果角度為180度,代表兩個牛馬不相干,皮爾遜的值為-1。
皮爾遜相關係數是比歐幾里德距離更加複雜的可以判斷人們興趣的相似度的一種方法。該相關係數是判斷兩組資料與某一直線擬合程式的一種試題。它在資料不是很規範的時候,會傾向於給出更好的結果。
如圖,Mick Lasalle為<<Superman>>評了3分,而Gene Seyour則評了5分,所以該影片被定位中圖中的(3,5)處。在圖中還可以看到一條直線。其繪製原則是儘可能地靠近圖上的所有座標點,被稱為最佳擬合線。如果兩位評論者對所有影片的評分情況都相同,那麼這條直線將成為對角線,並且會與圖上所有的座標點都相交,從而得到一個結果為1的理想相關度評價。
PYTHON實現:
此處我以python程式碼為例向大家展示,文末附上java程式碼的實現
# 構造一份打分資料集,可以去movielens下載真實的資料做實驗
users = {"小明": {"中國合夥人": 5.0, "太平輪": 3.0, "荒野獵人": 4.5, "老炮兒": 5.0, "我的少女時代": 3.0, "肖洛特煩惱": 4.5, "火星救援": 5.0},
"小紅": {"小時代4": 4.0, "荒野獵人": 3.0, "我的少女時代": 5.0, "肖洛特煩惱": 5.0, "火星救援": 3.0, "後會無期": 3.0},
"小陽": {"小時代4": 2.0, "中國合夥人": 5.0, "我的少女時代": 3.0, "老炮兒": 5.0, "肖洛特煩惱": 4.5, "速度與激情7": 5.0},
"小四": {"小時代4": 5.0, "中國合夥人": 3.0, "我的少女時代": 4.0, "匆匆那年": 4.0, "速度與激情7": 3.5, "火星救援": 3.5, "後會無期": 4.5},
"六爺": {"小時代4": 2.0, "中國合夥人": 4.0, "荒野獵人": 4.5, "老炮兒": 5.0, "我的少女時代": 2.0},
"小李": {"荒野獵人": 5.0, "盜夢空間": 5.0, "我的少女時代": 3.0, "速度與激情7": 5.0, "蟻人": 4.5, "老炮兒": 4.0, "後會無期": 3.5},
"隔壁老王": {"荒野獵人": 5.0, "中國合夥人": 4.0, "我的少女時代": 1.0, "Phoenix": 5.0, "甄嬛傳": 4.0, "The Strokes": 5.0},
"鄰村小芳": {"小時代4": 4.0, "我的少女時代": 4.5, "匆匆那年": 4.5, "甄嬛傳": 2.5, "The Strokes": 3.0}
}
# 定義幾種距離計算函式
# 更高效的方式為把得分向量化之後使用scipy中定義的distance方法
from math import sqrt
def pearson_dis(rating1, rating2):
"""計算2個打分序列間的pearson距離. 輸入的rating1和rating2都是打分dict
格式為{'小時代4': 1.0, '瘋狂動物城': 5.0}"""
sum_xy = 0
sum_x = 0
sum_y = 0
sum_x2 = 0
sum_y2 = 0
n = 0
for key in rating1:
if key in rating2:
n += 1
x = rating1[key]
y = rating2[key]
sum_xy += x * y
sum_x += x
sum_y += y
sum_x2 += pow(x, 2)
sum_y2 += pow(y, 2)
# now compute denominator
denominator = sqrt(sum_x2 - pow(sum_x, 2) / n) * sqrt(sum_y2 - pow(sum_y, 2) / n)
if denominator == 0:
return 0
else:
return (sum_xy - (sum_x * sum_y) / n) / denominator
# 查詢最近鄰
def computeNearestNeighbor(username, users):
"""在給定username的情況下,計算其他使用者和它的距離並排序"""
print("查詢最近鄰")
distances = []
for user in users:
if user != username:
# distance = manhattan_dis(users[user], users[username])
distance = pearson_dis(users[user], users[username])
distances.append((distance, user))
# 根據距離排序,距離越近,排得越靠前
distances.sort()
print("distances => ", distances)
return distances
# 推薦
def recommend(username, users):
print("輸入=》", username)
"""對指定的user推薦電影"""
# 找到最近鄰
nearest = computeNearestNeighbor(username, users)[0][1]
recommendations = []
# 找到最近鄰看過,但是我們沒看過的電影,計算推薦
neighborRatings = users[nearest]
print("nearest -> ", nearest)
print("neighborRatings -> ", neighborRatings)
userRatings = users[username]
print("userRatings -> ", userRatings)
for artist in neighborRatings:
if not artist in userRatings:
recommendations.append((artist, neighborRatings[artist]))
print("recommendations -> ", recommendations)
results = sorted(recommendations, key=lambda artistTuple: artistTuple[1], reverse=True)
for result in results:
print(result[0], result[1])
if __name__ == "__main__":
recommend('小明', users)
執行效果如下:
JAVA實現:
1.資料集合類
import java.util.ArrayList;
import java.util.List;
/**
* Created by ccwant on 2018-12-14.
*/
public class UserSet {
public List<User> users = new ArrayList<>();
public UserSet() {
}
public User put(String username) {
return new User(username);
}
public User getUser(int position) {
return users.get(position);
}
public User getUser(String username) {
for (User user : users) {
if (user.username.equals(username)) {
return user;
}
}
return null;
}
public final class User {
public String username;
public List<Set> list = new ArrayList<>();
private User(String username) {
this.username = username;
}
public User set(String username, int score) {
this.list.add(new Set(username, score));
return this;
}
public void create() {
users.add(this);
}
public Set find(String username) {
for (Set set : list) {
if (set.username.equals(username)) {
return set;
}
}
return null;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
'}';
}
}
public final class Set implements Comparable<Set> {
public String username;
public int score;
public Set(String username, int score) {
this.username = username;
this.score = score;
}
@Override
public String toString() {
return "Set{" +
"username='" + username + '\'' +
", score=" + score +
'}';
}
@Override
public int compareTo(Set o) {
return score > o.score ? -1 : 1;
}
}
}
2. 核心演算法
import java.util.*;
/**
* Created by ccwant on 2018-12-14.
*/
public class Recommend {
/**
* 在給定username的情況下,計算其他使用者和它的距離並排序
* @param username
* @param set
* @return
*/
private Map<Double, String> computeNearestNeighbor(String username, UserSet set) {
Map<Double, String> distances = new TreeMap<>();
UserSet.User u1 = set.getUser(username);
for (int i = 0; i < set.users.size(); i++) {
UserSet.User u2 = set.getUser(i);
if (!u2.username.equals(username)) {
double distance = pearson_dis(u2.list, u1.list);
distances.put(distance, u2.username);
}
}
System.out.println("distance => " + distances);
return distances;
}
/**
* 計算2個打分序列間的pearson距離
*
* @param rating1
* @param rating2
* @return
*/
private double pearson_dis(List<UserSet.Set> rating1, List<UserSet.Set> rating2) {
int sum_xy = 0;
int sum_x = 0;
int sum_y = 0;
double sum_x2 = 0;
double sum_y2 = 0;
int n = 0;
for (int i = 0; i < rating1.size(); i++) {
UserSet.Set key1 = rating1.get(i);
for (int j = 0; j < rating2.size(); j++) {
UserSet.Set key2 = rating2.get(j);
if (key1.username.equals(key2.username)) {
n += 1;
int x = key1.score;
int y = key2.score;
sum_xy += x * y;
sum_x += x;
sum_y += y;
sum_x2 += Math.pow(x, 2);
sum_y2 += Math.pow(y, 2);
}
}
}
double denominator = Math.sqrt(sum_x2 - Math.pow(sum_x, 2) / n) * Math.sqrt(sum_y2 - Math.pow(sum_y, 2) / n);
if (denominator == 0) {
return 0;
} else {
return (sum_xy - (sum_x * sum_y) / n) / denominator;
}
}
public List<UserSet.Set> recommend(String username, UserSet set) {
//找到最近鄰
Map<Double, String> distances = computeNearestNeighbor(username, set);
String nearest = distances.values().iterator().next();
System.out.println("nearest -> " + nearest);
List<UserSet.Set> recommendations = new ArrayList<>();
//找到最近鄰看過,但是我們沒看過的電影,計算推薦
UserSet.User neighborRatings = set.getUser(nearest);
System.out.println("neighborRatings -> " + neighborRatings.list);
UserSet.User userRatings = set.getUser(username);
System.out.println("userRatings -> " + userRatings.list);
for (UserSet.Set artist : neighborRatings.list) {
if (userRatings.find(artist.username) == null) {
recommendations.add(artist);
}
}
Collections.sort(recommendations);
System.out.println("recommendations -> " + recommendations.toString());
return recommendations;
}
}
3.測試類
import sys.Recommend;
import sys.UserSet;
import java.util.*;
/**
* Created by ccwant on 2018-12-14.
*/
public class Demo {
public static void main(String[] args) {
//輸入使用者總量
UserSet userSet = new UserSet();
userSet.put("小明")
.set("中國合夥人", 50)
.set("太平輪", 30)
.set("荒野獵人", 45)
.set("老炮兒", 50)
.set("我的少女時代", 30)
.set("肖洛特煩惱", 45)
.set("火星救援", 50)
.create();
userSet.put("小紅")
.set("小時代4", 40)
.set("荒野獵人", 30)
.set("我的少女時代", 50)
.set("肖洛特煩惱", 50)
.set("火星救援", 30)
.set("後會無期", 30)
.create();
userSet.put("小陽")
.set("小時代4", 20)
.set("中國合夥人", 50)
.set("我的少女時代", 30)
.set("老炮兒", 50)
.set("肖洛特煩惱", 45)
.set("速度與激情7", 50)
.create();
userSet.put("小四")
.set("小時代4", 50)
.set("中國合夥人", 30)
.set("我的少女時代", 40)
.set("匆匆那年", 40)
.set("速度與激情7", 35)
.set("火星救援", 35)
.set("後會無期", 45)
.create();
userSet.put("六爺")
.set("小時代4", 20)
.set("中國合夥人", 40)
.set("荒野獵人", 45)
.set("老炮兒", 50)
.set("我的少女時代", 20)
.create();
userSet.put("小李")
.set("荒野獵人", 50)
.set("盜夢空間", 50)
.set("我的少女時代", 30)
.set("速度與激情7", 50)
.set("蟻人", 45)
.set("老炮兒", 40)
.set("後會無期", 35)
.create();
userSet.put("隔壁老王")
.set("荒野獵人", 50)
.set("中國合夥人", 40)
.set("我的少女時代", 10)
.set("Phoenix", 50)
.set("甄嬛傳", 40)
.set("The Strokes", 50)
.create();
userSet.put("鄰村小芳")
.set("小時代4", 40)
.set("我的少女時代", 45)
.set("匆匆那年", 45)
.set("甄嬛傳", 25)
.set("The Strokes", 30)
.create();
Recommend recommend = new Recommend();
List<UserSet.Set> recommendations = recommend.recommend("小明", userSet);
System.out.println("-----------------------");
for (UserSet.Set set : recommendations) {
System.out.println(set.username+" "+set.score);
}
}
}
執行結果: