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

搖滾樂經過幾十年的發展,風格流派眾多,從blues,到brit invasion,之后是punk,disco,indie rock等等。發展歷程大致是這樣的:

history of rock

搖滾樂的聽眾,總是能體會到發現寶藏的快樂,可能突然就會邂逅某支自己不曾接觸過的歌曲、樂隊、風格,感覺好聽得不行,以前怎么從來不知道,接下來的一段時間便會沉浸于此,每天都在聽該風格的主要樂隊和專輯。用戶收聽音樂在一段時間內可能是有著某個“主題”的,這個主題可能是地理上的(俄羅斯的搖滾樂隊),可能是時間上的(2000年后優秀的專輯),還可能是某流派、甚至是都被某影視作品用作BGM。之前很少聽國內搖滾的筆者,在去年聽了刺猬、P.K.14、重塑雕像的權利、新褲子、海朋森等一些國內樂隊的很多作品后,才知道原來在老崔、竇唯、萬青、老謝之外還有這么多優秀的國產搖滾樂。

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

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

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。

image

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

image

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

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

故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]

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


image

訓練模型生成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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容