1. 程式人生 > >Kaggle 官方教程:嵌入

Kaggle 官方教程:嵌入

原文:Embeddings

譯者:飛龍

協議:CC BY-NC-SA 4.0

P.S…

本課程仍處於測試階段,因此我很樂意收到你的反饋意見。 如果你有時間填寫本課程的超短期調查,我將非常感激。 你也可以在下面的評論中或在學習論壇上留下公開反饋。

一、嵌入層

歡迎閱讀嵌入主題的第一課。 在本課程中,我將展示如何使用tf.keras API 實現帶嵌入層的模型。 嵌入是一種技術,使深度神經網路能夠處理稀疏的類別變數。

稀疏類別變數

我的意思是一個具有許多可能值(高基數)的類別變數,其中少數(通常只有 1)存在於任何給定的觀察中。 一個很好的例子是詞彙。 英語中的詞彙是成千上萬的,但一條推文可能只有十幾個詞。 詞嵌入是將深度學習應用於自然語言的關鍵技術。 但其他例子還有很多。

例如,洛杉磯餐館檢查的這個資料集有幾個稀疏的分類變數,包括:

  • employee_id:衛生部門的哪些員工進行了這次檢查? (約 250 個不同的值)
  • facility_zip:餐廳的郵政編碼是什麼? (約 3,000 個不同的值)
  • owner_name:誰擁有餐廳? (約 35,000 個不同的值)

對於將任何這些變數用作網路的輸入,嵌入層是個好主意。

在本課程中,我將使用 MovieLens 資料集作為示例。

MovieLens

MovieLens 資料集由使用者給電影的評分組成。這是一個示例:

userId movieId rating y title year
12904240 85731 1883 4.5 0.974498 Labyrinth 1986
6089380 45008 1221 4.5 0.974498 Femme Nikita, La (Nikita) 1990
17901393 125144 3948 4.0 0.474498 The Alamo 1960
9024816 122230 3027 3.5 -0.025502 Toy Story 2 1999
11655659 21156 5202 3.0 -0.525502 My Big Fat Greek Wedding

評分範圍是 0.5 到 5。我們的目標是預測由給定使用者ui給出的,特定電影mj的評分。 (列y只是評分列的副本,減去了平均值 - 這在以後會有用。)

userIdmovieId都是稀疏分類變數。 它們有許多可能的值:

138,493 個獨立使用者評分了 26,744 個不同的電影(總評分是 20,000,263 個)

在 Keras 中建立評分預測模型

我們想要構建一個模型,它接受使用者ui和電影mj,並輸出 0.5-5 的數字,表示我們認為該使用者將為該電影評分多少星。

注:你可能已經注意到MovieLens資料集包含每部電影的資訊,例如標題,發行年份,一組流派和使用者指定的標籤。 但就目前而言,我們不會試圖利用任何額外的資訊。

我說我們需要一個嵌入層來處理這些輸入。 為什麼? 讓我們回顧一些替代方案,看看為什麼它們不起作用。

壞主意 #1:使用使用者和電影 ID 作為數值輸入

為什麼不將使用者 ID 和電影 ID 作為數值來輸入,然後新增一些密集層?即:

model = keras.Sequential([
    # 2 個輸入值:使用者 ID 和電影 ID
    keras.layers.Dense(256, input_dim=2, activation='relu'),
    keras.layers.Dense(32, activation='relu'),
    # 單個輸出節點,包含預測的評分
    keras.layers.Dense(1)
])

用最簡單的術語來說,神經網路的原理是對輸入進行數學運算。 但分配給使用者和電影的 ID 的實際數值是沒有意義的。《辛德勒的名單》的 id 為 527,而《非常嫌疑犯》的 id 為 50,但這並不意味著《辛德勒的名單》比《非常嫌疑犯》大十倍。

壞主意 #2:獨熱編碼的使用者和電影輸入

如果你不熟悉單熱編碼,可能需要檢視我們的課程“使用獨熱編碼處理類別資料”

在該課程中,我們說獨熱編碼是“類別資料的標準方法”。 那麼為什麼這是一個壞主意呢? 讓我們看看模型是什麼樣子,它接受獨熱編碼的使用者和電影。

