電影推薦系統實作
資料採用公開資料集 MovieLens 做為數據,這裡只用ratings.csv和movies.csv,並參考網路上的資源實作內容過濾與協同過濾的程式。
I. 內容過濾
根據你過去喜歡的產品,去推薦給你相似的產品。
流程:
- 建立特徵矩陣,計算每個使用者過往對每一種 電影類型(genres) 的平均評分
- 計算 使用者 與 電影 的餘弦相似度(距離)
- 推薦 某使用者可能喜愛的電影 或 某電影可能喜愛的受眾
引入套件
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 = movieId
對 genres做完One-Hot Encoding
的矩陣,背後意義是電影Id對應到何種電影類型
user_arr = UserId
對 genres做完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 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
參考