音乐推荐引擎(个人兴趣)

音乐推荐引擎

关键词:pandas、sklearn、surprise、推荐系统、协同过滤

写在前面:

除pandas、sklearn等之外本次需要额外安装的包:seaborn(非必需,可用matplotlib.pyplot替换)、wordcloud(非必需,用于画词云图)、surprise(必需,提供推荐算法的库)。在安装surprise(pip install surprise)时我遇到了问题,可能具有普遍性,将过程分享如下:

error: Microsoft Visual C++ 14.0 or greater is required. Get it with “Microsoft C++ Build Tools”: https://visualstudio.microsoft.com/visual-cpp-build-tools/
[end of output]

note: This error originates from a subprocess, and is likely not a problem with pip.
error: legacy-install-failure

× Encountered error while trying to install package.
╰─> scikit-surprise

note: This is an issue with the package mentioned above, not pip.


我的问题在这里得到了解决:https://blog.csdn.net/Lc_001/article/details/129195335

================================ 正文分割号 ================================

目录

  • 一、数据集介绍
  • 二、基于排行榜的推荐
  • 三、基于协同过滤的推荐
  • 四、基于矩阵分解的推荐
  • 五、基于GBDT+LR预估的排序
  • 六、结语
# 导入包
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
import sqlite3
import seaborn as sns
import matplotlib.pyplot as plt
from wordcloud import WordCloud
from surprise import KNNBasic
from surprise import SVD
from surprise import Reader, Dataset, accuracy
from surprise.model_selection import KFold
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split

一、探索数据集

  • 我们的数据集
  • 数据集预处理

数据集是从网上的一个项目中获得的,这个项目由The Echonest和LABRosa一起完成。数据集主要是多年间外国音乐的量化特征,包含百万用户对几十万首歌曲的播放记录(train_triplets.txt,2.9G)和这些歌曲的详细信息(triplets_metadata.db,700M)。

用户的播放记录数据集train_triplets.txt的格式为:用户 歌曲 播放次数,其中用户和歌曲都匿名。

歌曲的详细信息数据集triplets_metadata.db则包括歌曲的发布时间、作者、作者热度等。

由于数据集太大,因此需要进行裁剪,仅从.txt文件中选取200万条数据使用。

1.1 对txt文件的处理

1. 通过编码和转换数据类型降低数据内存
2. 过滤掉播放量过低的用户
# 读取数据
data = pd.read_csv(r'C:\Users\admin\Desktop\如何才能毕业\MyProject\MusicRecommendationSystem\音乐推荐系统数据集\train_triplets.txt', 
                   sep='\t', header=None, 
                   names=['user', 'song', 'play_count'], nrows=2000000)   # 数据本身没有表头,自行添加
data.head()
usersongplay_count
0b80344d063b5ccb3212f76538f3d9e43d87dca9eSOAKIMP12A8C1309951
1b80344d063b5ccb3212f76538f3d9e43d87dca9eSOAPDEY12A81C210A91
2b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBBMDR12A8C13253B2
3b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBFNSP12AF72A0E221
4b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBFOVM12A58A7D4941
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000000 entries, 0 to 1999999
Data columns (total 3 columns):
 #   Column      Dtype 
---  ------      ----- 
 0   user        object
 1   song        object
 2   play_count  int64 
dtypes: int64(1), object(2)
memory usage: 45.8+ MB

可以看到,用户名和歌曲名已经被加过密,但并不妨碍我们做推荐。

为了方便后面快速运算,我们需要降低其内存大小。具体的,我们对user和song进行labelencoder,并将所有的数据类型转化为int32

# label编码
user_encoder = LabelEncoder()
data['user'] = user_encoder.fit_transform(data['user'].values)

song_encoder = LabelEncoder()
data['song'] = song_encoder.fit_transform(data['song'].values)


# 数据类型转换
data.astype({'user': 'int32', 'song': 'int32', 'play_count': 'int32'})
usersongplay_count
02999032091
12999047021
22999084752
32999097071
42999097171
............
1999995148691509102
1999996148691528031
1999997148691630722
1999998148691716501
199999914759346821

2000000 rows × 3 columns

# 当前数据集信息
data.info()

可以看到,**内存从450M降低到300M。**接下来需要对数据集进行进一步过滤,减少无效数据或者贡献不大的数据,以减少数据集,减少计算量。