input_size = n_movies + n_users
print("Input size = {:,} ({:,} movies + {:,} users)".format(
    input_size, n_movies, n_users,
))
model = keras.Sequential([
    # 一個帶有 128 個單元的隱藏層
    keras.layers.Dense(128, input_dim=input_size, activation='relu'),
    # 單個輸出節點,包含預測的評分
    keras.layers.Dense(1)
])
model.summary()
'''
Input size = 165,237 (26,744 movies + 138,493 users)
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_3 (Dense)              (None, 128)               21150464  
_________________________________________________________________
dense_4 (Dense)              (None, 1)                 129       
=================================================================
Total params: 21,150,593
Trainable params: 21,150,593
Non-trainable params: 0
_________________________________________________________________
'''

這裡的一個基本問題是擴充套件和效率。 我們模型的單個輸入是包含 165,237 個數字的向量(其中我們知道 165,235 是零)。 我們整個 2000 萬個評分例項資料集的特徵資料,將需要一個大小為 20,000,000 x 165,237 或大約 3 萬億個數字的二維陣列。 但願你能把這一切都放進記憶體中!

此外,在我們的模型上進行訓練和推斷將是低效的。 為了計算我們的第一個隱藏層的啟用,我們需要將我們的 165k 輸入乘以大約 2100 萬個權重 - 但是這些乘積的絕大多數都將為零。

對於具有少量可能值的分類變數,例如{Red, Yellow, Green}{Monday, Tuesday, Wednesday, Friday, Saturday, Sunday},獨熱編碼是合適的。 但在像我們的電影推薦問題的情況下,它並不是那麼好,其中變數有數十或數十萬個可能的值。

好主意:嵌入層

簡而言之,嵌入層將一組離散物件(如單詞,使用者或電影)中的每個元素對映到實數的密集(嵌入)向量。

注:一個關鍵的實現細節是嵌入層接受被嵌入實體的索引作為輸入(即我們可以將userIdmovieId作為輸入)。 你可以將其視為一種“查詢表”。 這比採用獨熱向量並進行巨大的矩陣乘法要有效得多!

例如,如果我們為電影學習大小為 8 的嵌入,則《律政俏佳人》(index = 4352)的嵌入可能如下所示:

[1.624,−0.612,−0.528,−1.073,0.865,−2.302,1.745,−0.761]

它們來自哪裡? 我們使用隨機噪聲為每個使用者和電影初始化嵌入,然後我們將它們訓練,作為整體評分預測模型訓練過程的一部分。

他們的意思是什麼? 如果物件的嵌入有任何好處,它應該捕獲該物件的一些有用的潛在屬性。 但這裡的關鍵詞是潛在,也就是隱藏的。 由模型來發現實體的任何屬性,並在嵌入空間中對它們編碼,對預測任務有用。 聽起來很神祕? 在後面的課程中,我將展示一些解釋習得的嵌入的技術,例如使用 t-SNE 演算法將它們視覺化。

實現它

我希望我的模型是這樣:

需要注意的一個關鍵點是,這個網路不僅僅是從輸入到輸出的一堆層級。 我們將使用者和電影視為單獨的輸入,只有在每個輸入經過自己的嵌入層之後才會聚集在一起。

這意味著keras.Sequential類(你可能從我們的影象資料深度學習課程中熟悉它)將無法工作。 我們需要使用keras.Model類轉向更強大的“函式式 API”。 函式式 API 的更多詳細資訊,請檢視 Keras 的指南

這是程式碼:

hidden_units = (32,4)
movie_embedding_size = 8
user_embedding_size = 8

# 每個例項將包含兩個輸入:單個使用者 ID 和單個電影 ID
user_id_input = keras.Input(shape=(1,), name='user_id')
movie_id_input = keras.Input(shape=(1,), name='movie_id')
user_embedded = keras.layers.Embedding(df.userId.max()+1, user_embedding_size, 
                                       input_length=1, name='user_embedding')(user_id_input)
movie_embedded = keras.layers.Embedding(df.movieId.max()+1, movie_embedding_size, 
                                        input_length=1, name='movie_embedding')(movie_id_input)
# 連線嵌入(並刪除無用的額外維度)
concatenated = keras.layers.Concatenate()([user_embedded, movie_embedded])
out = keras.layers.Flatten()(concatenated)

# 新增一個或多個隱層
for n_hidden in hidden_units:
    out = keras.layers.Dense(n_hidden, activation='relu')(out)

# 單一輸出:我們的預測評分
out = keras.layers.Dense(1, activation='linear', name='prediction')(out)

