人是會變的,今天她喜歡聽後朋,明天可能喜歡別的

搖滾樂通過幾十年的發展,風格流派衆多,從blues,到brit invasion,以後是punk,disco,indie rock等等。發展歷程大體是這樣的:html

history of rock

搖滾樂的聽衆,老是能體會到發現寶藏的快樂,可能忽然就會邂逅某支本身未曾接觸過的歌曲、樂隊、風格,感受好聽得不行,之前怎麼歷來不知道,接下來的一段時間便會沉浸於此,天天都在聽該風格的主要樂隊和專輯。用戶收聽音樂在一段時間內多是有着某個「主題」的,這個主題多是地理上的(俄羅斯的搖滾樂隊),多是時間上的(2000年後優秀的專輯),還多是某流派、甚至是都被某影視做品用做BGM。以前不多聽國內搖滾的筆者,在去年聽了刺蝟、P.K.1四、重塑雕像的權利、新褲子、海朋森等一些國內樂隊的不少做品後,才知道原來在老崔、竇惟、萬青、老謝以外還有這麼多優秀的國產搖滾樂。python

這種「在某一時間會被用戶放到一塊兒聽」的co-occurrence歌曲列表在音樂軟件裏的形態是playlist或radio,由editor或用戶編輯生成,固然,還有」專輯「這個很強的聯繫,特別是像《The Dark Side of the Moon》這樣的專輯。然而在前幾篇文章提到的內容中,最爲核心的數據結構是用戶物品關係矩陣,這裏面並無包含」一段時間「這個信息。這段時間能夠稱爲session,在其餘領域的實際應用中,這個session多是一篇研究石墨烯的論文,多是一個Airbnb用戶某天在30分鐘內尋找夏威夷租房信息的點擊狀況。把session內的co-occurrence關係考慮進去,能夠爲用戶作出更符合其當下所處情境的推薦結果。git

這篇文章使用Word2vec處理Last.fm 1K數據集,來完成這種歸入session信息的歌曲co-occurrence關係的創建。github

Word2vec與音樂推薦

Word2vec最初被提出是爲了在天然語言處理(NLP)中用一個低維稠密向量來表示一個word(該向量稱爲embedding),並進一步根據embedding來研究詞語之間的關係。它使用一個僅包含一層隱藏層的神經網絡來訓練被分紅許多句子的數據,來學習詞彙之間的co-occurrence關係,其中訓練時分爲CBOW(Continuous Bag-of-Words)與Skip-gram兩種方式,這裏簡單說一下使用Skip-gram獲取embedding的過程。算法

假設拿到了一些句子做爲數據集,要爲該神經網絡生成訓練樣本,這裏要定義一個窗口大小好比爲2,則對"shine on you crazy diamond"這句話來說,將窗口從左滑到右,按照下圖方式生成一系列單詞對兒,其中每一個單詞對兒即做爲一個訓練樣本,單詞對兒中的第一個單詞爲輸入,第二個單詞爲label。網絡

假設語料庫中有10000個互不相同的word,首先將某個單詞使用one-hot vector(10000維)來表示輸入神經網絡,輸出一樣爲10000維的vector,每一維上的數字表明此位置爲1所表明的one-hot vector所對應的word在輸入word周圍的可能性:session

輸入輸出層的節點數爲語料庫word數,隱藏層的節點數則爲表示每一個單詞的向量的維數。此模型每一個輸入層節點會與隱藏層的每一個節點相連且都對應了一個權重,而對某輸入節點來講,它與隱藏層相連的全部這些權重組成的向量即爲該節點爲1所表明的one-hot vector所對應的單詞的embedding向量。數據結構

但該模型其實並不瞭解語義,它擁有的只是統計學知識,那麼既然能夠根據one-hot vector來標識一個word,固然能夠用這種形式來標識一首歌曲,一支樂隊,一件商品,一間出租屋等等任何能夠被推薦的東西,再把這些數據餵給模型,一樣的訓練過程,即可以獲取到各類物品embedding,而後研究它們之間的關係,此謂Item2vec,萬物皆可embedding。app

故Word2vec與音樂推薦的關係就是,把一個歌單或者某個user在一個下午連續收聽的歌曲看成一句話(session),把每首歌看成一個獨立的word,而後把這樣的數據交給此模型去訓練便可獲取每首歌的embedding向量,這裏從歌單到一句話的抽象,即實現了上文中提到的考慮進去「一段時間」這個點。機器學習

加載數據

該數據集包含了1K用戶對960K歌曲的收聽狀況,文件1915萬行,2.4G,每行記錄了某用戶在某時間播放了某歌曲的信息。依然是用pandas把數據加載進來,此次須要timestamp的信息。

