如何建立一个音乐推荐引擎

423 阅读10分钟

构建一个音乐推荐引擎

有没有想过Spotify是如何根据用户当前的播放列表来预测新的播放列表的,或者YouTube是如何预测用户接下来可能想看的视频的。这是不是很神奇?

这是因为基于机器学习的推荐系统发生的。推荐系统是一个基于机器学习的模型,帮助用户根据用户过去的偏好发现新产品或服务。

推荐系统已经成为数字世界中的一个重要元素,用户可能会因为大量的数据而迷惑自己。因此,这一功能可以帮助用户挑选完全符合他们偏好的产品或服务。

读者应该有Python库的基本知识,如[pandas]、[numpy]、[scikit-learn],以及矢量代数的基础知识。

读者还需要阅读[Spotipy]库的文档。Spotipy是一个轻量级的Python库,我们将用它来访问[Spotify的网络API]。

主要收获

在本博客结束时,你将知道以下内容。

  • 推荐系统如何工作。
  • 推荐系统的类型。
  • 推荐系统背后的数学。
  • 使用 Python 从头开始实现一个 Spotify 播放列表推荐引擎。

简介

什么是推荐系统?

推荐系统的目的是预测用户的选择并推荐可能感兴趣的产品或服务。

这些系统能够做到这一点是因为有用户数据。推荐系统的功能主要取决于两类信息。

  1. 特征信息。定义产品(标签、类别等)或用户(偏好、简介等)特征的信息。
  2. 用户-物品的互动。定义用户-项目关系的信息(评级、喜欢/不喜欢,等等)。

在此基础上,我们可以将推荐系统中使用的算法分为两大类。

推荐系统的类型

  • 基于内容的过滤
  • 协作式过滤

基于内容的过滤

基于内容的过滤系统使用特征信息,根据用户过去的行为或明确的反馈向其推荐新的项目/产品。为了进一步解释它,我们将以一个简单的Spotify歌曲推荐器为例。

下图显示了一个特征矩阵,每一行代表一首歌,每一列代表一个特征。比方说,歌曲的特征包括流派,矩阵是二进制的,非零值代表该歌曲有该特征存在。

用户也将在同样的特征空间中被表示。一些与用户相关的特征将由用户明确提供,而其他特征则是基于特征信息的隐含特征。

feature-matrix

现在,这个模型应该推荐用户可能觉得有趣的歌曲。首先,我们要挑选一个相似度指标,比如余弦相似度。然后,模型必须根据这个相似度指标对每个候选人进行评分,然后模型将根据这个分数进行推荐。

分数越高,用户找到他/她感兴趣的那首歌的概率就越大。本博客将使用余弦相似性作为相似性指标来实现Spotify播放列表推荐器。但是,首先,让我们从数学上理解它。

余弦相似度

余弦相似性是n维特征空间中两个n维向量之间角度的余弦。它是两个向量的点积除以两个向量的大小之积。

equation

为了更好地理解它,让我们举一个例子:两个项目,项目1和项目2,以及两个特征x_1x\_1x_2x\_2,它们定义了一个项目。下面的图表示项目1和项目2在特征空间的向量。

cosine-similarity

向量之间的角度越小,余弦相似度越高。

协作式过滤

协作过滤系统使用用户与项目的互动来产生推荐。这意味着协作过滤同时使用用户和项目之间的相似性来提供建议。让我们参考下面的图表。

collaborative-filtering

正如我们在图中看到的,有两个用户,用户A和用户B。这两个用户对音乐的品味相似,因为他们都喜欢歌曲-1和歌曲-2,但有一首用户A喜欢的歌曲-3,但用户B从未听过。系统将根据这些用户-项目的互动关系向用户A推荐歌曲-3。

正如在这篇博客中,我们将实现的推荐系统是基于内容过滤的。因此,我们将把对协作过滤系统的讨论限制在它的定义上。

如何使用Spotify Web API来获取数据