model = keras.Model(
    inputs = [user_id_input, movie_id_input],
    outputs = out,
)
model.summary(line_length=88)
'''
________________________________________________________________________________________
Layer (type)                 Output Shape       Param #   Connected to                  
========================================================================================
user_id (InputLayer)         (None, 1)          0                                       
________________________________________________________________________________________
movie_id (InputLayer)        (None, 1)          0                                       
________________________________________________________________________________________
user_embedding (Embedding)   (None, 1, 8)       1107952   user_id[0][0]                 
________________________________________________________________________________________
movie_embedding (Embedding)  (None, 1, 8)       213952    movie_id[0][0]                
________________________________________________________________________________________
concatenate (Concatenate)    (None, 1, 16)      0         user_embedding[0][0]          
                                                          movie_embedding[0][0]         
________________________________________________________________________________________
flatten (Flatten)            (None, 16)         0         concatenate[0][0]             
________________________________________________________________________________________
dense_5 (Dense)              (None, 32)         544       flatten[0][0]                 
________________________________________________________________________________________
dense_6 (Dense)              (None, 4)          132       dense_5[0][0]                 
________________________________________________________________________________________
prediction (Dense)           (None, 1)          5         dense_6[0][0]                 
========================================================================================
Total params: 1,322,585
Trainable params: 1,322,585
Non-trainable params: 0
________________________________________________________________________________________
'''

訓練

我們將編譯我們的模型,來最小化平方誤差(‘MSE’)。 我們還將絕對值誤差(‘MAE’)作為在訓練期間報告的度量標準,因為它更容易解釋。

需要考慮的事情:我們知道評分只能取值{0.5,1,1.5,2,2.5,3,3.5,4,4.5,5} - 所以為什麼不將其視為 10 類的多類分類問題 ,每個可能的星級評分一個?

model.compile(
    # 技術說明:使用嵌入層時,我強烈建議使用
    # tf.train 中發現的優化器之一:
    # https://www.tensorflow.org/api_guides/python/train#Optimizers
    # 傳入像 'adam' 或 'SGD' 這樣的字串,會載入一個 keras 優化器 
    # (在 tf.keras.optimizers 下尋找)。 對於像這樣的問題,它們似乎要慢得多,
    # 因為它們無法有效處理稀疏梯度更新。
    tf.train.AdamOptimizer(0.005),
    loss='MSE',
    metrics=['MAE'],
)

讓我們訓練模型:

注:我傳入df.y而不是df.rating,作為我的目標變數。y列只是評分的“中心”版本 - 即評分列減去其在訓練集上的平均值。 例如,如果訓練集中的總體平均評分是 3 星,那麼我們將 3 星評分翻譯為 0, 5星評分為 2.0 等等,來獲得y。 這是深度學習中的常見做法,並且往往有助於在更少的時期內獲得更好的結果。 對於更多詳細資訊,請隨意使用我在 MovieLens 資料集上執行的所有預處理來檢查這個核心

history = model.fit(
    [df.userId, df.movieId],
    df.y,
    batch_size=5000,
    epochs=20,
    verbose=0,
    validation_split=.05,
);

為了判斷我們的模型是否良好,有一個基線是有幫助的。 在下面的單元格中,我們計算了幾個虛擬基線的誤差:始終預測全域性平均評分,以及預測每部電影的平均評分:

  • 訓練集的平均評分:3.53 星
  • 總是預測全域性平均評分,結果為 MAE=0.84,MSE=1.10
  • 預測每部電影的平均評分,結果為 MAE=0.73,MSE=0.88

這是我們的嵌入模型的絕對誤差隨時間的繪圖。 為了進行比較,我們的最佳基線(預測每部電影的平均評分)用虛線標出:

與基線相比,我們能夠將平均誤差降低超過 0.1 星(或約 15%)。不錯!

示例預測

讓我們嘗試一些示例預測作為健全性檢查。 我們首先從資料集中隨機挑選一個特定使用者。