import arrow
from tqdm import tqdm
import pandas as pd
import numpy as np
from scipy.sparse import csr_matrix, diags


df = pd.read_csv('~/music-recommend/dataset/lastfm-dataset-1K/userid-timestamp-artid-artname-traid-traname.tsv', 
            sep = '\t',
            header = None,                   
            names = ['user_id', 'timestamp', 'artist_id', 'artist_name', 'track_id', 'track_name'],
            usecols = ['user_id', 'timestamp', 'track_id', 'artist_name', 'track_name'],
           )
df = df.dropna()
print (df.info())
複製代碼
<class 'pandas.core.frame.DataFrame'>
Int64Index: 16936136 entries, 10 to 19098861
Data columns (total 5 columns):
user_id        object
timestamp      object
artist_name    object
track_id       object
track_name     object
dtypes: object(5)
memory usage: 775.3+ MB
複製代碼

接下來作一些輔助的數據,爲每一個user、每首track都生成一個用於標識本身的index,創建從index到id,從id到index的雙向查詢dict。

df['user_id'] = df['user_id'].astype('category')
df['track_id'] = df['track_id'].astype('category')

user_index_to_user_id_dict = df['user_id'].cat.categories # use it like a dict.
user_id_to_user_index_dict = dict()
for index, i in enumerate(df['user_id'].cat.categories):
    user_id_to_user_index_dict[i] = index
    
track_index_to_track_id_dict = df['track_id'].cat.categories # use it like a dict.
track_id_to_track_index_dict = dict()
for index, i in enumerate(df['track_id'].cat.categories):
    track_id_to_track_index_dict[i] = index
    
song_info_df = df[['artist_name', 'track_name', 'track_id']].drop_duplicates()
複製代碼

考慮到專輯翻唱、同名、專輯從新發行等狀況,須要用track_id來做爲一首歌的惟一標識,而當須要經過artist_nametrack_name來定位到一首歌時,這裏寫了一個函數,採起的策略是找到被播放最多的那一個。

def get_hot_track_id_by_artist_name_and_track_name(artist_name, track_name):
    track = song_info_df[(song_info_df['artist_name'] == artist_name) & (song_info_df['track_name'] == track_name)]
    max_listened = 0
    hotest_row_index = 0
    for i in range(track.shape[0]):
        row = track.iloc[i]
        track_id = row['track_id']
        listened_count = df[df['track_id'] == track_id].shape[0]
        if listened_count > max_listened:
            max_listened = listened_count
            hotest_row_index = i
    return track.iloc[hotest_row_index]['track_id']
複製代碼
print ('wish you were here tracks:')
print (song_info_df[(song_info_df['artist_name'] == 'Pink Floyd') & (song_info_df['track_name'] == 'Wish You Were Here')][['track_id']])
print ('--------')
print ('hotest one:')
print (get_hot_track_id_by_artist_name_and_track_name('Pink Floyd', 'Wish You Were Here'))
複製代碼
wish you were here tracks:
                                      track_id
60969     feecff58-8ee2-4a7f-ac23-dc8ce7925286
4401932   f479e316-56b4-4221-acd9-eed1a0711861
17332322  2210ba38-79af-4881-97ae-4ce8f32322c3
--------
hotest one:
feecff58-8ee2-4a7f-ac23-dc8ce7925286
複製代碼

生成sentences文件

加載過數據後接下來要生成在科普環節提到的由歌名歌單生成句子,因爲懶,沒有去爬雲音樂的歌單數據,這裏粗暴地將每一個用戶每一天收聽的全部歌曲做爲一個session,使用上文生成的track_index來標識各歌曲,將生成的sentences寫到磁盤上。

def generate_sentence_file(df):
    with open('sentences.txt', 'w') as sentences:
        for user_index in tqdm(range(len(user_index_to_user_id_dict))):
            user_id = user_index_to_user_id_dict[user_index]
            user_df = df[df['user_id'] == user_id].sort_values('timestamp')
            session = list()
            last_time = None
            for index, row in user_df.iterrows():
                this_time = row['timestamp']
                track_index = track_id_to_track_index_dict[row['track_id']]
                if arrow.get(this_time).date() != arrow.get(last_time).date() and last_time != None:
                    sentences.write(' '.join([str(_id) for _id in session]) + '\n')
                    session = list()
                session.append(track_index)
                last_time = this_time
複製代碼
generate_sentence_file(df)
複製代碼
100%|██████████| 992/992 [1:22:23<00:00,  5.62s/it]
複製代碼

生成後的文件長這個樣子:

訓練模型生成embedding

有不少種方式能夠獲取、實現Word2vec的代碼,能夠用Tensorflow、Keras基於神經網絡寫一個,亦可使用Google放到Google Code上的Word2vec實現,也能夠在Github上找到gensim這個優秀的庫使用其已經封裝好的實現。