在这篇博客中,我们将实现一个自定义的播放列表推荐器,为此我们将需要我们当前的Spotify播放列表数据,我们将产生进一步的推荐。

为此,Spotify提供了一个网络API来创建一个自定义应用程序并获取Spotify数据。Spotify Web API端点基于简单的REST原则,直接从Spotify数据目录返回关于音乐艺术家、专辑和曲目的JSON元数据。

api

使用Spotify Web API的简单步骤

打开Spotify Web API的仪表板。使用您的Spotify账户凭证登录。登录后,您的主页将看起来像这样。

dashboard

现在点击创建一个应用程序。这将产生一个独特的客户ID和密码,这将被进一步使用。让我们打开该应用程序。

application-page

进入编辑设置选项,添加你的本地主机URL。它将被用于以后的用户认证。现在我们已经准备好实现一个基于Spotify的播放列表推荐系统。

实施

为了访问Spotify Web API,我们将使用一个基于python的库,称为Spotipy,所以让我们首先安装这个库。

pip install spotipy

现在导入以下库

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
from spotipy.oauth2 import SpotifyOAuth
import spotipy.util as util

from skimage import io
import matplotlib.pyplot as plt
import pandas as pd
from datetime import datetime


from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics.pairwise import cosine_similarity

在这个实现中,我们将需要与Spotify应用程序中存在的歌曲特征有关的额外数据。利用这些特征,我们将确定我们的播放列表和不在我们播放列表中的歌曲之间的相似性。根据相似性,我们将得到一个新的播放列表推荐。

为了这个目的,我使用了Kaggle数据集。

spotify_data = pd.read_csv('data\SpotifyFeatures.csv')
spotify_data.head()

snapshot

特征工程

在数据集中,我们可以观察到多列代表一首歌曲的可能特征。其中,少数特征是分类的(具有离散值的列),如流派、关键、流行指数等。

因此,第一步是将这些分类特征转换成单次编码(OHD),这样我们的歌曲就可以在特征空间中被表示为矢量。

为了简单起见,现在我们只考虑两个分类特征。

spotify_features_df = spotify_data
genre_OHE = pd.get_dummies(spotify_features_df.genre)
key_OHE = pd.get_dummies(spotify_features_df.key)

由于我们可以看到数字列有不同的范围,我们将进行最大-最小归一化,将数据集中的数字列的值改为标准尺度。

这是最常见的归一化方法,特征列中的最小值被转换为0,而特征列中的最大值被转换为1。

最大-最小归一化的公式如下。

equation

scaled_features = MinMaxScaler().fit_transform([
  spotify_features_df['acousticness'].values,
  spotify_features_df['danceability'].values,
  spotify_features_df['duration_ms'].values,
  spotify_features_df['energy'].values,
  spotify_features_df['instrumentalness'].values,
  spotify_features_df['liveness'].values,
  spotify_features_df['loudness'].values,
  spotify_features_df['speechiness'].values,
  spotify_features_df['tempo'].values,
  spotify_features_df['valence'].values,
  ])
#Storing the transformed column vectors into our dataframe
spotify_features_df[['acousticness','danceability','duration_ms','energy','instrumentalness','liveness','loudness','speechiness','tempo','valence']] = scaled_features.T

我们放弃那些不考虑确定相似性的特征和已经转化为OHE向量的分类特征。

#discarding the categorical and unnecessary features 
spotify_features_df = spotify_features_df.drop('genre',axis = 1)
spotify_features_df = spotify_features_df.drop('artist_name', axis = 1)
spotify_features_df = spotify_features_df.drop('track_name', axis = 1)
spotify_features_df = spotify_features_df.drop('popularity',axis = 1)
spotify_features_df = spotify_features_df.drop('key', axis = 1)
spotify_features_df = spotify_features_df.drop('mode', axis = 1)
spotify_features_df = spotify_features_df.drop('time_signature', axis = 1)
#Appending the OHE columns of the categorical features
spotify_features_df = spotify_features_df.join(genre_OHE)
spotify_features_df = spotify_features_df.join(key_OHE)
spotify_features_df.head()