使用者 #26556 評分了 21 個電影(平均評分為 3.7)
userId movieId rating title year
4421455 26556 2705 5.0 Airplane! 1980
14722970 26556 2706 5.0 Airplane II: The Sequel 1982
7435440 26556 2286 4.5 Fletch 1985
16621016 26556 2216 4.5 History of the World: Part I 1981
11648630 26556 534 4.5 Six Degrees of Separation 1993
14805184 26556 937 4.5 Mr. Smith Goes to Washington 1939
14313285 26556 2102 4.5 Strangers on a Train 1951
13671173 26556 2863 4.5 Dr. No 1962
13661434 26556 913 4.0 Notorious 1946
11938282 26556 916 4.0 To Catch a Thief 1955
2354167 26556 3890 4.0 Diamonds Are Forever 1971
16095891 26556 730 4.0 Spy Hard 1996
16265128 26556 3414 3.5 Network 1976
13050537 26556 1414 3.5 Waiting for Guffman 1996
9891416 26556 2907 3.5 Thunderball 1965
3496223 26556 4917 3.5 Gosford Park 2001
1996728 26556 1861 3.0 On the Waterfront 1954
15893218 26556 1082 2.5 A Streetcar Named Desire 1951
13875921 26556 3445 2.5 Keeping the Faith 2000
13163853 26556 1225 2.0 The Day the Earth Stood Still 1951
7262983 26556 2348 0.5 A Civil Action 1998

使用者 26556 給電影《空前絕後滿天飛》和《空前絕後滿天飛 II》打了兩個完美的評分。很棒的選擇! 也許他們也會喜歡《白頭神探》系列 - 另一系列由 Leslie Nielsen 主演的惡搞電影。

我們沒有那麼多關於這個使用者討厭什麼的證據。 我們不根據他們的少數低評分做推斷,使用者不喜歡什麼的更好推斷是,他們甚至沒有評價的電影型別。 讓我們再舉幾個電影的例子,根據使用者的評分歷史記錄,他們似乎不太可能看過。

candidate_movies = movies[
    movies.title.str.contains('Naked Gun')
    | (movies.title == 'The Sisterhood of the Traveling Pants')
    | (movies.title == 'Lilo & Stitch')
].copy()

preds = model.predict([
    [uid] * len(candidate_movies), # 使用者 ID
    candidate_movies.index, # 電影 ID
])
# 注意:記住我們在 'y' 上訓練,這是評分列的中心為 0 的版本。
# 要將我們模型的輸出值轉換為 [0.5, 5] 原始的星級評分範圍, 
# 我們需要通過新增均值來對值“去中心化”
row = df.iloc[0] # rating 和 y 之間的差對於所有行都是相同的,所以我們可以使用第一行
y_delta = row.rating - row.y
candidate_movies['predicted_rating'] = preds + y_delta
# 新增一列,帶有我們的預測評分(對於此使用者)
# 和電影對於資料集中所有使用者的總體平均評分之間的差 
candidate_movies['delta'] = candidate_movies['predicted_rating'] - candidate_movies['mean_rating']
candidate_movies.sort_values(by='delta', ascending=False)
title year mean_rating n_ratings predicted_rating delta
movieId
366 Naked Gun 33 1/3: The Final Insult 1994 2.954226 13534.0 3.816926 0.862699
3776 The Naked Gun 2 1/2: The Smell of Fear 1991 3.132616 4415.0 3.946124 0.813508
3775 The Naked Gun: From the Files of Police Squad! 1988 3.580381 6973.0 4.236419 0.656037
5347 Lilo & Stitch 2002 3.489323 4402.0 3.971318 0.481995
10138 The Sisterhood of the Traveling Pants 2005 3.369987 773.0 2.041227 -1.328760

看起來很合理! 對於《白頭神探》系列中的每部電影,我們對此使用者的預測評分,大約比資料集中平均評分高一星,而我們的“out of left field”使它們的預測評分低於平均值。

你的回合

前往練習筆記本,進行嵌入層的實踐練習。

二、用於推薦問題的矩陣分解

在上一課中,我們訓練了一個模型來預測在 MovieLens 資料集中,使用者給電影的評分。 提醒一下,模型看起來像這樣:

我們為電影和使用者查詢嵌入向量,將它們連線在一起。 然後我們新增一些隱層。 最後,這些在一個輸出節點彙集在一起來預測評分。

在本節課中,我將展示一個更簡單的架構,來解決同樣的問題:矩陣分解。 更簡單可以是一件非常好的事情! 有時,簡單的模型會快速收斂到適當的解決方案,一個更復雜的模型可能會過擬合或無法收斂。

這是我們的矩陣分解模型的樣子:

點積

讓我們回顧一下數學。 如果你是線性代數專家,請跳過本節。

兩個長度為n的向量ab的點積定義為:

結果是單個標量(不是向量)。

點積僅為相同長度的向量而定義。 這意味著我們需要為電影嵌入和使用者嵌入使用相同的大小。

例如,假設我們已經訓練了大小為 4 的嵌入,並且電影 Twister 由向量表示:

m_Twister=[ 1.0 −0.5 0.3 −0.1 ]

使用者 Stanley 表示為:

u_Stanley=[ −0.2 1.5 −0.1 0.9 ]

我們認為 Stanley 會給 Twister 什麼評分? 我們可以將模型的輸出計算為:

m_Twister · u_Stanley 
= (1.0·−0.2)+(−0.5·1.5)+(0.3·−0.1)+(−0.1·0.9)
= −1.07

因為我們正在在評分列的中心版本上訓練,所以我們的模型輸出的比例為 0 等於訓練集中的總體平均評分(約 3.5)。 因此我們預測 Stanley 將給 Twister 3.5+(−1.07)=2.43星。

為什麼

有一個直觀的解釋,支援了以這種方式組合我們的嵌入向量的決定。 假設我們的電影嵌入空間的維度對應於以下變化的軸:

維度 1:多麼令人激動?
維度 2:多麼浪漫?
維度 3:目標受眾有多成熟?
維度 4:多麼好笑?

因此,Twister 是一部令人激動的災難電影,m1的正值為 1.0。

簡單來說,u1告訴我們“這個使用者對動作片的看法如何?”。 他們喜歡它嗎?討厭它?還是不喜歡也不討厭?

Stanley 的向量告訴,我們他是浪漫和喜劇的忠實粉絲,並且略微不喜歡動作和成熟的內容。 如果我們給他一部類似於最後一部的電影,除了它有很多浪漫元素,會怎麼樣?

m_Titanic=[ 1.0 1.1 0.3 −0.1 ]

不難預測這會如何影響我們的評分輸出。 我們給 Stanley 更多他喜歡的東西,所以他的預測評分會增加。

predicted_rating(Stanley,Titanic)
= m_Titanic·u_Stanley+3.5
=(1.0·−0.2)+(1.1·1.5)+(0.3·−0.1)+(−0.1·0.9)+3.5
=4.83 stars

注:在實踐中,我們的電影嵌入的維度的含義不會那麼明確,但我們的電影嵌入空間和使用者嵌入空間的含義從根本上聯絡在一起,這仍然是正確的:ui總是代表“這個使用者多麼喜歡某個電影,其質量由mi代表?“ (希望這也提供了一些直覺,為什麼電影嵌入空間和使用者嵌入空間在這個技巧中必須大小相同。)

實現它

建立此模型的程式碼,類似於我們在上一課中編寫的程式碼,除了我使用點積層來組合使用者和電影嵌入層的輸出(而不是連線它們,並輸入到密集層)。

movie_embedding_size = user_embedding_size = 8

# 每個例項由兩個輸入組成:單個使用者 ID 和單個電影 ID
user_id_input = keras.Input(shape=(1,), name='user_id')
movie_id_input = keras.Input(shape=(1,), name='movie_id')
user_embedded = keras.layers.Embedding(df.userId.max()+1, user_embedding_size, 
                                       input_length=1, name='user_embedding')(user_id_input)
movie_embedded = keras.layers.Embedding(df.movieId.max()+1, movie_embedding_size, 
                                        input_length=1, name='movie_embedding')(movie_id_input)

dotted = keras.layers.Dot(2)([user_embedded, movie_embedded])
out = keras.layers.Flatten()(dotted)

model = keras.Model(
    inputs = [user_id_input, movie_id_input],
    outputs = out,
)
model.compile(
    tf.train.AdamOptimizer(0.001),
    loss='MSE',
    metrics=['MAE'],
)
model.summary(line_length=88)
'''
________________________________________________________________________________________
Layer (type)                 Output Shape       Param #   Connected to                  
========================================================================================
user_id (InputLayer)         (None, 1)          0                                       
________________________________________________________________________________________
movie_id (InputLayer)        (None, 1)          0                                       
________________________________________________________________________________________
user_embedding (Embedding)   (None, 1, 8)       1107952   user_id[0][0]                 
________________________________________________________________________________________
movie_embedding (Embedding)  (None, 1, 8)       213952    movie_id[0][0]                
________________________________________________________________________________________
dot (Dot)                    (None, 1, 1)       0         user_embedding[0][0]          
                                                          movie_embedding[0][0]         
________________________________________________________________________________________
flatten (Flatten)            (None, 1)          0         dot[0][0]                     
========================================================================================
Total params: 1,321,904
Trainable params: 1,321,904
Non-trainable params: 0
________________________________________________________________________________________
'''