下列代碼使用smart_open來逐行讀取以前生成的sentences.txt文件,對內存非常友好。這裏使用50維的向量來表明一首歌曲,將收聽總次數不到20次的冷門歌曲篩選出去,設窗口大小爲5。

from smart_open import smart_open
from gensim.models import Word2Vec
import logging

logging.basicConfig()
logging.getLogger().setLevel(logging.INFO)

class LastfmSentences(object):
    
    def __init__(self, file_location):
        self.file_location = file_location
    
    def __iter__(self):
        for line in smart_open(self.file_location, 'r'):
            yield line.split()
            

lastfm_sentences = LastfmSentences('./sentences.txt')
model = Word2Vec(lastfm_sentences, size=50, min_count=20, window=10, hs=0, negative=20, workers=4, sg=1, sample=1e-5)
複製代碼

假如訓練的數據集爲歌單,一個歌單爲一個句子,因爲出如今同一個歌單內表明了其中歌曲的某種共性,那麼會但願將全部item兩兩之間的關係都考慮進去,故window size的取值能夠取(全部歌單長度最大值-1)/2,會取得更好的效果。這裏因爲是以用戶和天作分割,暫且拍腦殼拍出一個10。
sample用於控制對熱門詞彙的採樣比例,下降太過熱門的詞彙對整個模型的影響,好比Radiohead的creep,這裏面還有個計算公式再也不細說。
sg取0、1分別表示使用CBOW與Skip-gram算法,而hs取0、1分別表示使用hierarchical softmax與negative sampling。

關於negative sampling值得多說兩句,在神經網絡的訓練過程當中須要根據梯度降低去調整節點之間的weight,可因爲要調的weight數量巨大,在這個例子裏爲2*50*960000,效率會很低下,處理方法使用負採樣,僅選取此訓練樣本的label爲正例,其餘隨機選取5到20個(經驗數值)單詞爲反例,僅調整與這幾個word對應的weight,會使效率獲取明顯提高,而且效果也很良好。隨機選取的反例的規則亦與單詞出現頻率有關,出現頻次越多的單詞,越有可能會被選中爲反例。

利用embedding

如今已經用大量數據爲各track生成了與本身對應的低維向量,好比Wish You Were Here這首歌,這個embedding能夠做爲該歌曲的標識用於其餘機器學習任務好比learn to rank:

model.wv[str(track_id_to_track_index_dict[
    get_hot_track_id_by_artist_name_and_track_name(
        'Pink Floyd', 'Wish You Were Here')])]
複製代碼
array([-0.39100856,  0.28636533,  0.11853614, -0.41582254,  0.09754885,
        0.59501815, -0.07997745, -0.28060785, -0.0384276 , -0.84899545,
        0.03777567, -0.00727402,  0.6960302 ,  0.44756493, -0.13245133,
       -0.38473454, -0.07809031,  0.34377965, -0.19210865, -0.33457756,
       -0.36364776, -0.06028108,  0.17379969,  0.46617758, -0.04116876,
        0.07322323,  0.11769405,  0.42464802,  0.25167897, -0.35790011,
        0.01991512, -0.10950506,  0.26131895, -0.76148427,  0.48405901,
        0.61935854, -0.59583783,  0.28353232, -0.14503367,  0.3232002 ,
        1.00872386, -0.10348291, -0.0485305 ,  0.21677236, -1.33224928,
        0.57913464, -0.06729769, -0.32185984, -0.02978219, -0.43034038], dtype=float32)
複製代碼

這些embedding vector之間的類似度能夠表示兩首歌出如今同一session內的可能性大小:

shine_on_part_1 = str(track_id_to_track_index_dict[
    get_hot_track_id_by_artist_name_and_track_name('Pink Floyd', 'Shine On You Crazy Diamond (Parts I-V)')])
shine_on_part_2 = str(track_id_to_track_index_dict[
    get_hot_track_id_by_artist_name_and_track_name('Pink Floyd', 'Shine On You Crazy Diamond (Parts Vi-Ix)')])
good_times = str(track_id_to_track_index_dict[
    get_hot_track_id_by_artist_name_and_track_name('Chic', 'Good Times')])

print ('similarity between shine on part 1, 2:', model.wv.similarity(shine_on_part_1, shine_on_part_2))
print ('similarity between shine on part 1, good times:', model.wv.similarity(shine_on_part_1, good_times))
複製代碼
similarity between shine on part 1, 2: 0.927217
similarity between shine on part 1, good times: 0.425195
複製代碼

