在上一篇文章中,我們探討了使用圖技術構建推薦系統的理論基礎,本篇文章將以一個電影數據集作為 sample dataset 進行實作,並將實作內容分為兩個部分:
那我們開始吧~
電影與用戶行為的相似度矩陣可以視為一種特徵工程,類似於在機器學習模型中進行特徵建置的過程。通過構建這樣的矩陣,我們能有效量化電影間的關聯性與用戶偏好,為推薦系統提供核心數據基礎。
可以將流程分為四個步驟:
本實作選用 MovieLens 25M Dataset,這是一個電影推薦相關的資料集,包含用戶評分和用戶標籤數據,是研究推薦系統的經典資料集之一。
2. 時間範圍:覆蓋 1995 年 1 月 9 日至 2019 年 11 月 21 日,長達 24 年的數據記錄。
3. 數據來源:用戶是隨機選擇的,每位用戶至少評分過 20 部電影
資料集由多個 CSV 文件組成,分別記錄了電影、用戶行為及相關特徵,具體包括:
genome-scores.csv
和 genome-tags.csv
:包含標籤相關的數據,如標籤與電影的相關性分數movies.csv
:記錄電影的基本資訊(如電影標題和類型)ratings.csv
:包含電影的用戶評分數據tags.csv
:記錄用戶對電影應用的標籤數據我們先讀取 MovieLens 25M Dataset 中的主要數據文件,包括:
genome-scores.csv
:標籤相關性數據movies.csv
:電影基本資訊數據ratings.csv
:用戶評分數據tag_df = pd.read_csv('./data/genome-scores.csv')
movie_df = pd.read_csv('./data/movies.csv')
rating_df = pd.read_csv('./data/ratings.csv')
由於後續計算餘弦相似度(Cosine Similarity)時,會生成大規模的稀疏矩陣,佔用大量 RAM。本次實作為了控制內存消耗,限制電影的數量為 1000 部,僅保留這些電影的相關數據。
縮小範圍的具體步驟:
movieId
。movieId
,過濾相關的標籤數據、電影數據和評分數據。# limit to 1000 movies
movieId_list = movie_df['movieId'].sample(1000)
tag_df = tag_df[tag_df['movieId'].isin(movieId_list)]
movie_df = movie_df[movie_df['movieId'].isin(movieId_list)]
rating_df = rating_df[rating_df['movieId'].isin(movieId_list)]
print(tag_df.shape
,movie_df.shape
,rating_df.shape)
在進行後續電影相似度計算前,我們需要對 tag_df
(標籤相關性數據)進行處理,以轉換為適合計算的矩陣格式,並補全缺失的數據。以下是具體的步驟與說明:
1. 將數據轉換為透視表格式
首先,我們將 tag_df
中的數據透視為矩陣格式,使每部電影的 movieId
作為行索引,每個標籤的 tagId
作為列,標籤相關性分數(relevance
)作為矩陣值。
tag_df = tag_df.pivot(index=['movieId'], columns=['tagId'], values='relevance').reset_index()
2. 補全缺失的電影數據
因為我們限制了電影數量為 1000 部,某些 movieId
在 tag_df
中可能缺失(即:這些電影沒有標籤相關性數據)。為了確保矩陣的完整性,我們需要補全這些缺失的電影數據,並設置所有標籤相關性為 0。
'''
補那些沒有在裡面的 movieiId, tag 關係度都設置為 0'''
missing_movieId_list = list(set(movieId_list)-set(tag_df['movieId'].tolist()))
column_names = list(range(1, 1129))
new_rows = pd.DataFrame(
data=[[0] * len(column_names)] * len(missing_movieId_list),
index=missing_movieId_list,
columns=column_names )
tag_df = pd.concat([tag_df, new_rows])
在相似度計算中,電影的類型(genres
)是重要的特徵之一。因此,我們需要對 movie_df
中的類型數據進行處理,將其轉換為適合餘弦相似度計算的數值矩陣格式。
movie_df
中的 genres
欄位是以 |
分隔的字符串格式(如 "Action|Adventure|Comedy"
)。我們需要將每種電影類型分解為單獨的欄位,並用 One-Hot Encoding 表示是否包含該類型:
caculate_genres_df = movie_df['genres'].str.get_dummies(sep='|')
caculate_genres_df.insert(0, 'movieId', movie_df['movieId'])
caculate_genres_df.drop(columns=['(no genres listed)'])
caculate_genres_df.head()
我們對 rating_df
進行前處理,通過結合電影的年份、評分數量和平均評分來構建更具分析價值的數據結構
title_list = movie_df['title'].tolist()
for i in title_list:
if not '(' in i:
print(i)
def get_title_year(x):
if '(' in x and ')' in x:
if (x.split('(')[1])[0:4].isdigit():
return (x.split('(')[1])[0:4]
else:
return np.nan
else:
return np.nan
caculate_movie_year_df = movie_df.copy()
caculate_movie_year_df['title_year'] = movie_df['title'].apply(lambda x: get_title_year(x) )
2. 觀察年份分佈 for 年份分組
caculate_movie_year_df['title_year'] = pd.to_numeric(caculate_movie_year_df['title_year'], errors='coerce')
caculate_movie_year_df['title_year'].dropna().plot(
kind='hist',
bins=20,
edgecolor='black',
title='Distribution of Movie Release Years'
)
plt.xlabel('Year')
plt.ylabel('Count')
plt.show()
3. 年份分組
print(caculate_movie_year_df['title_year'].min())
print(caculate_movie_year_df['title_year'].max())
print(caculate_movie_year_df['title_year'].median())
num_bins = 5
bins = np.linspace(caculate_movie_year_df['title_year'].min(), caculate_movie_year_df['title_year'].max(), num_bins + 1)
caculate_movie_year_df['year_group'] = pd.cut(caculate_movie_year_df['title_year'], bins=bins, labels=range(1, num_bins + 1))
caculate_movie_year_df['year_group'] = caculate_movie_year_df['year_group'].cat.add_categories([0]).fillna(0)
caculate_movie_year_df.head(10)
4. 計算評分平均並分組
caculate_rating_mean_df =rating_df.groupby('movieId')['rating'].mean().reset_index(name='rating_mean')
caculate_rating_mean_df.describe()
def rating_mean_group(x):
if x<=2:
# 評分低
return 0
elif x<=3:
# 普通評分
return 1
elif x<=4:
# 高評分
return 2
else:
# 非常高
return 3
caculate_rating_mean_df['rating_mean_group'] = caculate_rating_mean_df['rating_mean'].apply(lambda x:rating_mean_group(x))
caculate_rating_mean_df.head(3)
5. 計算評分次數並分組
caculate_rating_count_df = rating_df.groupby('movieId')['timestamp'].count().reset_index(name='rating_count')
def rating_count_group(x):
if x<2:
# 極少評價的電影
return 0
elif x<=6:
# 少量評價
return 1
elif x<=33:
# 中等評價
return 2
else:
# 大量評價
return 3
caculate_rating_count_df['rating_count_group'] = caculate_rating_count_df['rating_count'].apply(lambda x:rating_count_group(x))
caculate_rating_count_df.head(3)
6. 合併最終數據集
caculate_rating_group_df = caculate_movie_year_df.merge(caculate_rating_count_df, on='movieId', how='left')
caculate_rating_group_df = caculate_rating_group_df.merge(caculate_rating_mean_df, on='movieId', how='left')
caculate_rating_group_df = caculate_rating_group_df[['movieId', 'year_group', 'rating_count_group', 'rating_mean_group']]
caculate_rating_group_df['rating_count_group'] = caculate_rating_group_df['rating_count_group'].fillna(-1)
caculate_rating_group_df['rating_mean_group'] = caculate_rating_group_df['rating_mean_group'].fillna(-1)
caculate_rating_group_df.head(3)
餘弦相似度(Cosine Similarity)是評估兩個非零向量之間相似程度的指標,通過計算它們之間的夾角餘弦值來衡量。
餘弦相似度的值範圍在 [−1,1][-1, 1][−1,1] 之間,其中:
⇒ 用餘弦相似度於衡量電影或用戶之間的相似性
計算已處理的三面向數據的 cos similarity
from sklearn.metrics.pairwise import cosine_similarity
tag_df = tag_df.set_index('movieId')
cos_tag = cosine_similarity(tag_df.values)
print(tag_df.shape)
print(cos_tag.shape)
caculate_genres_df = caculate_genres_df.set_index('movieId')
cos_genres = cosine_similarity(caculate_genres_df.values)
print(caculate_genres_df.shape)
print(cos_genres.shape)
caculate_rating_group_df = caculate_rating_group_df.set_index('movieId')
cos_rating = cosine_similarity(caculate_rating_group_df.values)
print(caculate_rating_group_df.shape)
print(cos_rating.shape)
接下來要把這三面向合併前,需要先處理矩陣形狀不一致的問題
由於不同數據集的電影數量可能不一致,導致生成的相似度矩陣形狀不同。我們需要對矩陣進行填充,統一形狀。
max_shape = max(cos_tag.shape[0], cos_genres.shape[0], cos_rating.shape[0])
def pad_matrix(matrix, target_shape):
padded_matrix = np.zeros((target_shape, target_shape)) # 創建目標大小的零矩陣
padded_matrix[:matrix.shape[0], :matrix.shape[1]] = matrix # 將原矩陣填入
return padded_matrix
cos_tag = pad_matrix(cos_tag, max_shape)
cos_genres = pad_matrix(cos_genres, max_shape)
cos_rating = pad_matrix(cos_rating, max_shape)
通過加權的方式做合併
cos = cos_tag * 0.5 + cos_genres * 0.25 + cos_rating * 0.25
print(cos.shape)
print(cos)
這樣我們就完成了相似度矩陣的建立!
相似度矩陣以 movieId 作為 index ,方便後續好查詢
cols = caculate_genres_df.index.values
inx = caculate_genres_df.index
movies_sim = pd.DataFrame(cos, columns=cols, index=inx)
movies_sim.head()
根據 movieId 提取相似的電影
# 通用函數:根據 movieId 提取相似的電影
def calculate_similar_movies(movieId, top_n=5, include_details=False, movie_df=None):
df = movies_sim.loc[movies_sim.index == movieId].reset_index(). \
melt(id_vars='movieId', var_name='sim_moveId', value_name='relevance'). \
sort_values('relevance', axis=0, ascending=False)[1:top_n+1]
if include_details and movie_df is not None:
df['sim_moveId'] = df['sim_moveId'].astype(int)
df = movie_df.merge(df, left_on='movieId', right_on='sim_moveId', how='inner'). \
sort_values('relevance', axis=0, ascending=False). \
loc[:, ['movieId_y', 'title', 'genres']]. \
rename(columns={'movieId_y': "movieId"})
return df
# 計算所有電影的相似矩陣
def calculate_all_similarities(top_n=5):
similarity_list = []
for movieId in movies_sim.index.tolist():
similarity_list.append(calculate_similar_movies(movieId, top_n=top_n))
return pd.concat(similarity_list, ignore_index=True)
# 單部電影推薦函數
def movie_recommender(movieId):
return calculate_similar_movies(movieId, top_n=5, include_details=True, movie_df=movie_df)
測試推薦的功能
#get recommendation for movie
choice_movieId = movieId_list.sample(1).iloc[0]
print('choice movie:')
print(movie_df[movie_df['movieId'] == choice_movieId])
print()
print('recommendation:')
print(movie_recommender(choice_movieId))
測試結果
目前的測試結果顯示,推薦效果可能顯得普通。這其中的一個主要原因是在實驗中縮小了資料庫中的電影數量,這直接影響了推薦系統能提供的電影選擇範圍。之所以採取這樣的方式,主要是受運算過程中記憶體限制的影響。
這種縮小資料庫的策略雖然解決了記憶體不足的問題,但也暴露了當前算法的一些缺點。特別是稀疏矩陣的使用會導致記憶體的高度耗用,從而限制了算法的擴展性和計算效率。針對這些問題,推薦系統逐漸演化出了更多不同的方法,例如採用降維技術或基於深度學習的推薦算法,這些方法在提升運算效率的同時,也能有效處理記憶體消耗的問題。
未來的改進方向包括:
這些改進不僅能解決記憶體限制的問題,也能提高算法的推薦效果和運算效率,為用戶提供更加精確且豐富的電影推薦選擇。
透過本篇文章,我們完整地實作了基於相似度矩陣的電影推薦系統,從資料預處理到餘弦相似度的計算,再到最終生成推薦結果。下一篇預告:
在下一篇文章中,我們將進一步探討如何利用 Neo4j 這一圖數據庫的強大功能來構建推薦系統。
感謝讀到這裡的你,我們下次見!