MaDi's Blog

一個紀錄自己在轉職軟體工程師路上的學習小空間

0%

Python實作電影推薦系統

電影推薦系統實作

資料採用公開資料集 MovieLens 做為數據,這裡只用ratings.csv和movies.csv,並參考網路上的資源實作內容過濾協同過濾的程式。

I. 內容過濾

根據你過去喜歡的產品,去推薦給你相似的產品。

流程:

  1. 建立特徵矩陣,計算每個使用者過往對每一種 電影類型(genres) 的平均評分
  2. 計算 使用者電影 的餘弦相似度(距離)
  3. 推薦 某使用者可能喜愛的電影某電影可能喜愛的受眾

引入套件

1
2
3
import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import paired_distances,cosine_similarity

下載公開資料集,並依照檔案路徑讀取資料

1
2
3
4
5
movies = pd.read_csv('./ml-latest-small/movies.csv')
rate = pd.read_csv('./ml-latest-small/ratings.csv')

display(movies.head())
display(rate.head())

合併資料做成DataFrame

1
2
3
4
5
6
7
8
# movies留下movieId與genres(電影分類)
# rate留下userId與movieId
# 把movies與rate以movieId合併成一個df

movies.drop('title',axis=1,inplace=True)
rate.drop(['rating', 'timestamp'],axis=1,inplace=True)
df = pd.merge(rate, movies, on='movieId')
df.head()

建立特徵矩陣:

movie_arr = movieIdgenres做完One-Hot Encoding 的矩陣,背後意義是電影Id對應到何種電影類型

user_arr = UserIdgenres做完One-Hot Encoding後使用者對每個電影類型的平均評分 的矩陣,背後意義是UserId對應到某種電影類型的平均評分

舉例來說,假設user1有10筆觀看紀錄,其中Adventure占了六筆,則One-Hot Encoding的Adventure這個欄位就是0.6分。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 建立movie的特徵矩陣
oneHot = movies["genres"].str.get_dummies("|") # One-Hot Encoding
movie_arr = pd.concat([movies, oneHot], axis=1)
movie_arr.drop("genres",axis=1,inplace=True)
movie_arr.set_index("movieId",inplace=True)
display(movie_arr.head())

# 建立user的特徵矩陣
oneHot = df["genres"].str.get_dummies("|") # One-Hot Encoding
user_arr = pd.concat([df, oneHot], axis=1)
user_arr.drop(["movieId","genres"],axis=1,inplace=True)
user_arr = user_arr.groupby('userId').mean()
display(user_arr.head())

計算 使用者與電影的餘弦相似度(距離) 來建立使用者與電影的相似度矩陣

1
2
3
4
# user-movie相似度矩陣
similar_matrix = cosine_similarity(user_arr.values, movie_arr.values)
similar_matrix = pd.DataFrame(similar_matrix, index = user_arr.index, columns = movie_arr.index)
similar_matrix

定義兩個函式分別取得前幾個最相似的電影前幾個最相似的使用者

1
2
3
4
5
6
7
8
9
10
11
# 取得與特定user最相似的前num部movie
def get_the_most_similar_movies(searchUserId, num):
vec = similar_matrix.loc[searchUserId].values
sorted_index = np.argsort(-vec)[:num] #找距離最短
return list(similar_matrix.columns[sorted_index])

# 取得與特定movie最相似的前num部movie
def get_the_most_similar_users(searchMovieId, num):
movie_vec = similar_matrix[searchMovieId].values
sorted_index = np.argsort(-movie_vec)[:num] #找距離最短
return list(similar_matrix.index[sorted_index])

設定欲搜尋的電影id和使用者id

1
2
3
4
# sort最相似的資料
searchMovieId = 1
searchUserId = 2
num = 10

開始搜尋

1
2
3
4
similar_movies_index = get_the_most_similar_movies(searchUserId, num)
similar_user_index = get_the_most_similar_users(searchMovieId, num)
print(similar_movies_index)
print(similar_user_index)

[7007, 20, 145, 5027, 1432, 5628, 67923, 96861, 41997, 26344]
[401, 388, 252, 161, 20, 539, 306, 276, 234, 605]

列出推薦電影與使用者的名單

1
2
3
4
5
6
7
8
9
10
# 重新讀入movies.csv為了要有title
movies = pd.read_csv('./ml-latest-small/movies.csv')

# 列出推薦名單
df_recommend_movies = pd.DataFrame({f'推薦給[使用者{searchUserId}]的前{num}部電影':movies[movies.movieId.isin(similar_movies_index)].title[:num]}).reset_index()
df_recommend_movies.drop('index',axis=1,inplace=True)
df_recommend_users = pd.DataFrame({f'可能會喜歡[電影{searchMovieId}]的前{num}個使用者':rate[rate.userId.isin(similar_user_index)].userId.unique()[:num]}).reset_index()
df_recommend_users.drop('index',axis=1,inplace=True)