稍微看下源碼便會發現上述similarity函數,gensim也是使用餘弦類似度來計算的,一樣能夠根據該類似度,來生成一些推薦列表,固然不可能去遍歷,gensim內部也是使用上篇文章提到的Annoy來構建索引來快速尋找近鄰的。爲了使用方便寫了以下兩個包裝函數。

def recommend_with_playlist(playlist, topn=25):
    if not isinstance(playlist, list):
        playlist = [playlist]
    playlist_indexes = [str(track_id_to_track_index_dict[track_id]) for track_id in playlist]
    similar_song_indexes = model.wv.most_similar(positive=playlist_indexes, topn=topn)
    return [track_index_to_track_id_dict[int(track[0])] for track in similar_song_indexes]

def display_track_info(track_ids):
    track_info = {
        'track_name': [],
        'artist_name': [],
    }
    for track_id in track_ids:
        track = song_info_df[song_info_df['track_id'] == track_id].iloc[0]
        track_info['track_name'].append(track['track_name'])
        track_info['artist_name'].append(track['artist_name'])
    print (pd.DataFrame(track_info))
複製代碼

接下來僞裝本身在聽後朋,提供幾首歌曲,看看模型會給咱們推薦什麼:

# post punk.

guerbai_playlist = [
    ('Joy Division', 'Disorder'),
    ('Echo & The Bunnymen', 'The Killing Moon'),
    ('The Names', 'Discovery'),
    ('The Cure', 'Lullaby'),
    
]

display_track_info(recommend_with_playlist([
    get_hot_track_id_by_artist_name_and_track_name(track[0], track[1]) 
    for track in guerbai_playlist], 20))
複製代碼
track_name          artist_name
0               Miss The Girl        The Creatures
1      Splintered In Her Head             The Cure
2    Return Of The Roughnecks       The Chameleons
3                P.S. Goodbye       The Chameleons
4                Chelsea Girl         Simple Minds
5    23 Minutes Over Brussels              Suicide
6          Not Even Sometimes            The Prids
7                     Windows  A Flock Of Seagulls
8     Ride The Friendly Skies       Lightning Bolt
9                Inmost Light      Double Leopards
10              Thin Radiance             Sunroof!
11        You As The Colorant            The Prids
12    Love Will Tear Us Apart         Boy Division
13                  Slip Away             Ultravox
14                Street Dude           Black Dice
15              Touch Defiles        Death In June
16     All My Colours (Zimbo)  Echo & The Bunnymen
17                Summernight             The Cold
18         Pornography (Live)             The Cure
19  Me, I Disconnect From You           Gary Numan
複製代碼

好多樂隊都沒見過,wiki一下發現果真大都是後朋與新浪潮樂隊的歌曲,搞笑的是Love Will Tear Us Apart居然成了Boy Division的了,這數據集有毒。。

過了半年又沉浸在前衛搖滾的長篇裏:

# long progressive

guerbai_playlist = [
    ('Rush', '2112: Ii. The Temples Of Syrinx'),
    ('Yes', 'Roundabout'),
    ('Emerson, Lake & Palmer', 'Take A Pebble'),
    ('Jethro Tull', 'Aqualung'),
]

display_track_info(recommend_with_playlist([
    get_hot_track_id_by_artist_name_and_track_name(track[0], track[1]) 
    for track in guerbai_playlist]))
複製代碼
track_name             artist_name
0                            Nutrocker  Emerson, Lake & Palmer
1                  Brain Salad Surgery  Emerson, Lake & Palmer
2                           Black Moon  Emerson, Lake & Palmer
3                            Parallels                     Yes
4                      Working All Day            Gentle Giant
5                            Musicatto                  Kansas
6                    Farewell To Kings                    Rush
7                    My Sunday Feeling             Jethro Tull
8             Thick As A Brick, Part 1             Jethro Tull
9                South Side Of The Sky                     Yes
10                  Living In The Past             Jethro Tull
11  The Fish (Schindleria Praematurus)                     Yes
12                    Starship Trooper                     Yes
13                                Tank  Emerson, Lake & Palmer
14              I Think I'M Going Bald                    Rush
15                          Here Again                    Rush
16                           Lucky Man  Emerson, Lake & Palmer
17                      Cinderella Man                    Rush
18                        Stick It Out                    Rush
19                   The Speed Of Love                    Rush
20                   New State Of Mind                     Yes
21         Karn Evil 9: 2Nd Impression  Emerson, Lake & Palmer
22                           A Venture                     Yes
23                          Cygnus X-1                    Rush
24                         Sweet Dream             Jethro Tull
複製代碼

人是會變的,今天她喜歡聽後朋,明天可能喜歡別的,但既然咱們有數學與集體智慧,這又有什麼關係呢?

參考

Using Word2vec for Music Recommendations
Word2Vec Tutorial - The Skip-Gram Model

相關文章
相關標籤/搜索