# 查看用户歌曲播放总量的分布情况。
# 字典user_playcounts记录每个用户的播放总量
user_playcounts = {}
for user, group in data.groupby('user'):
    user_playcounts[user] = group['play_count'].sum()
    

# 作图
sns.displot(list(user_playcounts.values()), bins=5000, kde=False)
plt.xlim(0, 200)
plt.xlabel('play_count')
plt.ylabel('nums of user')
plt.show()

在这里插入图片描述

可以看到,有大量用户歌曲播放量少于100,我们可以看看这些用户在总体数据上的占比情况。

temp_user = [user for user in user_playcounts.keys() if user_playcounts[user] > 100]  # 播放量大于100的用户
temp_playcounts = [playcounts for user, playcounts in user_playcounts.items() if playcounts > 100]  # 与之对应的播放量

print('歌曲播放量大于100的用户数量占总用户数量的比例:', str(round(len(temp_user)/len(user_playcounts), 4)*100)+'%')
print('歌曲播放量大于100的用户产生的播放总量占总体播放总量的比例为', str(round(sum(temp_playcounts) / sum(user_playcounts.values())*100, 4))+'%')
print('歌曲播放量大于100的用户产生的数据占总体数据的比例为', str(round(len(data[data.user.isin(temp_user)])/len(data)*100, 4))+"%")
歌曲播放量大于100的用户数量占总体用户数量的比例为 39.51%
歌曲播放量大于100的用户产生的播放总量占总体播放总量的比例为 80.278%
歌曲播放量大于100的用户产生的数据占总体数据的比例为 71.26%

通过分析,我们可以看到,播放量大于100的用户占了总体的40%,且这40%的用户,产生了80%的播放量,占了总数据的70%。因此,我们可以考虑将歌曲播放量少于100的用户过滤掉,而不影响整体数据。

# 过滤掉歌曲播放量少于100的用户的数据
data = data[data.user.isin(temp_user)]

类似的,我们挑选出具有一定播放量的歌曲。因为播放量太低的歌曲不但会增加计算复杂度,还会降低协同过滤的准确度。
我们首先看不同歌曲的播放量分布情况。

# song_playcounts字典,记录每首歌的播放量
song_playcounts = {}
songDataFreamGroupBy = data.groupby('song')
for song, group in songDataFreamGroupBy:
    song_playcounts[song] = group['play_count'].sum()
# 作图
sns.displot(list(song_playcounts.values()), bins=10000, kde=False)
plt.xlim(0, 90)
plt.xlabel('play_count')
plt.ylabel('nums of song')
plt.show()

在这里插入图片描述

k可以看到,大部分歌曲的播放量非常少,甚至不到50次!这些歌曲完全无人问津,属于我们可以过滤掉的对象。

temp_song = [song for song in song_playcounts.keys() if song_playcounts[song] > 50]
# temp_song = [song for song, playcounts in song_playcounts.items() if playcounts > 50]
temp_playcounts = [playcounts for song, playcounts in song_playcounts.items() if playcounts > 50]

print('播放量大于50的歌曲数量占总体歌曲数量的比例为:', str(round(len(temp_song)/len(song_playcounts), 4)*100)+'%')
print('播放量大于50的歌曲产生的播放总量占总体播放总量的比例为:', str(round(sum(temp_playcounts) / sum(song_playcounts.values())*100, 4))+'%')
print('播放量大于50的歌曲产生的数据占总体数据的比例为:', str(round(len(data[data.song.isin(temp_song)])/len(data)*100, 4))+"%")
播放量大于50的歌曲数量占总体歌曲数量的比例为: 10.2%
播放量大于50的歌曲产生的播放总量占总体播放总量的比例为: 71.0557%
播放量大于50的歌曲产生的数据占总体数据的比例为: 61.2672%

可以看到,播放量大于50的歌曲数量,占总体数量的27%,而这27%的歌曲,产生的播放总量和数据总量都占60%以上! 因此可以说,过滤掉这些播放量小于50的歌曲,对总体数据不会产生太大影响。

# 筛选出播放量大于50的歌曲
data = data[data.song.isin(temp_song)]

1.2 对.db文件进行处理

  1. 读取数据
  2. 对song_id进行labelencoder
  3. 将新读取的数据与原有data,按照song_id合并