snapshot

连接到Spotify Web API

在下一步,我将获取我的Spotify播放列表数据。要连接到Spotify Web API,你需要一个独特的客户端ID和一个客户秘密密钥,我已经向你展示了如何生成。

client_id = #write yours 
client_secret= #write yours

在下面的单元格中,你可以看到我使用了我的localhost URL。这个URL是用来验证客户端的。我已经在Spotify Web API部分谈到了这一点。在这里,我们将播放列表的细节存储在一个python字典中。

#Fetching the playlist
scope = 'user-library-read'
token = util.prompt_for_user_token(
    scope, 
    client_id= client_id, 
    client_secret=client_secret, 
    redirect_uri='http://localhost:8881/callback'
  )
sp = spotipy.Spotify(auth=token)
playlist_dic = {}
playlist_cover_art = {}

for i in sp.current_user_playlists()['items']:
    playlist_dic[i['name']] = i['uri'].split(':')[2]
    playlist_cover_art[i['uri'].split(':')[2]] = i['images'][0]['url']

print(playlist_dic)

snapshot

下面的单元格包含了使用我们从Kaggle下载的Spotify歌曲特征数据集为我们的播放列表创建一个新的数据框架的方法。

#creating the playlist dataframe with extended features using Spotify data
def generate_playlist_df(playlist_name, playlist_dic, spotify_data):
    
    playlist = pd.DataFrame()

    for i, j in enumerate(sp.playlist(playlist_dic[playlist_name])['tracks']['items']):
        playlist.loc[i, 'artist'] = j['track']['artists'][0]['name']
        playlist.loc[i, 'track_name'] = j['track']['name']
        playlist.loc[i, 'track_id'] = j['track']['id']
        playlist.loc[i, 'url'] = j['track']['album']['images'][1]['url']
        playlist.loc[i, 'date_added'] = j['added_at']

    playlist['date_added'] = pd.to_datetime(playlist['date_added'])  
    
    playlist = playlist[playlist['track_id'].isin(spotify_data['track_id'].values)].sort_values('date_added',ascending = False)

    return playlist
playlist_df = generate_playlist_df('MixedMood', playlist_dic, spotify_data) 
playlist_df.head()

snapshot

下面的单元格在Matplotlib库的帮助下,用于可视化歌曲曲目的封面图。

from skimage import io
import matplotlib.pyplot as plt

def visualize_cover_art(playlist_df):
    temp = playlist_df['url'].values
    plt.figure(figsize=(15,int(0.625 * len(temp))) , facecolor='#8cfc03')
    columns = 5
    
    for i, url in enumerate(temp):
        plt.subplot(len(temp) / columns + 1, columns, i + 1)

        image = io.imread(url)
        plt.imshow(image)
        plt.xticks([])
        plt.yticks([])
        s='' 
        plt.xlabel(s.join(playlist_df['track_name'].values[i].split(' ')[:4]), fontsize = 10, fontweight='bold')
        plt.tight_layout(h_pad=0.8, w_pad=0)
        plt.subplots_adjust(wspace=None, hspace=None)

    plt.show()
visualize_cover_art(playlist_df)

coverart-playlist

创建播放列表向量

为了在我们的播放列表和不在我们播放列表中的歌曲之间进行余弦相似度,我们将尝试把我们的播放列表总结为一个向量。这个向量将在特征空间中代表我们的播放列表,我们将能够找到与我们播放列表中的歌曲相似的歌曲。

下面的单元格包含了将我们的播放列表作为一个单一向量返回的方法,以及将所有不在我们的播放列表中的歌曲放在一个数据框中。

