你也可以構建的高級但卻很簡單的購物推薦系統--item2vec

簡介

本文可以算是一遍item2vector的工程化教程,其基本理論源自於論文《Item2Vec: Neural Item Embedding for Collaborative Filtering》(https://arxiv.org/abs/1603.04259)將引導你瞭解word2vec背後的思想,以及它在電商推薦系統領域中的一個擴展(item2vec)。具體來說,利用word2vec的概念和gensim軟件包提供的模塊,將構建一個輕量級的電影推薦系統。

item2vector簡介 : 計算items的相似性是現代推薦系統中的一個關鍵組成部分。過去,人們只是通過判斷“有無”的方式來向量化系統中的各個items,這樣的方式並不能很好地表示出items之間的關係,而且泛化能力很差。因此需要一種特殊的方法來將items映射到能夠滿足我們需求的向量上去。
   後來研究人員研究出了使用SVD(奇異值分解,矩陣分解的一種)的方式來計算表示items的向量,這種方法從某種程度上已經滿足了人們在計算items的相似性以及模型泛化能力的需求了。但是隨着自然語言領域不斷髮展,研究人員開發出了一種叫做word2vec的神經語言模型,通過神經網絡模型來隱式地計算出詞彙表中的每一個單詞的向量化表示,這種表示方式能夠很好地描述單詞間的語義和語法關係,從而使得模型具有相當的泛化能力,因此該方法及其變形已經被廣泛地應用到NLP領域的各個方面。
   所以,論文指出,可以使用相似的方法來計算協助過濾中所有items的向量化表示,從而隱式地得到items間的相互關係,使得我們能夠更好地計算出items間的相似性以及提升模型的泛化能力。
它是一種基於item的協同過濾算法(Collaborative Filtering algorithm),利用在潛在空間( latent space)中item embedding 表徵。與傳統的奇異值分解(SVD)方法甚至是深度學習算法相比,該算法具有很強的競爭力。

簡單介紹一下word2vec 和item2vec的原理

文章將引用 McCormick, C.的一篇關於 Word2Vec的教程,想了解更多關於word2vec的內容和原理,極力推薦閱讀原文
word2vec其實包括兩個淺層神經網絡結構。即CBOW和Skip-Gram結構。這些體系結構描述了神經網絡如何“學習”神經網絡中每一層節點的權重。這裏將簡單地解釋一下Skip-Gram的結構,這足以讓我們理解item2vec了。
Skip-Gram就是:給一個單詞,預測在字典裏頭常常出現在該單詞"鄰近"的單詞的概率。這裏的"鄰近"就是定義在一個固定大小的窗口(window)內,在句子中,距離給定單詞小於固定窗口(window)的大小的單詞。例如:“在 這裏 生活着,我 很 快樂!”,如果取大小爲2的窗口,“我”的鄰近單詞[“很”,“快樂”,“生活着”,“這裏”],[“在”]就不在鄰近的集合內部。
Skip Gram體系結構使用以下佈局來解決問題:
在這裏插入圖片描述
在這種結構中,對於一個給定的單詞,它的一個獨熱編碼(one-hot)的單詞向量被投影到一個嵌入到中間層的低維單詞(embedding)中,然後轉換成一個指定其周圍單詞概率的向量。想了解更多,請閱讀此文
這裏的機器學習優化問題是從數據中學習最優的投影矩陣。Wrod2vec是一種“自監督”算法,從這個意義上說,雖然我們不需要提供標記的數據,但是需要從數據中生成“正確的概率”,以便算法學習權重。下面的圖表最能說明生成training sample的過程。根據生成的詞對,可以計算出“正確”概率。每個訓練示例都將被傳遞到這個結構中,以調整投影矩陣的權重。
在這裏插入圖片描述
令人興奮的是,在這個體系結構的中間層學習到的嵌入表徵(embedding representation)保留了語義屬性和單詞之間的關係。而單詞的輸入編碼並不要求有任何的關於單詞特徵和單詞之間關係的屬性。例如,從輸入層中的一個獨熱編碼(one-hot)創建的詞表徵(representation)沒有這些屬性。
word2vec完整的skip-Gram的架構還包括負採樣,以降低計算複雜性,提高詞向量的質量。以上基本是描述了skip-Gram的簡單形式,但這就是理解item2vec所需要的全部內容。item2vec架構也是利用了skip-Gram和負採樣,所以只要假設每個item是一個“word”,每個項目集合是一個“sentence”,我們的目標是學習item embedding,以找出item之間的關係。想了解更多item2vec,請查看原論文。

準備數據

這裏將使用由movieens研究團隊策劃的movieens 20M數據集。它包含由138000名用戶完成關於27000部電影的2000萬個評價數據和46.5萬個標籤數據。有關詳細信息,您可以訪問官方網站。您可以通過此鏈接下載數據集。
爲了構建推薦系統,將使用下載數據集中的“movies.csv”和“ratings.csv”文件。“movies.csv”是一個查找表,用於查找電影的ID及其名稱。“ratings.csv”包含所有電影用戶的分級。以下代碼讀取csv文件,檢查數據並可視化分級分佈。爲了方便起見,我還爲電影ID和電影名稱創建了查找字典。

import pandas as pd
import numpy as np

df_movies = pd.read_csv('ml-20m/movies.csv')
df_ratings = pd.read_csv('ml-20m/ratings.csv')

movieId_to_name = pd.Series(df_movies.title.values, index = df_movies.movieId.values).to_dict()
name_to_movieId = pd.Series(df_movies.movieId.values, index = df_movies.title).to_dict()

# Randomly display 5 records in the dataframe
for df in list((df_movies, df_ratings)):
    rand_idx = np.random.choice(len(df), 5, replace=False)
    display(df.iloc[rand_idx,:])
    print("Displaying 5 of the total "+str(len(df))+" data points")

在這裏插入圖片描述

import matplotlib.pyplot as plt
import plotly.plotly as py
%matplotlib inline

plt.figure(figsize=(8, 6))
ax = plt.subplot(111)
ax.set_title("Distribution of Movie Ratings", fontsize=16)
ax.spines["top"].set_visible(False)  
ax.spines["right"].set_visible(False)  
  
plt.xticks(fontsize=12)  
plt.yticks(fontsize=12)  
  
plt.xlabel("Movie Rating", fontsize=14)  
plt.ylabel("Count", fontsize=14)  
  
plt.hist(df_ratings['rating'], color="#3F5D7D")  

plt.show()

在這裏插入圖片描述
在標準的機器學習開發流程中,首先需要將數據分爲訓練集和測試集。測試集用於評估模型。評估推薦系統有多種方法,這會影響如何分割數據。將會使用精確度、召回率和topK f-1評分來評估模型性能(在評估性能部分中進行了解釋),因此對userId進行分層分割。對於每個用戶,以70:30的比率將電影數據分爲“訓練集和測試集數據”分割開來。通過Scikit-Learn,可以在一行代碼中完成這項工作。

from sklearn.model_selection import train_test_split

df_ratings_train, df_ratings_test= train_test_split(df_ratings,
                                                    stratify=df_ratings['userId'],
                                                    random_state = 15688,
                                                    test_size=0.30)
print("Number of training data: "+str(len(df_ratings_train)))
print("Number of test data: "+str(len(df_ratings_test)))

在這裏插入圖片描述
爲了讓模型學習item embedding,需要從數據中獲取“單詞”和“句子”等價物。在這裏,把每個“電影”看做是一個“詞”,並且從用戶那裏獲得相似評級的電影都在同一個“句子”中。
具體來說,“句子”是通過以下過程生成的:爲每個用戶生成2個列表,分別存儲用戶“喜歡”和“不喜歡”的電影。第一個列表包含所有的電影評級爲4分或以上。第二個列表包含其餘的電影。這些列表就是訓練gensim word2vec模型的輸入了。

def rating_splitter(df):
    
    df['liked'] = np.where(df['rating']>=4, 1, 0)
    df['movieId'] = df['movieId'].astype('str')
    gp_user_like = df.groupby(['liked', 'userId'])

    return ([gp_user_like.get_group(gp)['movieId'].tolist() for gp in gp_user_like.groups])
pd.options.mode.chained_assignment = None
splitted_movies = rating_splitter(df_ratings_train)

利用Gensim 訓練item2vec 的模型

在本節中,我們將把訓練數據輸入gensim word2vec模塊中,調整窗口大小,訓練item2vec模型。
對於原來的word2vec,窗口大小會影響我們搜索“上下文”以定義給定單詞含義的範圍。按照定義,窗口的大小是固定的。但是,在item2vec實現中,電影的“含義”應該由同一列表中的所有鄰居捕獲。換句話說,我們應該考慮用戶“喜歡”的所有電影,以定義這些電影的“含義”。這也適用於用戶“不喜歡”的所有電影。然後需要根據每個電影列表的大小更改窗口大小。
爲了在不修改gensim模型的底層代碼的情況下解決這個問題,首先指定一個非常大的窗口大小,這個窗口大小遠遠大於訓練樣本中任何電影列表的長度。然後,在將訓練數據輸入模型之前對其進行無序處理,因爲在使用“鄰近”定義電影的“含義”時,電影的順序沒有任何意義。
Gensim模型中的窗口參數實際上是隨機動態的。我們指定最大窗口大小,而不是實際使用的窗口大小。儘管上面的解決方法並不理想,但它確實實現了可接受的性能。最好的方法可能是直接修改gensim中的底層代碼,但這就超出了我目前的能力範圍了,哈哈哈。
在訓練模型之前,需要確保gensim是使用C編譯器的,運行下面的代碼來驗證這一點。

import warnings
warnings.filterwarnings(action='ignore', category=UserWarning, module='gensim')

import gensim
assert gensim.models.word2vec.FAST_VERSION > -1

然後打亂數據集:

import random

for movie_list in splitted_movies:
    random.shuffle(movie_list)

下面訓練兩個模型,當然,訓練模型需要些時間,不同的機器耗時不一樣:

from gensim.models import Word2Vec
import datetime
start = datetime.datetime.now()

model = Word2Vec(sentences = splitted_movies, # We will supply the pre-processed list of moive lists to this parameter
                 iter = 5, # epoch
                 min_count = 10, # a movie has to appear more than 10 times to be keeped
                 size = 200, # size of the hidden layer
                 workers = 4, # specify the number of threads to be used for training
                 sg = 1, # Defines the training algorithm. We will use skip-gram so 1 is chosen.
                 hs = 0, # Set to 0, as we are applying negative sampling.
                 negative = 5, # If > 0, negative sampling will be used. We will use a value of 5.
                 window = 9999999)

print("Time passed: " + str(datetime.datetime.now()-start))
#Word2Vec.save('item2vec_20180327')

Time passed: 2:12:26.134283


from gensim.models import Word2Vec
import datetime
start = datetime.datetime.now()

model_w2v_sg = Word2Vec(sentences = splitted_movies,
                        iter = 10, # epoch
                        min_count = 5, # a movie has to appear more than 5 times to be keeped
                        size = 300, # size of the hidden layer
                        workers = 4, # specify the number of threads to be used for training
                        sg = 1,
                        hs = 0,
                        negative = 5,
                        window = 9999999)

print("Time passed: " + str(datetime.datetime.now()-start))
model_w2v_sg.save('item2vec_word2vecSg_20180328')
del model_w2v_sg

Time passed: 5:32:50.270232

模型訓練完之後,模型可以保存在您的存儲中以備將來使用。注意,gensim保存了所有關於模型的信息,包括隱藏的權重、詞彙頻率和模型的二叉樹,因此可以在加載文件後繼續訓練。然而,這是以運行模型時的內存爲代價的,因爲它將存儲在你的RAM中。如果你只需要隱藏層的權重,它可以從模型中單獨提取。下面的代碼演示如何保存、加載模型和提取單詞向量(embedding)。

import warnings
warnings.filterwarnings(action='ignore', category=UserWarning, module='gensim')

from gensim.models import Word2Vec
model = Word2Vec.load('item2vec_20180327')
word_vectors = model.wv
# del model # uncomment this line will delete the model

推薦系統來了!!!

一旦模型訓練完,就可以使用Gensim中的內置方法來做推薦!真正使用的是gensim model.wv.most_similar_word()方法。這些實用程序將接受用戶的輸入,並將它們輸入gensim方法中,從中根據對IMDB的搜索推斷最可能的電影名稱,將它們轉換爲電影ID。

import requests
import re
from bs4 import BeautifulSoup

def refine_search(search_term):
    """
    Refine the movie name to be recognized by the recommender
    Args:
        search_term (string): Search Term

    Returns:
        refined_term (string): a name that can be search in the dataset
    """
    target_url = "http://www.imdb.com/find?ref_=nv_sr_fn&q="+"+".join(search_term.split())+"&s=tt"
    html = requests.get(target_url).content
    parsed_html = BeautifulSoup(html, 'html.parser')
    for tag in parsed_html.find_all('td', class_="result_text"):
        search_result = re.findall('fn_tt_tt_1">(.*)</a>(.*)</td>', str(tag))
        if search_result:
            if search_result[0][0].split()[0]=="The":
                str_frac = " ".join(search_result[0][0].split()[1:])+", "+search_result[0][0].split()[0]
                refined_name = str_frac+" "+search_result[0][1].strip()
            else:
                refined_name = search_result[0][0]+" "+search_result[0][1].strip()
    return refined_name

def produce_list_of_movieId(list_of_movieName, useRefineSearch=False):
    """
    Turn a list of movie name into a list of movie ids. The movie names has to be exactly the same as they are in the dataset.
       Ambiguous movie names can be supplied if useRefineSearch is set to True
    
    Args:
        list_of_movieName (List): A list of movie names.
        useRefineSearch (boolean): Ambiguous movie names can be supplied if useRefineSearch is set to True

    Returns:
        list_of_movie_id (List of strings): A list of movie ids.
    """
    list_of_movie_id = []
    for movieName in list_of_movieName:
        if useRefineSearch:
            movieName = refine_search(movieName)
            print("Refined Name: "+movieName)
        if movieName in name_to_movieId.keys():
            list_of_movie_id.append(str(name_to_movieId[movieName]))
    return list_of_movie_id

def recommender(positive_list=None, negative_list=None, useRefineSearch=False, topn=20):
    recommend_movie_ls = []
    if positive_list:
        positive_list = produce_list_of_movieId(positive_list, useRefineSearch)
    if negative_list:
        negative_list = produce_list_of_movieId(negative_list, useRefineSearch)
    for movieId, prob in model.wv.most_similar_cosmul(positive=positive_list, negative=negative_list, topn=topn):
        recommend_movie_ls.append(movieId)
    return recommend_movie_ls

給出他/她喜歡迪斯尼電影“Up(2009)”,下面的代碼顯示了對該用戶的最喜歡的前五的推薦結果:

ls = recommender(positive_list=["UP"], useRefineSearch=True, topn=5)
print('Recommendation Result based on "Up (2009)":')
display(df_movies[df_movies['movieId'].isin(ls)])

在這裏插入圖片描述
在這裏插入圖片描述
再看看更有趣的的,我和我的朋友都喜歡科幻經典小說《“The Matrix (1999)”》。但當談到昆汀·塔倫蒂諾的標誌性作品《Django Unchained (2012)》時,我們有不同的看法。雖然我喜歡荒誕和幽默的諷刺混合,但我的朋友厭惡鮮血和暴力。根據我們的口味,這個模型會推薦什麼?把這些數據輸入我們的模型,《Men in Black (1997)》和《Ghostbusters (a.k.a Ghost Busters)》都在我朋友的推薦名單上。我有《“Inglourious Basterds (2009)”》《“Inception (2010)”》和《The Dark Knight Rises (2006)》。

ls = recommender(positive_list=["The Matrix"], negative_list=["Django Unchained"], useRefineSearch=True, topn=7)
print('Recommendation Result based on "The Matrix (1999)" minus "Django Unchained (2012)":')
display(df_movies[df_movies['movieId'].isin(ls)])

在這裏插入圖片描述
在這裏插入圖片描述

ls = recommender(positive_list=["The Matrix", "Django Unchained"], useRefineSearch=True, topn=7)
print('Recommendation Result based on "The Matrix (1999)" + ""Django Unchained (2012)":')
display(df_movies[df_movies['movieId'].isin(ls)])

在這裏插入圖片描述
在這裏插入圖片描述
這不是一篇刷分博文,就不針對模型的表現做評估了,各位心動的小夥伴可以試試哦。
聲明:本文參考一個同性交友平臺的某一篇博客。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章