# 读取数据
conn = sqlite3.connect(r'C:\Users\admin\Desktop\如何才能毕业\MyProject\MusicRecommendationSystem\音乐推荐系统数据集\track_metadata.db')
cur = conn.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
cur.fetchall()

# 获得数据的dataframe
track_metadata_df = pd.read_sql(con=conn, sql='select * from songs')
track_metadata_df.head()
# track_metadata_df.describe()
# track_metadata_df.info()
track_idtitlesong_idreleaseartist_idartist_mbidartist_namedurationartist_familiarityartist_hotttnesssyeartrack_7digitalidshs_perfshs_work
0TRMMMYQ128F932D901Silent NightSOQMMHC12AB0180CB8Monster Ballads X-MasARYZTJS1187B98C555357ff05d-848a-44cf-b608-cb34b5701ae5Faster Pussy cat252.055060.6498220.39403220037032331-10
1TRMMMKD128F425225DTanssi vaanSOVFVAK12A8C1350D9KarkuteilläARMVN3U1187FB3A1EB8d7ef530-a6fd-4f8f-b2e2-74aec765e0f9Karkkiautomaatti156.551380.4396040.35699219951514808-10
2TRMMMRX128F93187D9No One Could EverSOGTUKN12AB017F4F1ButterARGEKB01187FB507503d403d44-36ce-465c-ad43-ae877e65adc4Hudson Mohawke138.970980.6436810.43750420066945353-10
3TRMMMCH128F425532CSi Vos QuerésSOBNYVR12A8C13558CDe CuloARNWYLR1187B9B2F9C12be7648-7094-495f-90e6-df4189d68615Yerba Brava145.057510.4485010.37234920032168257-10
4TRMMMWA128F426B589Tangle Of AspensSOHSBXH12A8C13B0DFRene Ablaze Presents Winter SessionsAREQDTE1269FB37231Der Mystic514.298320.0000000.00000002264873-10
type(song_encoder.classes_)
numpy.ndarray
# 对于之前的歌曲编码,我们给一个字典,对歌曲和编码进行一一映射
song_labels = dict(zip(song_encoder.classes_, range(len(song_encoder.classes_))))  # len(song_labels) = 193683

# 对于那些在之前没有出现过的歌曲,我们直接给一个最大的编码
encoder = lambda x: song_labels[x] if x in song_labels.keys() else len(song_labels)

# 对数据进行labelencoder
track_metadata_df['song_id'] = track_metadata_df['song_id'].apply(encoder)
# 对song_id重命名为song
track_metadata_df = track_metadata_df.rename(columns={'song_id': 'song'})
# 根据特征song进行拼接,将拼接后的数据重新命名为data
data = pd.merge(data, track_metadata_df, on='song')
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 907134 entries, 0 to 907133
Data columns (total 16 columns):
 #   Column              Non-Null Count   Dtype  
---  ------              --------------   -----  
 0   user                907134 non-null  int32  
 1   song                907134 non-null  int32  
 2   play_count          907134 non-null  int64  
 3   track_id            907134 non-null  object 
 4   title               907134 non-null  object 
 5   release             907134 non-null  object 
 6   artist_id           907134 non-null  object 
 7   artist_mbid         907134 non-null  object 
 8   artist_name         907134 non-null  object 
 9   duration            907134 non-null  float64
 10  artist_familiarity  907134 non-null  float64
 11  artist_hotttnesss   907134 non-null  float64
 12  year                907134 non-null  int64  
 13  track_7digitalid    907134 non-null  int64  
 14  shs_perf            907134 non-null  int64  
 15  shs_work            907134 non-null  int64  
dtypes: float64(3), int32(2), int64(5), object(6)
memory usage: 110.7+ MB
data.columns
Index(['user', 'song', 'play_count', 'track_id', 'title', 'release',
       'artist_id', 'artist_mbid', 'artist_name', 'duration',
       'artist_familiarity', 'artist_hotttnesss', 'year', 'track_7digitalid',
       'shs_perf', 'shs_work'],
      dtype='object')

为了降低内存,我们同样进行类型转换,

  1. 将int64转换成int32
  2. 将float64转换为float32
data = data.astype({'play_count': 'int32', 'duration': 'float32', 'artist_familiarity': 'float32',
            'artist_hotttnesss': 'float32', 'year': 'int32', 'track_7digitalid': 'int32', 'shs_perf':'int32', 
                   'shs_work': 'int32'})


