《Python数据分析与机器学习实战-唐宇迪》读书笔记第14章--音乐推荐系统实战
第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