第14章推薦系統項目實戰——打造音樂推薦系統python
上一章介紹了推薦系統的基本原理,本章的目標就要從零開始打造一個音樂推薦系統,包括音樂數據集預處理、基於類似度進行推薦以及基於矩陣分解進行推薦。git
14.1數據集清洗算法
不少時候拿到手的數據集並不像想象中那麼完美,基本都須要先把數據清洗一番才能使用,首先導入須要的Python工具包:sql
1 import pandas as pd 2 import numpy as np 3 import time 4 import sqlite3 5 6 data_home = './'
因爲數據中有一部分是數據庫文件,須要使用sqlite3工具包進行數據的讀取,你們能夠根據本身狀況設置數據存放路徑。數據庫
先來看一下數據的規模,對於不一樣格式的數據,read_csv()函數中有不少參數能夠選擇,例如分隔符與列名:app
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個指標特徵。dom
若是想更詳細地瞭解數據的狀況,能夠打印其info信息,下面觀察不一樣列的類型以及總體佔用內存:eclipse
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