data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 907134 entries, 0 to 907133
Data columns (total 16 columns):
 #   Column              Non-Null Count   Dtype  
---  ------              --------------   -----  
 0   user                907134 non-null  int32  
 1   song                907134 non-null  int32  
 2   play_count          907134 non-null  int32  
 3   track_id            907134 non-null  object 
 4   title               907134 non-null  object 
 5   release             907134 non-null  object 
 6   artist_id           907134 non-null  object 
 7   artist_mbid         907134 non-null  object 
 8   artist_name         907134 non-null  object 
 9   duration            907134 non-null  float32
 10  artist_familiarity  907134 non-null  float32
 11  artist_hotttnesss   907134 non-null  float32
 12  year                907134 non-null  int32  
 13  track_7digitalid    907134 non-null  int32  
 14  shs_perf            907134 non-null  int32  
 15  shs_work            907134 non-null  int32  
dtypes: float32(3), int32(7), object(6)
memory usage: 83.1+ MB

1.3 数据清洗

包括:

  1. 去重
  2. 丢掉无用信息

实际上,有些信息我们比较肯定是无用的,比如

  • track_id
  • artist_id
  • artist_mbid
  • duration
  • track_7digitalid
  • shs_perf
  • shs_work

我们主要利用评分矩阵进行召回和排序,上面的信息我们应该用不到。

# 去重
data.drop_duplicates(inplace=True)

# 丢掉无用信息
data.drop(['track_id', 'artist_id', 'artist_mbid', 'duration', 'track_7digitalid', 'shs_perf', 'shs_work'], axis=1, inplace=True)
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 907134 entries, 0 to 907133
Data columns (total 9 columns):
 #   Column              Non-Null Count   Dtype  
---  ------              --------------   -----  
 0   user                907134 non-null  int32  
 1   song                907134 non-null  int32  
 2   play_count          907134 non-null  int32  
 3   title               907134 non-null  object 
 4   release             907134 non-null  object 
 5   artist_name         907134 non-null  object 
 6   artist_familiarity  907134 non-null  float32
 7   artist_hotttnesss   907134 non-null  float32
 8   year                907134 non-null  int32  
dtypes: float32(2), int32(4), object(3)
memory usage: 48.4+ MB

1.4 可视化

这里,我们使用刚刚安装WordCloud,直观感受最受欢迎的歌手、专辑和歌曲:

data.head()
usersongplay_counttitlereleaseartist_nameartist_familiarityartist_hotttnesssyear
02999032091The CoveThicker Than WaterJack Johnson0.8320120.6774820
12016032091The CoveThicker Than WaterJack Johnson0.8320120.6774820
21908332093The CoveThicker Than WaterJack Johnson0.8320120.6774820
3586032091The CoveThicker Than WaterJack Johnson0.8320120.6774820
41026232096The CoveThicker Than WaterJack Johnson0.8320120.6774820

1.4.1 每个歌手获得的点击量:

# 字典artist_playcounts记录每个歌手获得的点击量
artist_playcounts = {}
for artist, group in data.groupby('artist_name'):
    artist_playcounts[artist] = group['play_count'].sum()

# 作图
plt.figure(figsize=(12, 8))
wc = WordCloud(width=1000, height=800)
wc.generate_from_frequencies(artist_playcounts)
plt.imshow(wc)
plt.axis('off')
plt.show()

在这里插入图片描述

1.4.2 每个专辑获得的点击量:

# 字典release_playcounts记录每个专辑获得的点击量
release_playcounts = {}
for release, group in data.groupby('release'):
    release_playcounts[release] = group['play_count'].sum()

# 作图
plt.figure(figsize=(12, 8))
wc = WordCloud(width=1000, height=800)
wc.generate_from_frequencies(release_playcounts)
plt.imshow(wc)
plt.axis('off')
plt.show()

在这里插入图片描述

popular_release = data[['release','play_count']].groupby('release').sum().reset_index()
popular_release_top_20 = popular_release.sort_values('play_count', ascending=False).head(n=20)

objects = (list(popular_release_top_20['release']))
y_pos = np.arange(len(objects))
count = list(popular_release_top_20['play_count'])

