第14章推薦系統項目實戰——打造音樂推薦系統
上一章介紹了推薦系統的基本原理,本章的目標就要從零開始打造一個音樂推薦系統,包括音樂數據集預處理、基於相似度進行推薦以及基於矩陣分解進行推薦。
14.1數據集清洗
很多時候拿到手的數據集並不像想象中那么完美,基本都需要先把數據清洗一番才能使用,首先導入需要的Python工具包:
1 import pandas as pd 2 import numpy as np 3 import time 4 import sqlite3 5 6 data_home = './'
由於數據中有一部分是數據庫文件,需要使用sqlite3工具包進行數據的讀取,大家可以根據自己情況設置數據存放路徑。
先來看一下數據的規模,對於不同格式的數據,read_csv()函數中有很多參數可以選擇,例如分隔符與列名:
1 triplet_dataset = pd.read_csv(filepath_or_buffer=data_home+'train_triplets.txt', 2 sep='\t', header=None, 3 names=['user','song','play_count'])
1 triplet_dataset.shape 2 #(48373586, 3)
輸出結果顯示共48373586個樣本,每個樣本有3個指標特征。
如果想更詳細地了解數據的情況,可以打印其info信息,下面觀察不同列的類型以及整體占用內存:
1 triplet_dataset.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 48373586 entries, 0 to 48373585 Data columns (total 3 columns): # Column Dtype --- ------ ----- 0 user object 1 song object 2 play_count int64 dtypes: int64(1), object(2) memory usage: 1.1+ GB
打印前10條數據:
1 triplet_dataset.head(n=10)
user song play_count
0 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOAKIMP12A8C130995 1
1 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOAPDEY12A81C210A9 1
2 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBBMDR12A8C13253B 2
3 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBFNSP12AF72A0E22 1
4 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBFOVM12A58A7D494 1
5 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBNZDC12A6D4FC103 1
6 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBSUJE12A6D4F8CF5 2
7 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBVFZR12A6D4F8AE3 1
8 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBXALG12A8C13C108 1
9 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBXHDL12A81C204C0 1
數據中包括用戶的編號、歌曲編號以及用戶對該歌曲播放的次數。
14.1.1統計分析
掌握數據整體情況之后,下一步統計出關於用戶與歌曲的各項指標,例如對每一個用戶,分別統計他的播放總量,代碼如下:
1 output_dict = {} 2 with open(data_home+'train_triplets.txt') as f: 3 for line_number, line in enumerate(f): 4 #找到當前的用戶 5 user = line.split('\t')[0] 6 #得到其播放量數據 7 play_count = int(line.split('\t')[2]) 8 #如果字典中已經有該用戶信息,在其基礎上增加當前的播放量 9 if user in output_dict: 10 play_count +=output_dict[user] 11 output_dict.update({user:play_count}) 12 output_dict.update({user:play_count}) 13 # 統計 用戶-總播放量 14 output_list = [{'user':k,'play_count':v} for k,v in output_dict.items()] 15 #轉換成DF格式 16 play_count_df = pd.DataFrame(output_list) 17 #排序 18 play_count_df = play_count_df.sort_values(by = 'play_count', ascending = False)
構建一個字典結構,統計不同用戶分別播放的總數,需要把數據集遍歷一遍。當數據集比較龐大的時候,每一步操作都可能花費較長時間。后續操作中,如果稍有不慎,可能還得從頭再來一遍。這就得不償失,最好把中間結果保存下來。既然已經把結果轉換成df格式,直接使用to_csv()函數,就可以完成保存操作。
1 play_count_df.to_csv(path_or_buf='user_playcount_df.csv', index = False)
在實驗階段,最好把費了好大功夫處理出來的數據保存到本地,免得一個不小心又得重跑一遍,令人頭疼。
對於每一首歌,可以分別統計其播放總量,代碼如下:
1 #統計方法跟上述類似 2 output_dict = {} 3 with open(data_home+'train_triplets.txt') as f: 4 for line_number, line in enumerate(f): 5 #找到當前歌曲 6 song = line.split('\t')[1] 7 #找到當前播放次數 8 play_count = int(line.split('\t')[2]) 9 #統計每首歌曲被播放的總次數 10 if song in output_dict: 11 play_count +=output_dict[song] 12 output_dict.update({song:play_count}) 13 output_dict.update({song:play_count}) 14 output_list = [{'song':k,'play_count':v} for k,v in output_dict.items()] 15 #轉換成df格式 16 song_count_df = pd.DataFrame(output_list) 17 song_count_df = song_count_df.sort_values(by = 'play_count', ascending = False)
1 song_count_df.to_csv(path_or_buf='song_playcount_df.csv', index = False)
下面來看看排序后的統計結果:
1 song_count_df = pd.read_csv(filepath_or_buffer='song_playcount_df.csv') 2 song_count_df.head(10)
上述輸出結果顯示,最忠實的粉絲有13132次播放。
1 song_count_df = pd.read_csv(filepath_or_buffer='song_playcount_df.csv') 2 song_count_df.head(10)
上述輸出結果顯示,最受歡迎的一首歌曲有726885次播放。
由於該音樂數據集十分龐大,考慮執行過程的時間消耗以及矩陣稀疏性問題,依據播放量指標對數據集進行了截取。因為有些注冊用戶可能只是關注了一下,之后就不再登錄平台,這些用戶對后續建模不會起促進作用,反而增大矩陣的稀疏性。對於歌曲也是同理,可能有些歌曲根本無人問津。由於之前已經對用戶與歌曲播放情況進行了排序,所以分別選擇其中按播放量排名的前10萬名用戶和3萬首歌曲,關於截取的合適比例,大家也可以通過觀察選擇數據的播放量占總體的比例來設置。
1 #10W名用戶的播放量占總體的比例 2 total_play_count = sum(song_count_df.play_count) 3 print ((float(play_count_df.head(n=100000).play_count.sum())/total_play_count)*100) 4 play_count_subset = play_count_df.head(n=100000)
40.8807280500655
輸出結果顯示,前10萬名最多使用平台的用戶的播放量占到總播放量的40.88%
(float(song_count_df.head(n=30000).play_count.sum())/total_play_count)*100
78.39315366645269
輸出結果顯示,前3萬首歌的播放量占到總播放量的78.39%。
接下來就要對原始數據集進行過濾清洗,也就是在原始數據集中,剔除掉不包含這10萬名忠實用戶以及3萬首經典歌曲的數據。
1 song_count_subset = song_count_df.head(n=30000) 2 3 user_subset = list(play_count_subset.user) 4 song_subset = list(song_count_subset.song) 5 6 #讀取原始數據集 7 triplet_dataset = pd.read_csv(filepath_or_buffer=data_home+'train_triplets.txt',sep='\t', 8 header=None, names=['user','song','play_count']) 9 #只保留有這10W名用戶的數據,其余過濾掉 10 triplet_dataset_sub = triplet_dataset[triplet_dataset.user.isin(user_subset) ] 11 del(triplet_dataset) 12 #只保留有這3W首歌曲的數據,其余也過濾掉 13 triplet_dataset_sub_song = triplet_dataset_sub[triplet_dataset_sub.song.isin(song_subset)] 14 del(triplet_dataset_sub) 15 triplet_dataset_sub_song.to_csv(path_or_buf=data_home+'triplet_dataset_sub_song.csv', index=False)
再來看一下過濾后的數據規模:
1 triplet_dataset_sub_song.shape
#(10774558, 3)
雖然過濾后的數據樣本個數不到原來的1/4,但是過濾掉的樣本都是稀疏數據,不利於建模,所以,當拿到數據之后,對數據進行清洗和預處理工作還是非常有必要的,它不僅能提升計算的速度,還會影響最終的結果。
14.1.2數據集整合
目前拿到的音樂數據只有播放次數,可利用的信息實在太少,對每首歌曲來說,正常情況下,都應該有一份詳細信息,例如歌手、發布時間、主題等,這些信息都存在一份數據庫格式文件中,接下來通過sqlite工具包讀取這些數據:
1 conn = sqlite3.connect(data_home+'track_metadata.db') 2 cur = conn.cursor() 3 cur.execute("SELECT name FROM sqlite_master WHERE type='table'") 4 cur.fetchall() 5 6 track_metadata_df = pd.read_sql(con=conn, sql='select * from songs') 7 track_metadata_df_sub = track_metadata_df[track_metadata_df.song_id.isin(song_subset)] 8 9 track_metadata_df_sub.to_csv(path_or_buf=data_home+'track_metadata_df_sub.csv', index=False) 10 11 track_metadata_df_sub.shape
#(30447, 14)
這里並不需要大家熟練掌握sqlite工具包的使用方法,只是在讀取.db文件時,用它更方便一些,大家也可以直接讀取保存好的.csv文件。
1 triplet_dataset_sub_song = pd.read_csv(filepath_or_buffer=data_home+'triplet_dataset_sub_song.csv',encoding = "ISO-8859-1") 2 track_metadata_df_sub = pd.read_csv(filepath_or_buffer=data_home+'track_metadata_df_sub.csv',encoding = "ISO-8859-1")
1 triplet_dataset_sub_song.head() 2 3 track_metadata_df_sub.head()
這回就有了一份詳細的音樂作品清單,該份數據一共有14個指標,只選擇需要的特征信息來利用:
1 # 去掉無用的信息 2 del(track_metadata_df_sub['track_id']) 3 del(track_metadata_df_sub['artist_mbid']) 4 # 去掉重復的 5 track_metadata_df_sub = track_metadata_df_sub.drop_duplicates(['song_id']) 6 # 將這份音樂信息數據和我們之前的播放數據整合到一起 7 triplet_dataset_sub_song_merged = pd.merge(triplet_dataset_sub_song, track_metadata_df_sub, how='left', left_on='song', right_on='song_id') 8 # 可以自己改變列名 9 triplet_dataset_sub_song_merged.rename(columns={'play_count':'listen_count'},inplace=True)
1 # 去掉不需要的指標 2 del(triplet_dataset_sub_song_merged['song_id']) 3 del(triplet_dataset_sub_song_merged['artist_id']) 4 del(triplet_dataset_sub_song_merged['duration']) 5 del(triplet_dataset_sub_song_merged['artist_familiarity']) 6 del(triplet_dataset_sub_song_merged['artist_hotttnesss']) 7 del(triplet_dataset_sub_song_merged['track_7digitalid']) 8 del(triplet_dataset_sub_song_merged['shs_perf']) 9 del(triplet_dataset_sub_song_merged['shs_work'])
上述代碼去掉數據中不需要的一些特征,並且把這份音樂數據和之前的音樂播放次數數據整合在一起,現在再來看看這些數據:
1 triplet_dataset_sub_song_merged.head(n=10)
數據經處理后看起來工整多了,不只有用戶對某個音樂作品的播放量,還有該音樂作品的名字和所屬專輯名稱,以及歌手的名字和發布時間。
現在只是大體了解了數據中各個指標的含義,對其具體內容還沒有加以分析,推薦系統還可能會遇到過冷啟動問題,也就是一個新用戶來了,不知道給他推薦什么好,這時候就可以利用排行榜單,統計最受歡迎的歌曲和歌手:
1 import matplotlib.pyplot as plt; plt.rcdefaults() 2 import numpy as np 3 import matplotlib.pyplot as plt 4 #按歌曲名字來統計其播放量的總數 5 popular_songs = triplet_dataset_sub_song_merged[['title','listen_count']].groupby('title').sum().reset_index() 6 #對結果進行排序 7 popular_songs_top_20 = popular_songs.sort_values('listen_count', ascending=False).head(n=20) 8 9 #轉換成list格式方便畫圖 10 objects = (list(popular_songs_top_20['title'])) 11 #設置位置 12 y_pos = np.arange(len(objects)) 13 #對應結果值 14 performance = list(popular_songs_top_20['listen_count']) 15 #繪圖 16 plt.bar(y_pos, performance, align='center', alpha=0.5) 17 plt.xticks(y_pos, objects, rotation='vertical') 18 plt.ylabel('Item count') 19 plt.title('Most popular songs') 20 21 plt.show()
使用groupby函數可以很方便地統計每首歌曲的播放情況,也就是播放量。這份排行數據可以當作最受歡迎的歌曲推薦給用戶,把大家都喜歡的推薦出去,也是大概率受歡迎的。
采用同樣的方法,可以對專輯和歌手的播放情況分別進行統計:
1 #按專輯名字來統計播放總量 2 popular_release = triplet_dataset_sub_song_merged[['release','listen_count']].groupby('release').sum().reset_index() 3 #排序 4 popular_release_top_20 = popular_release.sort_values('listen_count', ascending=False).head(n=20) 5 6 objects = (list(popular_release_top_20['release'])) 7 y_pos = np.arange(len(objects)) 8 performance = list(popular_release_top_20['listen_count']) 9 #繪圖 10 plt.bar(y_pos, performance, align='center', alpha=0.5) 11 plt.xticks(y_pos, objects, rotation='vertical') 12 plt.ylabel('Item count') 13 plt.title('Most popular Release') 14 15 plt.show()
1 #按歌手來統計其播放總量 2 popular_artist = triplet_dataset_sub_song_merged[['artist_name','listen_count']].groupby('artist_name').sum().reset_index() 3 #排序 4 popular_artist_top_20 = popular_artist.sort_values('listen_count', ascending=False).head(n=20) 5 6 objects = (list(popular_artist_top_20['artist_name'])) 7 y_pos = np.arange(len(objects)) 8 performance = list(popular_artist_top_20['listen_count']) 9 #繪圖 10 plt.bar(y_pos, performance, align='center', alpha=0.5) 11 plt.xticks(y_pos, objects, rotation='vertical') 12 plt.ylabel('Item count') 13 plt.title('Most popular Artists') 14 15 plt.show()
這份數據中,還有很多信息值得關注,這里只舉例進行分析,實際任務中還是要把所有潛在的信息全部考慮進來,再來看一下該平台用戶播放的分布情況:
1 user_song_count_distribution = triplet_dataset_sub_song_merged[['user','title']].groupby('user').count().reset_index().sort_values( 2 by='title',ascending = False) 3 user_song_count_distribution.title.describe()
count 99996.000000 mean 107.749890 std 79.742561 min 1.000000 25% 53.000000 50% 89.000000 75% 141.000000 max 1189.000000 Name: title, dtype: float64
通過describe()函數可以得到其具體的統計分布指標,但這樣看不夠直觀,最好還是通過繪圖展示:
1 x = user_song_count_distribution.title 2 n, bins, patches = plt.hist(x, 50, facecolor='green', alpha=0.75) 3 plt.xlabel('Play Counts') 4 plt.ylabel('Num of Users') 5 plt.title(r'$\mathrm{Histogram\ of\ User\ Play\ Count\ Distribution}\ $') 6 plt.grid(True) 7 plt.show()
輸出結果顯示絕大多數用戶播放100首歌曲左右,一部分用戶只是聽一聽,特別忠實的粉絲占比較少。現在已經做好數據的處理和整合,接下來就是構建一個能實際進行推薦的程序。
14.2基於相似度的推薦
如何推薦一首歌曲呢?最直接的想法就是推薦大眾都認可的或者基於相似度來猜測他們的口味。
14.2.1排行榜推薦
最簡單的推薦方式就是排行榜單,這里創建了一個函數,需要傳入原始數據、用戶列名、待統計的指標(例如按歌曲名字、歌手名字、專輯名字,也就是選擇使用哪些指標得到排行榜單):
1 import Recommenders as Recommenders 2 from sklearn.model_selection import train_test_split 3 4 triplet_dataset_sub_song_merged_set = triplet_dataset_sub_song_merged 5 train_data, test_data = train_test_split(triplet_dataset_sub_song_merged_set, test_size = 0.40, random_state=0) 6 7 train_data.head() 8 9 def create_popularity_recommendation(train_data, user_id, item_id): 10 #根據指定的特征來統計其播放情況,可以選擇歌曲名,專輯名,歌手名 11 train_data_grouped = train_data.groupby([item_id]).agg({user_id: 'count'}).reset_index() 12 #為了直觀展示,我們用得分來表示其結果 13 train_data_grouped.rename(columns = {user_id: 'score'},inplace=True) 14 15 #排行榜單需要排序 16 train_data_sort = train_data_grouped.sort_values(['score', item_id], ascending = [0,1]) 17 18 #加入一項排行等級,表示其推薦的優先級 19 train_data_sort['Rank'] = train_data_sort['score'].rank(ascending=0, method='first') 20 21 #返回指定個數的推薦結果 22 popularity_recommendations = train_data_sort.head(20) 23 return popularity_recommendations 24 25 recommendations = create_popularity_recommendation(triplet_dataset_sub_song_merged,'user','title')
上述代碼返回一份前20名的歌曲排行榜單,對於其中的得分,這里只是進行了簡單的播放計算,在設計的時候,也可以綜合考慮更多的指標,例如綜合計算歌曲發布年份、歌手的流行程度等。
14.2.2基於歌曲相似度的推薦
另一種方案就要使用相似度計算推薦歌曲,為了加快代碼的運行速度,選擇其中一部分數據進行實驗。
1 song_count_subset = song_count_df.head(n=5000) 2 user_subset = list(play_count_subset.user) 3 song_subset = list(song_count_subset.song) 4 triplet_dataset_sub_song_merged_sub = triplet_dataset_sub_song_merged[triplet_dataset_sub_song_merged.song.isin(song_subset)]
實驗階段,可以先用部分數據來測試,確定代碼無誤后,再用全部數據跑一遍,這樣比較節約時間,畢竟代碼都是不斷通過實驗來修正的。
下面執行相似度計算:
1 import Recommenders as Recommenders 2 train_data, test_data = train_test_split(triplet_dataset_sub_song_merged_sub, test_size = 0.30, random_state=0) 3 is_model = Recommenders.item_similarity_recommender_py() 4 is_model.create(train_data, 'user', 'title') 5 user_id = list(train_data.user)[7] 6 user_items = is_model.get_user_items(user_id)
細心的讀者應該觀察到了,首先導入Recommenders,它類似於一個自定義的工具包,包括接下來要使用的所有函數。由於要計算的代碼量較大,直接在Notebook中進行展示比較麻煩,所以需要寫一個.py文件,所有實際計算操作都在這里完成。
大家在實踐這份代碼的時候,可以選擇一個合適的IDE,因為Notebook並不支持debug操作。拿到一份陌生的代碼而且量又比較大的時候,最好先通過debug方式一行代碼一行代碼地執行,這樣才可以更清晰地熟悉整個函數做了什么。
對於初學者來說,直接看整體代碼可能有些難度,建議大家選擇一個合適的IDE,例如pycharm、eclipse等都是不錯的選擇。
Is_model.create(train_data,’user’,’title’)表示該函數需要傳入原始數據、用戶ID和歌曲信息,相當於得到所需數據,源碼如下:
1 #Create the item similarity based recommender system model 2 def create(self, train_data, user_id, item_id): 3 self.train_data = train_data 4 self.user_id = user_id 5 self.item_id = item_id
User_id=list(train_data.user)[7]表示這里需要選擇一個用戶,哪個用戶都可以,基於他進行推薦。
Is_model.get_user_items(user_id)表示得到該用戶聽過的所有歌曲,源碼如下:
1 #Get unique items (songs) corresponding to a given user 2 def get_user_items(self, user): 3 user_data = self.train_data[self.train_data[self.user_id] == user] 4 user_items = list(user_data[self.item_id].unique()) 5 6 return user_items
Is_model.recommend(user_id)表示全部的核心計算,首先展示其流程,然后再分別解釋其細節:
1 #Use the item similarity based recommender system model to 2 #make recommendations 3 def recommend(self, user): 4 5 ######################################## 6 #A. Get all unique songs for this user 7 ######################################## 8 user_songs = self.get_user_items(user) 9 10 print("No. of unique songs for the user: %d" % len(user_songs)) 11 12 ###################################################### 13 #B. Get all unique items (songs) in the training data 14 ###################################################### 15 all_songs = self.get_all_items_train_data() 16 17 print("no. of unique songs in the training set: %d" % len(all_songs)) 18 19 ############################################### 20 #C. Construct item cooccurence matrix of size 21 #len(user_songs) X len(songs) 22 ############################################### 23 cooccurence_matrix = self.construct_cooccurence_matrix(user_songs, all_songs) 24 25 ####################################################### 26 #D. Use the cooccurence matrix to make recommendations 27 ####################################################### 28 df_recommendations = self.generate_top_recommendations(user, cooccurence_matrix, all_songs, user_songs) 29 30 return df_recommendations
上述代碼的關鍵點就是第3步計算相似矩陣了。其中cooccurence_matrix=self.construct_cooccurence_matrix(user_songs,all_songs)表示需要傳入該用戶聽過哪些歌曲,以及全部數據集中有多少歌曲。下面通過源碼解讀一下其計算流程:
1 #Construct cooccurence matrix 2 def construct_cooccurence_matrix(self, user_songs, all_songs): 3 4 #################################### 5 #Get users for all songs in user_songs. 6 #################################### 7 user_songs_users = [] 8 for i in range(0, len(user_songs)): 9 user_songs_users.append(self.get_item_users(user_songs[i])) 10 11 ############################################### 12 #Initialize the item cooccurence matrix of size 13 #len(user_songs) X len(songs) 14 ############################################### 15 cooccurence_matrix = np.matrix(np.zeros(shape=(len(user_songs), len(all_songs))), float) 16 17 ############################################################# 18 #Calculate similarity between user songs and all unique songs 19 #in the training data 20 ############################################################# 21 for i in range(0,len(all_songs)): 22 #Calculate unique listeners (users) of song (item) i 23 songs_i_data = self.train_data[self.train_data[self.item_id] == all_songs[i]] 24 users_i = set(songs_i_data[self.user_id].unique()) 25 26 for j in range(0,len(user_songs)): 27 28 #Get unique listeners (users) of song (item) j 29 users_j = user_songs_users[j] 30 31 #Calculate intersection of listeners of songs i and j 32 users_intersection = users_i.intersection(users_j) 33 34 #Calculate cooccurence_matrix[i,j] as Jaccard Index 35 if len(users_intersection) != 0: 36 #Calculate union of listeners of songs i and j 37 users_union = users_i.union(users_j) 38 39 cooccurence_matrix[j,i] = float(len(users_intersection))/float(len(users_union)) 40 else: 41 cooccurence_matrix[j,i] = 0 42 43 44 return cooccurence_matrix
整體代碼量較多,先從整體上介紹這段代碼做了什么,大家debug一遍,效果會更好。首先,想要針對某個用戶進行推薦,需要先知道他聽過哪些歌曲,將已被聽過的歌曲與整個數據集中的歌曲進行對比,看哪些歌曲與用戶已聽過的歌曲相似,就推薦這些相似的歌曲。
如何計算呢?例如,當前用戶聽過66首歌曲,整個數據集有4879首歌曲,那么,可以構建一個[66,4879]矩陣,表示用戶聽過的每一個歌曲和數據集中每一個歌曲的相似度。這里使用Jaccard相似系數,矩陣 [I,j]中,i表示用戶聽過的第i首歌曲被多少人聽過,例如被3000人聽過;j表示j這首歌曲被多少人聽過,例如被5000人聽過。Jaccard相似系數計算式為:
如果兩個歌曲相似,其受眾應當一致,Jaccard相似系數的值應該比較大。如果兩個歌曲沒什么相關性,其值應當比較小。
最后推薦的時候,還應當注意:對於數據集中每一首待推薦的歌曲,都需要與該用戶所有聽過的歌曲合在一起計算Jaccard值。例如,歌曲j需要與用戶聽過的66首歌曲合在一起計算Jaccard值,還要處理最終是否推薦的得分值,即把這66個值加在一起,最終求一個平均值,代表該歌曲的平均推薦得分。也就是說,給用戶推薦歌曲時,不能單憑一首歌進行推薦,需要考慮所有用戶聽過的所有歌曲。
對於每一位用戶來說,通過相似度計算,可以得到數據集中每一首歌曲的得分值以及排名,然后可以向每一個用戶推薦其可能喜歡的歌曲,推薦的最終結果如圖14-1所示。
1 #執行推薦 2 is_model.recommend(user_id)
No. of unique songs for the user: 66 no. of unique songs in the training set: 4879 Non zero values in cooccurence_matrix :290327
#運行大約25分鍾
圖14-1 推薦的最終結果
14.3基於矩陣分解的推薦
相似度計算的方法看起來比較簡單,很容易就能實現,但是,當數據較大的時候,計算的開銷實在太大,對每一個用戶都需要多次遍歷整個數據集進行計算,這很難實現。矩陣分解可以更快速地得到結果,也是當下比較熱門的方法。
14.3.1奇異值分解
奇異值分解(Singular Value Decomposition,SVD)是矩陣分解中一個經典方法,接下來的推薦就可以使用SVD進行計算,它的基本出發點與隱語義模型類似,都是將大矩陣轉換成小矩陣的組合,它的最基本形式如圖14-2所示。
圖14-2 SVD矩陣分解
其中n和m都是比較大的數值,代表原始數據;r是較小的數值,表示矩陣分解后的結果可以用較小的矩陣組合來近似替代。下面借用一個經典的小例子,看一下SVD如何應用在推薦系統中(見圖14-3)。
圖14-3 用戶評分矩陣
首先將數據轉換成矩陣形式,如下所示:
對上述矩陣執行SVD分解,結果如下:
依照SVD計算公式:
A=USVT (14.1)
其中,U、S和V分別為分解后的小矩陣,通常更關注S矩陣,S矩陣的每一個值都代表該位置的重要性指標,它與降維算法中的特征值和特征向量的關系類似。
如果只在S矩陣中選擇一部分比較重要的特征值,相應的U和V矩陣也會發生改變,例如只保留2個特征值。
再把上面3個矩陣相乘,即A2=USVT,結果如下:
對比矩陣A2和矩陣A,可以發現二者之間的數值很接近。如果將U矩陣的第一列當成x值,第二列當成y值,也就是把U矩陣的每一行在二維空間中進行展示。同理V矩陣也是相同操作,可以得到一個有趣的結果。
SVD矩陣分解后的意義如圖14-4所示,可以看出用戶之間以及商品之間的相似性關系,假設現在有一個名叫Flower的新用戶,已知該用戶對各個商品的評分向量為 [5 5 0 0 0 5],需要向這個用戶進行商品的推薦,也就是根據這個用戶的評分向量尋找與該用戶相似的用戶,進行如下計算:
圖14-4 SVD矩陣分解后的意義
現在可以在上述的二維坐標中尋找這個坐標點,然后看這個點與其他點的相似度,根據相似程度進行推薦。
14.3.2使用SVD算法進行音樂推薦
在SVD中所需的數據是用戶對商品的打分,但在現在的數據集中,只有用戶播放歌曲的情況,並沒有實際的打分值,所以,需要定義用戶對每首歌曲的評分值。如果一個用戶喜歡某首歌曲,他應該經常播放這首歌曲;相反,如果不喜歡某首歌曲,播放次數肯定比較少。
在建模過程中,使用工具包非常方便,但是一定要知道輸入的是什么數據,倒推也是不錯的思路,先知道想要輸入什么,然后再對數據進行處理操作。
用戶對歌曲的打分值,定義為用戶播放該歌曲數量/該用戶播放總量。代碼如下:
1 triplet_dataset_sub_song_merged_sum_df = triplet_dataset_sub_song_merged[['user','listen_count']].groupby('user').sum().reset_index() 2 triplet_dataset_sub_song_merged_sum_df.rename(columns={'listen_count':'total_listen_count'},inplace=True) 3 triplet_dataset_sub_song_merged = pd.merge(triplet_dataset_sub_song_merged,triplet_dataset_sub_song_merged_sum_df) 4 triplet_dataset_sub_song_merged.head()
1 triplet_dataset_sub_song_merged['fractional_play_count'] = \
triplet_dataset_sub_song_merged['listen_count']/triplet_dataset_sub_song_merged['total_listen_count']
1 triplet_dataset_sub_song_merged[triplet_dataset_sub_song_merged.user =='d6589314c0a9bcbca4fee0c93b14bc402363afea'][['user','song','listen_count','fractional_play_count']].head()
user song listen_count fractional_play_count
0 d6589314c0a9bcbca4fee0c93b14bc402363afea SOADQPP12A67020C82 12 0.036474
1 d6589314c0a9bcbca4fee0c93b14bc402363afea SOAFTRR12AF72A8D4D 1 0.003040
2 d6589314c0a9bcbca4fee0c93b14bc402363afea SOANQFY12AB0183239 1 0.003040
3 d6589314c0a9bcbca4fee0c93b14bc402363afea SOAYATB12A6701FD50 1 0.003040
4 d6589314c0a9bcbca4fee0c93b14bc402363afea SOBOAFP12A8C131F36 7 0.021277
上述代碼先根據用戶進行分組,計算每個用戶的總播放量,然后用每首歌曲的播放量除以該用戶的總播放量。最后一列特征fractional_play_count就是用戶對每首歌曲的評分值。
評分值確定之后,就可以構建矩陣了,這里有一些小問題需要處理,原始數據中,無論是用戶ID還是歌曲ID都是很長一串,表達起來不太方便,需要重新對其制作索引。
1 user_codes[user_codes.user =='2a2f776cbac6df64d6cb505e7e834e01684673b6']
user_index user us_index_value
27516 2981434 2a2f776cbac6df64d6cb505e7e834e01684673b6 27516
在矩陣中,知道用戶ID、歌曲ID、評分值就足夠了,需要去掉其他指標(見圖14-5)。由於數據集比較稀疏,為了計算、存儲的高效,可以用索引和評分表示需要的數值,其他位置均為0。
圖14-5 評分矩陣
整體實現代碼如下:
1 from scipy.sparse import coo_matrix 2 3 small_set = triplet_dataset_sub_song_merged 4 user_codes = small_set.user.drop_duplicates().reset_index() 5 song_codes = small_set.song.drop_duplicates().reset_index() 6 user_codes.rename(columns={'index':'user_index'}, inplace=True) 7 song_codes.rename(columns={'index':'song_index'}, inplace=True) 8 song_codes['so_index_value'] = list(song_codes.index) 9 user_codes['us_index_value'] = list(user_codes.index) 10 small_set = pd.merge(small_set,song_codes,how='left') 11 small_set = pd.merge(small_set,user_codes,how='left') 12 mat_candidate = small_set[['us_index_value','so_index_value','fractional_play_count']] 13 data_array = mat_candidate.fractional_play_count.values 14 row_array = mat_candidate.us_index_value.values 15 col_array = mat_candidate.so_index_value.values 16 17 data_sparse = coo_matrix((data_array, (row_array, col_array)),dtype=float)
矩陣構造好之后,就要執行SVD矩陣分解,這里還需要一些額外的工具包完成計算,scipy就是其中一個好幫手,里面已經封裝好SVD計算方法。
1 import math as mt 2 from scipy.sparse.linalg import * #used for matrix multiplication 3 from scipy.sparse.linalg import svds 4 from scipy.sparse import csc_matrix
在執行SVD的時候,需要額外指定K值,其含義就是選擇前多少個特征值來做近似代表,也就是S矩陣的維數。如果K值較大,整體的計算效率會慢一些,但是會更接近真實結果,這個值需要自己衡量。
1 def compute_svd(urm, K): 2 U, s, Vt = svds(urm, K) 3 4 dim = (len(s), len(s)) 5 S = np.zeros(dim, dtype=np.float32) 6 for i in range(0, len(s)): 7 S[i,i] = mt.sqrt(s[i]) 8 9 U = csc_matrix(U, dtype=np.float32) 10 S = csc_matrix(S, dtype=np.float32) 11 Vt = csc_matrix(Vt, dtype=np.float32) 12 13 return U, S, Vt
此處選擇的K值等於50,其中PID表示最開始選擇的部分歌曲,UID表示選擇的部分用戶。
1 K=50 2 urm = data_sparse 3 MAX_PID = urm.shape[1] 4 MAX_UID = urm.shape[0] 5 6 U, S, Vt = compute_svd(urm, K)
執行過程中,還可以打印出各個矩陣的大小,並進行觀察分析。
強烈建議大家將代碼復制到IDE中,打上斷點一行一行地走下去,觀察其中每一個變量的值,這對理解整個流程非常有幫助。
接下來需要選擇待測試用戶:
1 uTest = [4,5,6,7,8,873,23] 2 3 uTest_recommended_items = compute_estimated_matrix(urm, U, S, Vt, uTest, K, True)
隨便選擇一些用戶就好,其中的數值表示用戶的索引編號,接下來需要對每一個用戶計算其對候選集中3萬首歌曲的喜好程度,也就是估計他對這3萬首歌的評分值應該等於多少,前面通過SVD矩陣分解已經計算出所需的各個小矩陣,接下來把其還原回去即可:
1 def compute_estimated_matrix(urm, U, S, Vt, uTest, K, test): 2 rightTerm = S*Vt 3 max_recommendation = 250 4 estimatedRatings = np.zeros(shape=(MAX_UID, MAX_PID), dtype=np.float16) 5 recomendRatings = np.zeros(shape=(MAX_UID,max_recommendation ), dtype=np.float16) 6 for userTest in uTest: 7 prod = U[userTest, :]*rightTerm 8 estimatedRatings[userTest, :] = prod.todense() 9 recomendRatings[userTest, :] = (-estimatedRatings[userTest, :]).argsort()[:max_recommendation] 10 return recomendRatings
計算好推薦結果之后,可以進行打印展示:
1 for user in uTest: 2 print("當前待推薦用戶編號 {}". format(user)) 3 rank_value = 1 4 for i in uTest_recommended_items[user,0:10]: 5 song_details = small_set[small_set.so_index_value == i].drop_duplicates('so_index_value')[['title','artist_name']] 6 print("推薦編號: {} 推薦歌曲: {} 作者: {}".format(rank_value, list(song_details['title'])[0],list(song_details['artist_name'])[0])) 7 rank_value+=1
輸出結果顯示每一個用戶都得到了與其對應的推薦結果,並且將結果按照得分值進行排序,也就完成了推薦工作。從整體效率上比較,還是優於相似度計算的方法。
最終沒運行到結果,是內存不足,世紀最大遺憾!
第二天適逢周末,於是嘗試修改了下虛擬內存,結果成功運行。其實16G物理內存並沒有用完,但是python運行時卻受虛擬內存制約。原來的2G(SSD)+2G(HDD)改為8G+8G即可。
項目小結:本章選擇音樂數據集進行個性化推薦任務,首先對數據進行預處理和整合,並選擇兩種方法分別完成推薦任務。在相似度計算中,根據用戶所聽過的歌曲,在候選集中選擇與其最相似的歌曲,存在的問題就是計算消耗太多,每一個用戶都需要重新計算一遍,才能得出推薦結果。在SVD矩陣分解的方法中,首先構建評分矩陣,對其進行SVD分解,然后選擇待推薦用戶,還原得到其對所有歌曲的估測評分值,最后排序,返回結果即可。
第14章完。
該書資源下載,請至異步社區:https://www.epubit.com