讓我們訓練它。

history = model.fit(
    [df.userId, df.movieId],
    df.y,
    batch_size=5000,
    epochs=20,
    verbose=0,
    validation_split=.05,
);

讓我們將這個模型隨時間的誤差,與我們在上一課中訓練的深度神經網路進行比較:

我們新的,更簡單的模型(藍色)看起來非常好。

然而,即使我們的嵌入相當小,兩種模型都會產生一些明顯的過擬合。 也就是說,訓練集上的誤差 - 實線 - 明顯好於看不見的資料。 我們將在練習中儘快解決這個問題。

你的回合

前往練習筆記本,進行矩陣分解的實踐練習。

三、使用 Gensim 探索嵌入

早些時候,我們訓練了一個模型,使用一個網路,它帶有為每個電影和使用者學習的嵌入,來預測使用者為電影提供的評分。 嵌入是強大的!但他們實際如何工作?

以前,我說嵌入捕獲了它們所代表的物件的“含義”,並發現了有用的潛在結構。 讓我們來測試吧!

查詢嵌入

讓我們載入我們之前訓練過的模型,這樣我們就可以研究它學到的嵌入權重。

import os

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import tensorflow as tf
from tensorflow import keras

input_dir = '../input/movielens-preprocessing'
model_dir = '../input/movielens-spiffy-model'
model_path = os.path.join(model_dir, 'movie_svd_model_32.h5')
model = keras.models.load_model(model_path)

嵌入權重是模型內部的一部分,因此我們必須進行一些挖掘才能訪問它們。 我們將獲取負責嵌入電影的層,並使用get_weights()方法獲取其學習的權重。

emb_layer = model.get_layer('movie_embedding')
(w,) = emb_layer.get_weights()
w.shape
# (26744, 32)

對於那麼多電影,我們的權重矩陣有 26,744 行。 每行是 32 個數字 - 我們的電影嵌入的大小。

我們來看一個示例電影向量:

w[0]
'''
array([-0.08716497, -0.25286013, -0.52679837, -0.2602235 , -0.4349191 ,
       -0.48805636, -0.30346015, -0.1416321 ,  0.08305884, -0.17578898,
       -0.36220485,  0.14578693,  0.37118354, -0.02961254, -0.063666  ,
       -0.5223456 ,  0.0526049 ,  0.47991064, -0.19034313, -0.3271599 ,
        0.32792446, -0.3794548 , -0.55778086, -0.42602876,  0.14532137,
        0.21002969, -0.32203963, -0.46950188, -0.22500233, -0.08298543,
       -0.00373308, -0.3885791 ], dtype=float32)
'''

這是什麼電影的嵌入? 讓我們載入我們的電影元資料的資料幀。

movies_path = os.path.join(input_dir, 'movie.csv')
movies_df = pd.read_csv(movies_path, index_col=0)
movies_df.head()
movieId title genres key year n_ratings mean_rating
0 0 Toy Story Adventure Animation Children Comedy Fantasy
1 1 Jumanji Adventure Children Fantasy Jumanji 1995
2 2 Grumpier Old Men Comedy Romance Grumpier Old Men 1995 12735
3 3 Waiting to Exhale Comedy Drama Romance Waiting to Exhale 1995
4 4 Father of the Bride Part II Comedy Father of the Bride Part II 1995 12161 3.064592

當然,這是《玩具總動員》! 我應該在任何地方認出這個向量。

好吧,我很滑稽。此時很難利用這些向量。 我們從未告訴模型如何使用任何特定嵌入維度。 我們只讓它學習它認為有用的任何表示。

那麼我們如何檢查這些表示是否合理且連貫?

向量相似度

測試它的一種簡單方法是,檢視嵌入空間中電影對有多麼接近或遠離。 嵌入可以被認為是智慧的距離度量。 如果我們的嵌入矩陣是良好的,它應該將類似的電影(如《玩具總動員》和《怪物史萊克》)對映到類似的向量。

i_toy_story = 0
i_shrek = movies_df.loc[
    movies_df.title == 'Shrek',
    'movieId'
].iloc[0]