plt.figure(figsize=(16,8))
plt. bar(y_pos, count, align='center', alpha=0.5)
plt.xticks(y_pos, objects, rotation='vertical', fontsize=12)
plt.ylabel(u'play_count')
plt.title(u'Most Popular Release')

plt.show ()

在这里插入图片描述

1.4.3 每首歌获得的点击量:

# 字典song_playcounts记录每首歌获得的点击量
song_playcounts = {}
for song, group in data.groupby('title'):            # title是歌曲,那song又是什么??--->>之前是加密后的歌曲名,后来编码后成为歌曲id
    song_playcounts[song] = group['play_count'].sum()

# 作图
plt.figure(figsize=(12, 8))
wc = WordCloud(width=1000, height=800)
wc.generate_from_frequencies(song_playcounts)
plt.imshow(wc)
plt.axis('off')
plt.show()

在这里插入图片描述

popular_music = data[['title','play_count']].groupby('title').sum().reset_index()
popular_music_top_20 = popular_music.sort_values('play_count', ascending=False).head(n=20)
popular_music_top_20
titleplay_count
16462You're The One22430
15138Undo18610
11241Revelry17065
5991Horn Concerto No. 4 in E flat K495: II. Romanc...12932
11819Sehr kosmisch11147
3456Dog Days Are Over (Radio Edit)10784
2198Canada9216
15225Use Somebody9104
11798Secrets8814
6988Invalid8802
434Ain't Misbehavin8086
12526Somebody To Love8069
2303Catch You Baby (Steve Pitron & Max Sanna Radio...7768
11203Représente7751
12223Sincerité Et Jalousie7140
16256Yellow6579
5808Hey_ Soul Sister6002
1902Bring Me To Life5772
13806The Gift5694
8412Love Story5622

二、不同的推荐引擎

对于系统的召回阶段,我们将给出如下三种推荐方式,分别是

  1. 基于热度的推荐
  2. 基于协同过滤的推荐
  3. 基于矩阵分解的推荐

2.1 基于热度的推荐

我们将每首歌听过的人数作为每首歌的打分。这里之所以不将点击量作为打分,是因为一个人可能对一首歌多次点击,但这首歌其他人并不喜欢。

# 基于排行榜的推荐
def recommendation_basedonPopularity(df, N=5):
    my_df = df.copy()
    
    # 字典song_peopleplay,记录每首歌听过的人数
    song_peopleplay = {}
    for song, group in my_df.groupby('title'):
        song_peopleplay[song] = group['user'].count()
    
    # 根据人数从大到小排序,并推荐前N首歌
    sorted_dict = sorted(song_peopleplay.items(), key=lambda x: x[1], reverse=True)[:N]
    # 取出歌曲
#     return list(dict(sorted_dict).keys())
    return sorted_dict



# 测试推荐结果
recommendation_basedonPopularity(data, N=5)
[('Use Somebody', 2774),
 ('Sehr kosmisch', 2438),
 ('Dog Days Are Over (Radio Edit)', 2227),
 ('Yellow', 2107),
 ('Undo', 2052)]

2.2 基于协同过滤的推荐

协同过滤需要用户-物品评分矩阵。
这里,用户对某首歌的评分的计算公式如下,

  • 该用户的最大歌曲点击量:User_Max_playCount
  • 当前歌曲点击量/最大歌曲点击量:x = CurSong_playCount / User_Max_playCount
  • 评分为log(2 + x)

得到用户-物品评分矩阵之后,我们用surprise库中的knnbasic函数进行协同过滤。

# 每个用户点击量的平均数
user_averageScore = {}
for user, group in data.groupby('user'):
    user_averageScore[user] = group['play_count'].mean()
#     user_averageScore[user] = group['play_count'].max()

data['rating'] = data.apply(lambda x: np.log(2 + x.play_count / user_averageScore[x.user]), axis=1)
sns.displot(data['rating'].values, bins=100)
plt.show()

在这里插入图片描述

# 得到用户-音乐-评分矩阵
user_item_rating = data[['user', 'song', 'rating']]
user_item_rating = user_item_rating.rename(columns={'song': 'item'})
user_item_rating
useritemrating
02999032090.969401
12016032090.937644
21908332091.018099
3586032090.871477
41026232091.188672
............
907129198901729342.596228
907130133311297202.068112
90713126899899271.877162
90713219366744501.886947
90713316949408512.057780

907134 rows × 3 columns

2.2.1 itemCF推荐
  • 首先,我们做itemCF的推荐。

ItemCF(Item-based Collaborative Filtering)是一种协同过滤推荐算法,它基于物品(item)之间的相似性来进行推荐。使用ItemCF算法进行推荐的一般步骤为:构建用户-物品评分矩阵–>>计算物品相似度–>>为目标用户生成推荐列表–>>过滤和排序.

ItemCF算法更适用于解决物品冷启动问题(用户对物品评分数据稀疏)和推荐长尾物品的场景。

###################### itemCF ######################

# 阅读器
reader = Reader(line_format='user item rating', sep=',')

# 载入数据
raw_data = Dataset.load_from_df(user_item_rating, reader=reader)

# 分割数据集
kf = KFold(n_splits=5)   # k折交叉验证法,其中每个折都被用作验证集一次,而其余 k-1 个折则作为训练集。

# 构建模型
knn_itemcf = KNNBasic(k=40, sim_options={'user_based': False})   # 基于K最近邻算法的协同过滤推荐算法。

# 训练数据集,并返回rmse误差
for trainset, testset in kf.split(raw_data):
    knn_itemcf.fit(trainset)
    predictions = knn_itemcf.test(testset)
    accuracy.rmse(predictions, verbose=True)

Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2753
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2747
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2756
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2774
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2773
# 用户听过的歌曲集合
user_songs = {}
for user, group in user_item_rating.groupby('user'):
    user_songs[user] = group['item'].values.tolist()   # 获取分组后每个用户对应的'item'列。values表示将该列的值转换为numpy数组。tolist()表示将numpy数组转换为Python列表。

# 歌曲集合
songs = user_item_rating['item'].unique().tolist()

# 实现歌曲id和歌曲名称对应关系的映射
songID_titles = {}
for index in data.index:
    songID_titles[data.loc[index, 'song']] = data.loc[index, 'title']   # data.loc[index, 'song']获取到歌曲id
# itemCF 推荐
def recommendation_basedonItemCF(userID, N=5):
    # 用户听过的音乐列表
    used_items = user_songs[userID]
    
    # 用户对未听过音乐的评分
    item_ratings = {}
    for item in songs:
        if item not in used_items:
            item_ratings[item] = knn_itemcf.predict(userID, item).est   # .est表示获取评分的属性值
    
    # 找出评分靠前的N首歌曲
    song_ids = dict(sorted(item_ratings.items(), key=lambda x: x[1], reverse=True)[:N])
    song_topN = [songID_titles[s] for s in song_ids.keys()]
    
    return song_topN

recommendation_basedonItemCF(5860)
['War On Machines',
 'Downside',
 'Endless Summer',
 'Data Blast',
 'Blues (2005 Digital Remaster)']
2.2.2 userCF推荐
  • 我们试一下userCF的推荐。

基于用户协同过滤(User-based Collaborative Filtering)推荐,可以按照以下步骤:

  1. 计算用户之间的相似度(余弦相似度、皮尔逊相关系数等),

  2. 选择邻居用户,

  3. 预测用户对未听过的音乐的评分,

  4. 根据预测评分生成推荐列表。

###################### userCF ######################

# 阅读器
reader = Reader(line_format='user item rating', sep=',')

# 载入数据
raw_data = Dataset.load_from_df(user_item_rating, reader=reader)

# 分割数据集
kf = KFold(n_splits=5)

# 构建模型
knn_usercf = KNNBasic(k=40, sim_options={'user_based': True})  ## 当'user_based'为True时,表示使用基于用户的协同过滤算法。

# 训练数据集,并返回rmse误差
for trainset, testset in kf.split(raw_data):
    knn_usercf.fit(trainset)
    predictions = knn_usercf.test(testset)
    accuracy.rmse(predictions, verbose=True)
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2721
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2724
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2724
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2737
Computing the msd similarity matrix...
Done computing similarity matrix.
RMSE: 0.2721
# userCF 推荐
def recommendation_basedonUserCF(userID, N=10):
    # 用户听过的音乐列表
    used_items = user_songs[userID]
    
    # 用户对未听过音乐的评分
    item_ratings = {}
    for item in songs:
        if item not in used_items:
            item_ratings[item] = knn_usercf.predict(userID, item).est
    
    # 找出评分靠前的5首歌曲
    song_ids = dict(sorted(item_ratings.items(), key=lambda x: x[1], reverse=True)[:N])
    song_topN = [songID_titles[s] for s in song_ids.keys()]
    
    return song_topN

recommendation_basedonUserCF(5860, 5)
['In the Summertime',
 'A Whiter Shade Of Pale',
 'Invocation (LP Version)',
 'Point Of No Return (1995 Digital Remaster)',
 'Sitting on Top of the World']

2.3 基于矩阵分解的推荐

矩阵分解同样需要用户-物品评分矩阵。我们依然沿用上面的评分矩阵进行预测。同样的,我们用surprise库里面的SVD来进行矩阵分解方法。但在实际中,可能更适合使用迭代的方式求解。

# 矩阵分解(SVD)

# 阅读器
reader = Reader(line_format='user item rating', sep=',')

# 载入数据
raw_data = Dataset.load_from_df(user_item_rating, reader=reader)

# 分割数据集
kf = KFold(n_splits=5)

# 构建模型
algo = SVD(n_factors=40, biased=True)

# 训练数据集,并返回rmse误差
for trainset, testset in kf.split(raw_data):
    algo.fit(trainset)
    predictions = algo.test(testset)
    accuracy.rmse(predictions, verbose=True)
RMSE: 0.2738
RMSE: 0.2757
RMSE: 0.2750
RMSE: 0.2732
RMSE: 0.2757
# 矩阵分解 推荐
def recommendation_basedonMF(userID, N=10):
    # 用户听过的音乐列表
    used_items = user_songs[userID]
    
    # 用户对未听过音乐的评分
    item_ratings = {}
    for item in songs:
        if item not in used_items:
            item_ratings[item] = algo.predict(userID, item).est
    
    # 找出评分靠前的5首歌曲
    song_ids = dict(sorted(item_ratings.items(), key=lambda x: x[1], reverse=True)[:N])
#     song_topN = [songID_titles[s] for s in song_ids.keys()]
    
    song_topN = {}
    for key, value in song_ids.items():
        song_topN[songID_titles[key]] = value
    
    return song_ids

recommendation_basedonMF(5860, 5)
{53372: 1.545875939577059,
 164248: 1.5340108259640375,
 76614: 1.5126193583435195,
 608: 1.5059726797011346,
 48325: 1.4792192139226963}

三、推荐系统的排序

对于系统的排序阶段,我们通常是这样的,

  • 以召回阶段的输出作为输入
  • 用CTR(Click-Through Rate,CTR = 点击次数 / 展示次数)预估作为进一步的排序标准

这里,我们可以召回50首音乐,用GBDT+LR对这些音乐做CTR预估,给出评分排序,选出5首歌曲。

现在,仅仅用用户-物品评分是不够的,因为我们需要考虑特征之间的组合。为此,我们用之前的data数据。

这里的数据处理思路是,

  • 复制一份新的数据,命名为new_data
  • 去掉title列,因为它不需要参与特征组合
  • 对其余object列进行labelencoder编码
  • 根据rating列数值情况,为了样本的正负均衡,我们令rating小于0.7的为0,也就是不喜欢,令rating大于0.7的为1,也就是喜欢
  • 将new_data按照0.5的比例分成两份,一份给gbdt作为训练集,一份给lr作为训练集
# 复制原data数据
rank_data = data.copy()

# 去掉无用的title列
rank_data.drop('title', axis=1, inplace=True)

# 将object类型数据用labelencoder编码
release_encoder = LabelEncoder()
rank_data['release'] = release_encoder.fit_transform(rank_data['release'].values)

artist_name_encoder = LabelEncoder()
rank_data['artist_name'] = artist_name_encoder.fit_transform(rank_data['artist_name'].values)

# 根据rating的取值,更新rating值
rank_data['rating'] = rank_data['rating'].apply(lambda x: 0 if x < 0.7 else 1)
rank_data.head(10)
usersongplay_countreleaseartist_nameartist_familiarityartist_hotttnesssyearrating
02999032091874025560.8320120.67748201
12016032091874025560.8320120.67748201
21908332093874025560.8320120.67748201
3586032091874025560.8320120.67748201
41026232096874025560.8320120.67748201
51981232096874025560.8320120.67748201
62946532092874025560.8320120.67748201
73098332091874025560.8320120.67748201
81806132092874025560.8320120.67748201
92870932091874025560.8320120.67748201

3.1 GBDT+LR预估

这里,我们做一个ctr点击预估(由于没有实际点击数据,此处用GBDT+LR预测输出概率值),将点击概率作为权重与rating结合作为最终的评分。
为了做这个,我们需要

  • 分割数据集,一部分作为GBDT的训练集,一部分作为LR的训练集
  • 先训练GBDT,将其结果作为输入,送进LR里面,再生成结果
  • 最后看AUC指标

为了加快训练速度,我们从new_data的90多万条数据中,取出20万条作为训练数据。

# 取出20%的数据作为数据集
small_data = rank_data.sample(frac=0.2)

# 将数据集分成gbdt训练街和lr训练集
X_gbdt, X_lr, y_gbdt, y_lr = train_test_split(small_data.iloc[:, :-1].values, small_data.iloc[:, -1].values, test_size=0.5)
print(X_gbdt.shape)
print(small_data.shape)
(90713, 8)
(181427, 9)
depth = 3
n_estimator = 50

print('当前n_estimators=', n_estimator)

# 训练gbdt
gbdt = GradientBoostingClassifier(n_estimators=n_estimator, max_depth=depth, min_samples_split=3, min_samples_leaf=2)
gbdt.fit(X_gbdt, y_gbdt)

print('当前gbdt训练完成!')

# one-hot编码
onehot = OneHotEncoder()
onehot.fit(gbdt.apply(X_gbdt).reshape(-1, n_estimator))

# 对gbdt结果进行one-hot编码,然后训练lr
lr = LogisticRegression()
lr.fit(onehot.transform(gbdt.apply(X_lr).reshape(-1, n_estimator)), y_lr)

print('当前lr训练完成!')

# 用AUC作为指标
lr_pred = lr.predict(onehot.transform(gbdt.apply(X_lr).reshape(-1, n_estimator)))
auc_score = roc_auc_score(y_lr, lr_pred)

print('当前n_estimators和auc分别为', n_estimator, auc_score)
当前n_estimators= 50
当前gbdt训练完成!
当前lr训练完成!
当前n_estimators和auc分别为 50 0.5

3.2 排序

这里,我们通过svd召回50首歌,然后根据gbdt+lr的结果做权重,给它们做排序,选出其中的5首歌作为推荐结果。

rank_data[rank_data.song==608].values[0]
array([8.77000000e+03, 6.08000000e+02, 9.50000000e+01, 7.34600000e+03,
       6.31400000e+03, 7.09323347e-01, 4.50977266e-01, 1.99800000e+03,
       1.00000000e+00])
# 推荐
def recommendation(userID):
    # 召回50首歌
    recall = recommendation_basedonMF(userID, 50)
#     print(recall)
    
    print('召回完毕!')
    
    # 根据召回的歌曲信息,写出特征向量
    feature_lines = []
    for song in recall.keys():
        feature = rank_data[rank_data.song==song].values[0]
        
        # 丢掉最后一列rating,将user数值改成当前userID
        feature = feature[:-1]
        feature[0] = userID
        
        # 放入特征向量中
        feature_lines.append(feature)
    
    # 用gbdt+lr计算权重
    weights = lr.predict_proba(onehot.transform(gbdt.apply(feature_lines).reshape(-1, n_estimator)))[:, 1]
    
#     print(weights.shape)   # (50,)
    print('排序权重计算完毕!')
    
    # 计算最终得分
    score = {}
    i = 0
    for song in recall.keys():
        score[song] = recall[song] * weights[i]   # recall[song]是用户对该song的评分
        i += 1
    
#     print(score)
    
    # 选出排名前5的歌曲id
    song_ids = dict(sorted(score.items(), key=lambda x: x[1], reverse=True)[: 5])
    
    # 前5歌曲名称
    song_topN = [songID_titles[s] for s in song_ids.keys()]
    
    return song_topN


# 测试
print('最终推荐列表为:')
recommendation(29990)
最终推荐列表为:
召回完毕!
排序权重计算完毕!





['221',
 'Working With Homesick',
 'In League With Satan',
 'Sheena Is A Punk Rocker',
 'Tomber La Chemise']

最后:本项目基于牛客网视频和博客:https://blog.csdn.net/qq_30841655/article/details/107989560,只用于个人学习。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值