skip-gram 是 Mikolov 等人 2013 年初提出的模型,訓練目標是找出一組詞向量,好用一個詞去預測它周圍出現的那些詞。同年稍晚另一篇論文處理的是怎麼把它訓練得動,開頭第一句就是回頭指自己那個「recently introduced」的模型,下面那段負面樣本的說法出自那裡。這篇練習照 TensorFlow 官方範例走。

把字詞轉成 word embedding

要在字詞中找到他們之間的某種關聯,而不只是分散無意義的符號代表。

做這個問題的核心概念是: 「假設兩個不同句子中的詞上下文相同,則代表兩個詞的語意相近。」

今天要來使用 skip-gram 模型,一個類似二元分類的方式 (判斷像或是不像)。一開始也同之前的問題,先做數據處理。

  1. 計算出現數量[(most count word1, n1), (second word2, n2)]
  2. 文字轉成向量

例如:The actual code for this tutorial is very short

生成的 skip-gram pairs 示意:

  • ([the, code], actual), ([actual, for], code), …
  • (actual, the), (actual, code), (code, actual), …

在這之間都會給他編號,轉化為 (10, 20), (10, 30), (30, 10), (30, 40) ... 的形式。

用到 nce_loss,目前我還不是非常熟練,概念上是讓目標詞的機率越高越好,並讓其餘 K 個負面樣本 (negative samples) 的機率降低。

經典案例: king - queen = man - woman ==> king - queen + woman = man

給 queen 加上負號,並取不要的值,我想是這種感覺吧?

nce_loss 裡的負面樣本

照 skip-gram 的定義寫下來,模型每看到一組 pair,要回答的是一道選擇題,選項是整張詞彙表,答案只有一個。機率得加總成一,分母就要把詞彙表裡每個詞的分數都算過一遍。詞彙表只有幾十個詞的時候這不算什麼,換成真的語料就不是這樣,詞彙表幾萬個詞,pair 動輒幾百萬組,每一組都從頭掃到尾,訓練跑不完。

nce_loss 換掉的就是這個分母。它不去問「是幾萬個裡的哪一個」,改問一連串是非題:這組(輸入詞、正確答案)是真的嗎?這組(輸入詞、抽出來的詞)是假的嗎?正例一組,抽出來的負例 K 組,每一題都只是一次二元判斷,一步要動到的參數也就從整張表縮成 K+1 個詞。前面那句「讓目標詞的機率越高越好,並讓其餘 K 個負面樣本的機率降低」,講的就是這個。

K 要抽幾個,論文給的經驗值是小訓練資料 5 到 20,大資料可以低到 2 到 5。這份練習呼叫的是 tf.nn.nce_loss(W, b, Y_, embed, 12, len_dic),第五個參數 12 是 K,第六個是詞彙表大小。語料是十幾句關於貓和狗的短句,數完出現次數,詞彙表不到 30 個詞;一步抽 12 個負面樣本下去,沒被碰到的詞也只剩十幾個。

抽的時候不是均勻亂抽。nce_loss 預設的抽樣器照編號跑 log-uniform 分布,編號小的被抽中的機率高,編號大的低,文件因此附了一條要求:「By default this uses a log-uniform (Zipfian) distribution for sampling, so your labels must be sorted in order of decreasing frequency to achieve good results.」資料處理的第一步先數出現次數、再照次數由高到低給編號,那個順序就是這條要求要的東西。抽出來的負面樣本因此偏向常見詞,跟它們在語料裡的比例對得上。

函式名字裡的 NCE 是 noise contrastive estimation,Gutmann 和 Hyvärinen 提出、Mnih 和 Teh 拿去做語言模型,論文寫著「NCE posits that a good model should be able to differentiate data from noise by means of logistic regression」。同一篇論文另外定義了一個更省的版本叫 negative sampling,差別在 NCE 要用到噪音分布的數值機率,negative sampling 只要樣本本身。TensorFlow 這個函式算的是前者,抽負面樣本的動作兩邊一樣。

結果

會把相似的詞分的近一些:

tf_word2vec

原版 tensorflow 範例有用上 sklearnTSNE 來做降維,在很多地方都比 PCA 效果好。

(這裡的 embeddings 只開了兩維,出來就是平面座標,直接丟給 matplotlib 畫,所以沒走降維那一步;圖上標的是出現次數排前十的詞。)

一個詞一個向量

練習跑完,embeddings 這張表每一列對應一個詞,要用的時候拿編號去查。查表這個動作跟詞出現在哪一句話裡無關,所以「蘋果」在「今天買了兩顆蘋果」和「蘋果發表新手機」裡拿到的會是同一串數字,一個詞在這種模型裡只有一個位置。

要讓同一個詞在不同句子裡落到不同位置,向量得改成由整段話算出來,也就是現在的 embedding 模型在做的事:向量由一個函式算出來,輸入是整段文字,語意搜尋和 RAG 底下用的都是這種。2018 年的 ELMo 論文就把「how these uses vary across linguistic contexts (i.e., to model polysemy)」列成要處理的目標之一。至於上面那句核心假設,上下文相同的詞語意相近,目前這些模型還是照著這條在學。

My Github

系列文章