《Python數據分析與機器學習實戰-唐宇迪》讀書筆記第14章--音樂推薦系統實戰

python數據分析個人學習讀書筆記-目錄索引

第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章完。

python數據分析個人學習讀書筆記-目錄索引

 

該書資源下載,請至異步社區:https://www.epubit.com

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章