def generate_playlist_vector(spotify_features, playlist_df, weight_factor):
    
    spotify_features_playlist = spotify_features[spotify_features['track_id'].isin(playlist_df['track_id'].values)]
    spotify_features_playlist = spotify_features_playlist.merge(playlist_df[['track_id','date_added']], on = 'track_id', how = 'inner')
    
    spotify_features_nonplaylist = spotify_features[~spotify_features['track_id'].isin(playlist_df['track_id'].values)]
    
    playlist_feature_set = spotify_features_playlist.sort_values('date_added',ascending=False)
    
    
    most_recent_date = playlist_feature_set.iloc[0,-1]
    
    for ix, row in playlist_feature_set.iterrows():
        playlist_feature_set.loc[ix,'days_from_recent'] = int((most_recent_date.to_pydatetime() - row.iloc[-1].to_pydatetime()).days)
        
    
    playlist_feature_set['weight'] = playlist_feature_set['days_from_recent'].apply(lambda x: weight_factor ** (-x))
    
    playlist_feature_set_weighted = playlist_feature_set.copy()
    
    playlist_feature_set_weighted.update(playlist_feature_set_weighted.iloc[:,:-3].mul(playlist_feature_set_weighted.weight.astype(int),0))   
    
    playlist_feature_set_weighted_final = playlist_feature_set_weighted.iloc[:, :-3]
    

    
    return playlist_feature_set_weighted_final.sum(axis = 0), spotify_features_nonplaylist
playlist_vector, nonplaylist_df = generate_playlist_vector(spotify_features_df, playlist_df, 1.2)
print(playlist_vector.shape)
print(nonplaylist_df.head())

snapshot

生成推荐

现在是我们实现的最后一部分,生成推荐。如前所述,我们将使用余弦相似度作为一种相似度量,以确定与我们的播放列表非常相似的歌曲。

我们将在我们的播放列表向量和不在我们播放列表中的歌曲之间进行余弦相似性。

然后,我们将使用基于Python的库Scikit进行余弦相似度,并将余弦相似度值存储在一个单独的列中。

接下来,我们将根据余弦相似度列对数据框架进行反向排序。最后,我们将生成前15首推荐歌曲作为我们的推荐播放列表。

def generate_recommendation(spotify_data, playlist_vector, nonplaylist_df):

    non_playlist = spotify_data[spotify_data['track_id'].isin(nonplaylist_df['track_id'].values)]
    non_playlist['sim'] = cosine_similarity(nonplaylist_df.drop(['track_id'], axis = 1).values, playlist_vector.drop(labels = 'track_id').values.reshape(1, -1))[:,0]
    non_playlist_top15 = non_playlist.sort_values('sim',ascending = False).head(15)
    non_playlist_top15['url'] = non_playlist_top15['track_id'].apply(lambda x: sp.track(x)['album']['images'][1]['url'])
    
    return  non_playlist_top15
top15 = generate_recommendation(spotify_data, playlist_vector, nonplaylist_df)  
top15.head()

snapshot

#Visulaizing the cover-art of the recommended playlist
visualize_cover_art(top10)

snapshot

我已经把这个实现的笔记本文件上传到我的GitHub账户。你可以从这里访问它。

需要思考的几个问题

  • 需要注意的一点是,在我们的案例中,我们已经实现了基于内容的过滤机制。因此,该模型将只能根据该特定用户的兴趣来进行推荐。因此,它限制了用户扩展其现有兴趣的能力。
  • 我们用来开发推荐系统的Spotify歌曲特征数据不一定包含你播放列表中的所有歌曲。因此,有时我们将无法完全驾驭播放列表来产生推荐。
  • 要使用这个推荐器,用户应该在他们的Spotify账户上至少有一个播放列表,这对于一个完全是Spotify应用程序的新用户来说是一个缺点。

结论

使用余弦相似度的概念,我们看到我们能够生成与我们播放列表中的歌曲相似的歌曲推荐。因此,你也可以按照上面的步骤来开发你的自定义Spotify播放列表推荐器。