toy_story_vec = w[i_toy_story]
shrek_vec = w[i_shrek]

print(
    toy_story_vec,
    shrek_vec,
    sep='\n',
)
'''
[-0.08716497 -0.25286013 -0.52679837 -0.2602235  -0.4349191  -0.48805636
 -0.30346015 -0.1416321   0.08305884 -0.17578898 -0.36220485  0.14578693
  0.37118354 -0.02961254 -0.063666   -0.5223456   0.0526049   0.47991064
 -0.19034313 -0.3271599   0.32792446 -0.3794548  -0.55778086 -0.42602876
  0.14532137  0.21002969 -0.32203963 -0.46950188 -0.22500233 -0.08298543
 -0.00373308 -0.3885791 ]
[ 0.0570179   0.5991162  -0.71662885  0.22245468 -0.40536046 -0.33602375
 -0.24281627  0.08997302  0.03362623 -0.12569055 -0.2764452  -0.12710975
  0.48197436  0.2724923   0.01551001 -0.20889504 -0.04863157  0.39106563
 -0.24811408 -0.05642252  0.24475795 -0.53363544 -0.2281187  -0.17529544
  0.21050802 -0.37807122  0.03861505 -0.27024794 -0.24332719 -0.17732081
  0.07961234 -0.39079434]
'''

逐個維度地比較,這些看起來大致相似。 如果我們想為它們的相似度分配一個數字,我們可以計算這兩個向量之間的歐氏距離。 (這是我們傳統的“烏鴉飛過的”兩點之間的距離的概念。容易在 1,2 或 3 維上進行研究。在數學上,我們也可以將它擴充套件到 32 維,雖然需要好運來視覺化它。)

from scipy.spatial import distance

distance.euclidean(toy_story_vec, shrek_vec)
# 1.4916094541549683

這與我們認為非常不同的一對電影相比如何?

i_exorcist = movies_df.loc[
    movies_df.title == 'The Exorcist',
    'movieId'
].iloc[0]

exorcist_vec = w[i_exorcist]

distance.euclidean(toy_story_vec, exorcist_vec)
# 2.356588363647461

更遠了,和我們期待的一樣。

餘弦距離

如果你看看scipy.spatial模組的文件,你會發現人們用於不同任務的距離,實際上有很多不同的衡量標準。

在判斷嵌入的相似性時,使用餘弦相似性更為常見。

簡而言之,兩個向量的餘弦相似度範圍從 -1 到 1,並且是向量之間的角度的函式。 如果兩個向量指向同一方向,則它們的餘弦相似度為 1。如果它們指向相反的方向,它為 -1。 如果它們是正交的(即成直角),則它們的餘弦相似度為 0。

餘弦距離定義為 1 減去餘弦相似度(因此範圍從 0 到 2)。

讓我們計算電影向量之間的幾個餘弦距離:

print(
    distance.cosine(toy_story_vec, shrek_vec),
    distance.cosine(toy_story_vec, exorcist_vec),
    sep='\n'
)
'''
0.3593705892562866
0.811933159828186
'''

注:為什麼在使用嵌入時常用餘弦距離? 與許多深度學習技巧一樣,簡短的答案是“憑經驗,它能用”。 在即將進行的練習中,你將進行一些實踐調查,更深入地探討這個問題。

哪部電影與《玩具總動員》最相似? 在嵌入空間中哪些電影落在 Psycho 和 Scream 之間? 我們可以編寫一堆程式碼來解決這樣的問題,但這樣做非常繁瑣。 幸運的是,已經有一個庫可以完成這類工作:Gensim。

使用 Gensim 探索嵌入

我將使用我們的模型的電影嵌入和相應電影的標題,來例項化WordEmbeddingsKeyedVectors

注:你可能會注意到,Gensim 的文件及其許多類和方法名稱都指的是詞嵌入。 雖然庫最常用於文字領域,但我們可以使用它來探索任何型別的嵌入。

from gensim.models.keyedvectors import WordEmbeddingsKeyedVectors

# 將資料集中的電影限制為至少具有這麼多評分
threshold = 100
mainstream_movies = movies_df[movies_df.n_ratings >= threshold].reset_index(drop=True)

movie_embedding_size = w.shape[1]
kv = WordEmbeddingsKeyedVectors(movie_embedding_size)
kv.add(
    mainstream_movies['key'].values,
    w[mainstream_movies.movieId]
)