pd.concat([df_recommend_movies,df_recommend_users],axis=1)

II. 基於使用者為基礎的協同過濾法

先找出喜好行為與你類似的使用者,再將他們喜好的內容推薦給你,將有很大的機率會激起你的興趣

流程:

  1. 找到與使用者有共同喜好的其他使用者
  2. 將這些其他使用者看過的最高評分的電影推薦給使用者

引入套件

1
2
3
import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import paired_distances,cosine_similarity

下載公開資料集,並依照檔案路徑讀取資料

1
2
3
4
5
movies = pd.read_csv('./ml-latest-small/movies.csv')
rate = pd.read_csv('./ml-latest-small/ratings.csv')

display(movies.head())
display(rate.head())

合併資料做成DataFrame

1
2
3
4
movies.drop('genres',axis=1,inplace=True)
rate.drop('timestamp',axis=1,inplace=True)
df = pd.merge(rate,movies,on='movieId')
df


看一下資料的分布

1
2
groups = df.groupby('userId')
pd.DataFrame(groups.size(),columns=['count']).plot();

定義find_common_movies 函式,目的是找出使用者與其他使用者共同看過的電影id

1
2
3
4
def find_common_movies(user, other_users):
s1 = set(user.movieId.values)
s2 = set(other_users.movieId.values)
return s1.intersection(s2)

因為sklearn裡面的cosine_similarity是常數計算,為了計算向量之間的餘弦相似度,特別客製化一個函式

1
2
3
4
5
6
def vec2matrix_cosine_similarity(vec1,vec2):
vec1 = np.mat(vec1)
vec2 = np.mat(vec2)
cos = float(vec1*vec2.T)/(np.linalg.norm(vec1)*np.linalg.norm(vec2))
sim = 0.5 + 0.5 * cos
return sim

由使用者與其他使用者共同看過的電影id的rating去計算兩者的餘弦相似度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def cal_cosine_similarity_from_rating(user,other_users,common_moviesId):
user_rating = user[user.movieId.isin(common_moviesId)].sort_values(by="movieId")["rating"].values.reshape(1,len(common_moviesId))
other_user_rating = other_users[other_users.movieId.isin(common_moviesId)].sort_values(by="movieId")["rating"].values.reshape(1,len(common_moviesId))
sim = vec2matrix_cosine_similarity(user_rating,other_user_rating)
return sim

def cal_each_user_similarity(userId):
user_similarity = []
for other_userId in df.userId.unique():
if other_userId == userId:
continue
user = groups.get_group(userId)
other_users = groups.get_group(other_userId)
common_moviesId = find_common_movies(user,other_users)
# 避免都無關,common_moviesId = {}
if common_moviesId != set():
sim = cal_cosine_similarity_from_rating(user,other_users,common_moviesId)
user_similarity.append([other_userId,sim])
return user_similarity

找出前num數量個相似的使用者Id

1
2
3
4
5
def top_num_similar_users(user_Id, num):
user_similarity = cal_each_user_similarity(user_Id)
user_similarity = sorted(user_similarity, key=lambda x: x[1], reverse=True)
similar_users = [x[0] for x in user_similarity][0:num]
return similar_users

由這些相似的使用者再找出他們評分最高的電影作為推薦

1
2
3
4
5
6
7
8
9
10
11
12
def recommend(user_Id, num=10):
# 找尋最相近的前num個使用者
similar_users = top_num_similar_users(user_Id, num)
# 欲搜尋的user_Id看過的電影
seen_movies = df.loc[df.userId==user_Id,"movieId"].values
# 由其他相似的使用者看過的電影來找出欲搜尋的user_Id沒看過的電影
other_similarUsers_seen_movies = df.loc[df.userId.isin(similar_users),"movieId"].values
not_seen_movies = set(other_similarUsers_seen_movies)-set(seen_movies)
# 計算這些沒看過的電影的平均評分
movie_groups = df.loc[df.movieId.isin(not_seen_movies)].groupby('movieId')
top_num_movies = movie_groups.mean().sort_values(by='rating', ascending=False)[:num].index
return df.loc[df.movieId.isin(top_num_movies), "title"].unique()

設定欲搜尋的使用者Id與推薦電影的數量num,並透過基於使用者的協同過濾法去推薦電影

1
2
3
4
5
6
7
8
9
10
11
# sort最相似的資料
searchUserId = 3
num = 10

# 透過協同過濾法推薦給searchUserId前num個電影
recommend_top_num_movies = recommend(searchUserId, num)

# 列出推薦名單
df_recommend_movies = pd.DataFrame({f'推薦給[使用者{searchUserId}]的前{num}部電影':recommend_top_num_movies}).reset_index()
df_recommend_movies.drop('index',axis=1,inplace=True)
df_recommend_movies

程式碼

based on content
based on user CF

參考