深度學習進階NLP:word2vec的高速化

作者:雲不見
鏈接:https://www.yuque.com/docs/share/6ecfa369-8870-48e2-8e24-63efd3d3fab1?#
編輯:王萌

word2vec的高速化

上一篇我們講到了在神經網絡中詞向量的表示方法:最著名的就是word2vec,並且實現了CBOW模型的代碼。想要回顧的可以看這裏師妹問我:如何在7分鐘內徹底搞懂word2vec?

word2vec雖然簡單,但是的確存在一些問題,比如隨着語料庫中詞彙量的增加,計算量也隨之增加。當詞彙量達到一定程度之後, CBOW 模型的計算就會花費過多的時間。

因此,本節將對 word2vec 進行加速。

兩點改進:

  • 引入名爲Embedding 層(嵌入層)的新層;

  • 引入名爲 Negative Sampling(負採樣) 的新損失函數。

改進之後,我們會在 PTB 數據集上進行學習,實際評估一下所獲得的單詞的分佈式表示的優劣。

先回顧一下上一節的簡單CBOW模型

深度學習進階NLP:word2vec的高速化

數據在模型中的傳遞步驟如下:

1.接收 2 個單詞的上下文,以預測目標詞的概率;

2.輸入層和輸入側權重(Win)之間的矩陣乘積計算出中間層;

3.中間層和輸出側權重(Wout)之間的矩陣乘積計算每個單詞的得分

4.這些得分經過Softmax函數,得到每個單詞的出現概率;

5.將這些概率與正確解標籤進行比較(使用交叉熵誤差函數進行對比),從而計算出損失;

6.再將損失值通過反向傳播給前向網絡,進行權重Win的更新,重新回到步驟1進行迭代;

7.不斷迭代更新權重,直到交叉熵誤差的損失值小到你的要求,就停止迭代,得到最終的權重值,也就是詞向量。

這個時候,語料庫還很小,模型還hold得住。但是現實生活中可沒這麼理想。假設詞彙量有 100 萬個,CBOW 模型的中間層神經元有 100 個,那麼

深度學習進階NLP:word2vec的高速化

這中間的計算過程需要很長時間,比如

1.輸入層的 one-hot 表示和權重矩陣 Win 的乘積

2.中間層和權重矩陣 Wout 的乘積以及 Softmax 層的計算

結論先行。

對於問題一,通過引入新的 Embedding 層來解決;

對於問題二,通過引入 Negative Sampling 這一新的損失函數來解決。

代碼:

改進前的 word2vec 實現在 ch03/ simple_cbow.py(或 者 simple_skip_gram.py)中。

改進後的 word2vec 實現在 ch04/ cbow.py(或者 skip_gram.py)中。

word2vec的改進一:引入Embedding 層

深度學習進階NLP:word2vec的高速化

我們把輸入層的 one-hot 表示和權重矩陣 Win 的乘積這一個計算單獨拿出來看看它究竟做了什麼。我們知道根據簡單的CBOW模型就是矩陣乘積,但是從圖中我們可以發現,其實它就是把單詞one-hot爲1的對應權重矩陣Win的行(向量)抽取出來了。因此,矩陣乘積沒有必要。

於是我們可以創建一個從權重參數中抽取“單詞 ID 對應行(向量)”的層,稱之爲 Embedding 層。順便說一句,Embedding 來自“詞嵌入”(word embedding)這一術語。也就是說,在這個 Embedding 層存放詞嵌入(分佈式表示)。

Embedding 的代碼實現

從矩陣中取出某一行的處理是很容易實現的。假設權重 W 是二維數組。如果要從這個權重中取出某個特定的行,只需寫 W[2]或者 W[5](取出第二行或第五行)。用 Python 代碼來實現,如下所示。

>>> import numpy as np
>>> W = np.arange(21).reshape(7, 3)
>>> W
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11],
       [12, 13, 14],
       [15, 16, 17],
       [18, 19, 20]])
>>> W[2]
array([6, 7, 8])

>>> W[5]
array([15, 16, 17])

一次性提取多行的處理也很簡單,只需通過數組指定行號即可。

>>> idx = np.array([1, 0, 3, 0])
>>> W[idx]
array([[ 3,  4,  5],
       [ 0,  1,  2],
       [ 9, 10, 11],
       [ 0,  1,  2]])

正向傳播


下面,我們來實現 Embedding 層的 forward() 方法。假定用於mini-batch 處理。(common/layers.py)。

class Embedding:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.idx = None  # idx中以數組的形式保存需提取的行索引(單詞 ID)

    def forward(self, idx):
        W, = self.params
        self.idx = idx
        out = W[idx]
        return out

反向傳播


Embedding 層的正向傳播只是從權重矩陣 W 中提取特定的行,並將該特定行的神經元原樣傳給下一層。因此,在反向傳播時,從上一層(輸出側的層)傳過來的梯度將原樣傳給下一層(輸入側的層)。不過,從上一層傳來的梯度會被應用到權重梯度 dW 的特定行(idx),如圖 4-4 所示。

深度學習進階NLP:word2vec的高速化

因此,反向傳播backward()代碼如下:

def backward(self, dout):
    dW, = self.grads
    dW[...] = 0
    dW[self.idx] = dout # 不太好的方式
    return None

這裏,取出權重梯度 dW,通過 dW[...] = 0 將 dW 的元素設爲 0(並不是將 dW 設爲 0,而是保持 dW 的形狀不變,將它的元素設爲 0)。然後,將上一層傳來的梯度 dout 寫入 idx 指定的行。

但是在 idx 的元素出現重複時,會出現問題。比如,當 idx 爲 [0, 2, 0, 4] 時,就會發生圖 4-5中的問題。

深度學習進階NLP:word2vec的高速化

如圖 4-5 所示,我們將 dh 各行的值寫入 dW 中 idx 指定的位置。在這種情況下,dW 的第 0 行會被寫入兩次。這樣一來,其中某個值就會被覆蓋掉。爲了解決這個重複問題,需要進行“加法”,而不是“寫入”(請讀者考慮一下爲什麼是加法)。也就是說,應該把 dh 各行的值累加到 dW 的對應行中。下面,我們來實現正確的反向傳播。

def backward(self, dout):
    dW, = self.grads
    dW[...] = 0
    for i, word_id in enumerate(self.idx):  # 使用 for 循環將梯度累加到對應索引上
        dW[word_id] += dout[i]
    # 或者
    # np.add.at(dW, self.idx, dout)  # np.add.at(A, idx, B) 將B加到A上,idx指定A中需進行加法的行。
    return None

通常情況下,NumPy 的內置方法比 Python 的 for循環處理更快。這是因爲 NumPy 的內置方法在底層做了高速化和提高處理效率的優化。因此,上面的代碼如果使用 np.add.at()來實現,效率會比使用 for循環處理高得多。

關於 Embedding 層的改進就介紹到這裏。現在,我們可以將 word2vec(CBOW 模型)的實現中的輸入側的 MatMul 層換成 Embedding 層。這樣一來,既能減少內存使用量,又能避免不必要的計算。

word2vec的改進二:引入新損失函數:負採樣

word2vec 的另一個瓶頸在於中間層之後的處理,即矩陣乘積和 Softmax 層的計算。

這裏我們採用負採樣(negative sampling)的方法來解決。使用 Negative Sampling 替代 Softmax,無論詞彙量有多大,都可以使計算量保持較低或恆定。

深度學習進階NLP:word2vec的高速化

如圖 4-6 所示,輸入層和輸出層有 100 萬個神經元。在上一節中,我們通過引入 Embedding 層,節省了輸入層中不必要的計算。剩下的問題就是中間層之後的處理。此時,在以下兩個地方還需要很多計算時間。

  • 中間層的神經元和權重矩陣(Wout)的乘積

  • Softmax 層的計算

什麼是負採樣?


負採樣:這個方法的關鍵思想在於用二分類(binary classification)去擬合多分類(multiclass classification),這是理解負採樣的重點。

上述問題中,我們處理的都是多分類問題。拿剛纔的例子來說,我們把它看作了從 100 萬個單詞中選擇 1 個正確單詞的任務。那麼,可不可以將這個問題處理成二分類問題呢?更確切地說,我們是否可以用二分類問題來擬合這個多分類問題呢?

二 分 類 處 理 的 是 答 案 爲“Yes/No”的 問 題。諸 如,“這個數字是7 嗎?”“這 是 貓 嗎?”“目 標 詞 是 say 嗎?”等,這 些 問 題 都 可 以 用“Yes/No”來回答。

對於“當上下文是 you 和 goodbye 時,目標詞是什麼?”這個問題,神經網絡可以給出正確答案。

現在,我們來考慮如何將多分類問題轉化爲二分類問題。爲此,我們先考察一個可以用“Yes/No”來回答的問題。比如,讓神經網絡來回答“當上下文是 you 和 goodbye 時,目標詞是 say 嗎?”這個問題,這時輸出層只需要一個神經元即可。可以認爲輸出層的神經元輸出的是 say 的得分。

深度學習進階NLP:word2vec的高速化

如圖 4-7 所示,輸出層的神經元僅有一個。因此,要計算中間層和輸出側的權重矩陣的乘積,只需要提取 say 對應的列(單詞向量),並用它與中間層的神經元計算內積即可。這個計算的詳細過程如圖 4-8 所示。

深度學習進階NLP:word2vec的高速化

如圖 4-8 所示,輸出側的權重 Wout 中保存了各個單詞 ID 對應的單詞向量。此處,我們提取 say 這個單詞向量,再求這個向量和中間層神經元的內積,這就是最終的得分。

原來輸出層是以全部單詞爲對象進行計算的。這裏,我們僅關注單詞 say,計算它的得分。然後,使用 sigmoid 函數將其轉化爲概率。

-

在多分類的情況下,輸出層使用Softmax 函數將得分轉化爲概率,損失函數使用交叉熵誤差。

在二分類的情況下,輸出層使用sigmoid 函數,損失函數也使用交叉熵誤差。

下面我們從層的角度來看看CBOW模型:

多分類

深度學習進階NLP:word2vec的高速化

二分類

深度學習進階NLP:word2vec的高速化

當答案是“Yes”時,向 Sigmoid with Loss 層輸入 1。

當答案是“No”時,向 Sigmoid with Loss 層輸入 0。

爲了便於理解模型後半部分,我們把它單獨擰出來看,且將圖 4-12 中的 Embedding 層和 dot運算(內積)合併起來處理,可以簡化成圖 4-13。

深度學習進階NLP:word2vec的高速化

Embedding Dot 層的實現

class EmbeddingDot:
    def __init__(self, W):
        self.embed = Embedding(W)  # 保存 Embedding 層
        self.params = self.embed.params  # 保存參數
        self.grads = self.embed.grads  # 保存梯度
        self.cache = None  # 保存正向傳播時的計算結果

    def forward(self, h, idx):  # idx是單詞ID列表,通過idx實現mini-batch處理,得出Wout
        target_W = self.embed.forward(idx)
        out = np.sum(target_W * h, axis=1)  # axis=1表示按第一維度,也就是按行進行求和
        self.cache = (h, target_W)
        return out

    def backward(self, dout):
        h, target_W = self.cache
        dout = dout.reshape(dout.shape[0], 1)
        dtarget_W = dout * h
        self.embed.backward(dtarget_W)
        dh = dout * target_W
        return dh

我們用具體值來舉個例子:

深度學習進階NLP:word2vec的高速化

idx=[0 3 1] 表示取第0行、3行和1行。(實現mini-batch處理,同時處理提高效率)

target_W * h 表示內積,也就是對應元素相乘。

out 表示對結果逐行(axis=1)進行求和。

以上就是對 Embedding Dot 層的正向傳播的介紹。反向傳播以相反的順序傳播梯度,這裏我們省略對其實現的說明(並不是特別難,請大家自己思考)。

負採樣


至此,我們實現了多分類轉化爲二分類問題,問題就解決了嘛?不是的。如上所述,我們只考慮了正確解“say”,而沒有考慮錯誤解的情況。也就是說,我們目前僅學習了正例(正確答案),還不確定負例(錯誤答案)會有怎樣的結果。

如果此時模型有“好的權重”,則 Sigmoid 層的輸出(概率)將接近 1。

深度學習進階NLP:word2vec的高速化

當前的神經網絡只是學習了正例 say,但是對 say 之外的負例一無所知。而我們真正要做的事情是,對於正例(say),使 Sigmoid 層的輸出接近 1;對於負例(say 以外的單詞),使 Sigmoid 層的輸出接近 0。

深度學習進階NLP:word2vec的高速化

比如,當上下文是 you 和 goodbye 時,我們希望目標詞是 hello(錯誤答案)的概率較低。也就是越接近0越好。

爲了把多分類問題處理爲二分類問題,對於“正確答案”(正例)和“錯誤答案”(負例),都需要能夠正確地進行分類(二分類)。因此,需要同時考慮正例和負例。

但是除去say以外的詞都是負例,我們都需要考慮嘛?肯定不是啊!那樣就違背了我們想解決計算量大這個問題的初衷。所以我們會用近似的方法,選擇若干個(5 個或者 10 個)負例去計算。這就是負採樣方法的含義。

最後,將正例和採樣出來的負例的損失加起來就是最終的損失。

負採樣的採樣方法


那麼如何抽取負例呢?基於語料庫的統計數據進行採樣的方法比隨機抽樣要好。也就是說,語料庫中經常出現的單詞容易被抽到,語料庫中不經常出現的單詞難以被抽到。

深度學習進階NLP:word2vec的高速化

基於語料庫中各個單詞的出現次數求出概率分佈後,只需根據這個概率分佈進行採樣就可以了。

處理稀有單詞的重要性較低。相反,處理好高頻單詞才能獲得更好的結果。

下面,我們使用 Python 來實現基於概率分佈的採樣。可以使用NumPy 的 np.random.choice() 方法進行採樣。

# 基於概率分佈進行採樣
>>> words = ['you', 'say', 'goodbye', 'I', 'hello', '.']
>>> p = [0.5, 0.1, 0.05, 0.2, 0.05, 0.1]
>>> np.random.choice(words, p=p)
'you'

word2vec 中提出的負採樣對剛纔的概率分佈增加了一個步驟。如式 (4.4)所示,對原來的概率分佈取 0.75 次方。

深度學習進階NLP:word2vec的高速化

P(wi) 表示第 i 個單詞的概率。式 (4.4) 只是對原來的概率分佈的各個元素取 0.75 次方。爲了使變換後的概率總和仍爲 1,分母需要變成“變換後的概率分佈的總和”。

爲什麼這麼做呢?這是爲了防止低頻單詞被忽略。通過取 0.75 次方,低頻單詞的概率將稍微變高。我們來看一個具體例子,如下所示。

>>> p = [0.7, 0.29, 0.01]
>>> new_p = np.power(p, 0.75)
>>> new_p /= np.sum(new_p)
>>> print(new_p)
[ 0.64196878  0.33150408  0.02652714]>>> p = [0.7, 0.29, 0.01]
>>> new_p = np.power(p, 0.75)
>>> new_p /= np.sum(new_p)
>>> print(new_p)
[ 0.64196878  0.33150408  0.02652714]

通過這種方式,使得低頻單詞稍微更容易被抽到。此外,0.75 這個值並沒有什麼理論依據,也可以設置成0.75 以外的值。

因此,負採樣的步驟就是:

  • 從語料庫生成單詞的概率分佈,在取其 0.75 次方

  • 使用 np.random.choice() 對負例進行採樣。

具體實現在 ch04/negative_sampling_layer.py 的UnigramSampler 類中。這裏僅簡單說明UnigramSampler 類的使用方法,具體實現可參考附帶代碼。

unigram 是“1 個(連 續)單 詞”的 意 思。同 樣 地,bigram 是“2個 連 續 單 詞”的 意 思,trigram 是“3 個 連 續 單 詞”的 意 思。這 裏使 用 UnigramSampler這 個 名 字,是 因 爲 我 們 以 1 個 單 詞 爲 對 象 創建 概 率 分 布。如 果 是 bigram,則 以 (‘you’, ‘say’)、(‘you’, ‘goodbye’)……這樣的 2 個單詞的組合爲對象創建概率分佈。

UnigramSampler 類有 3 個參數,分別是單詞 ID 列表格式的 corpus、對概率分佈取的次方值 power(默認值是0.75)和負例的採樣個數 sample_size。

UnigramSampler 類有 get_negative_sample(target) 方法,該方法以參數 target 指定的單詞 ID 爲正例,對其他的單詞 ID 進行採樣。

# 指定三個參數的具體值
corpus = np.array([0, 1, 2, 3, 4, 1, 2, 3])
power = 0.75
sample_size = 2

sampler = UnigramSampler(corpus, power, sample_size)
target = np.array([1, 3, 0])
negative_sample = sampler.get_negative_sample(target)
print(negative_sample)
# [[0 3]
#  [1 2]
#  [2 3]]

這裏,將 [1, 3, 0] 這 3 個數據的 mini-batch 作爲正例。此時,對各個數據採樣 2 個負例。第 1 個數據的負例是 [0, 3],第 2 個是 [1, 2],第 3 個是 [2, 3]。這樣一來,我們就完成了負採樣。

負採樣的實現


接下來我們要實現負採樣,我們把它實現爲 NegativeSamplingLoss 類。(ch04/negative_sampling_layer.py)。

class NegativeSamplingLoss:
    def __init__(self, W, corpus, power=0.75, sample_size=5):
        self.sample_size = sample_size  # 採樣負例的數量
        self.sampler = UnigramSampler(corpus, power, sample_size)
        self.loss_layers = [SigmoidWithLoss() for _ in range(sample_size + 1)]  # sample_size + 1是因爲要生成一個正例用的層和 sample_size 個負例用的層
        self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size + 1)]
        self.params, self.grads = [], []
        for layer in self.embed_dot_layers:
            self.params += layer.params
            self.grads += layer.grads    

    # 正向傳播  
    def forward(self, h, target):  # h=中間層的神經元;target=正例目標詞
    batch_size = target.shape[0]
    negative_sample = self.sampler.get_negative_sample(target)  # 使用 self.sampler 採樣負例

    # 正例的正向傳播,假設loss_layers[0] 和 embed_dot_layers[0] 是處理正例的層
    score = self.embed_dot_layers[0].forward(h, target)
    correct_label = np.ones(batch_size, dtype=np.int32) # 正例標籤是1
    loss = self.loss_layers[0].forward(score, correct_label)

    # 負例的正向傳播
    negative_label = np.zeros(batch_size, dtype=np.int32) # 負例標籤是0
    for i in range(self.sample_size):
        negative_target = negative_sample[:, i]
        score = self.embed_dot_layers[1 + i].forward(h, negative_target)
        loss += self.loss_layers[1 + i].forward(score, negative_label)

    return loss  # 正例、負例的損失和

  # 反向傳播,只需要以與正向傳播相反的順序調用各層的backward()函數即可
    def backward(self, dout=1):
        dh = 0
        for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):
            dscore = l0.backward(dout)
            dh += l1.backward(dscore)
        return dh

改進版word2vec的學習

到目前爲止,我們首先實現 Embedding層,又實現了負採樣。現在我們進一步來實現進行了這些改進的神經網絡,並在 PTB 數據集上進行學習,以獲得更加實用、真實的單詞的分佈式表示。

CBOW模型的實現


這裏,我們將改進上一節的簡單的 SimpleCBOW 類,來實現改進版本的 CBOW 模型。改進之處在於使用Embedding 層和 Negative Sampling Loss 層。此外,我們將上下文部分擴展爲可以處理任意的窗口大小。


import sys
sys.path.append('..')
import numpy as np
from common.layers import Embedding
from ch04.negative_sampling_layer import NegativeSamplingLoss

class CBOW:
    # 先進行初始化
    def __init__(self, vocab_size, hidden_size, window_size, corpus): #(詞彙量,中間層的神經元個數,上下文的大小,單詞ID列表)
        V, H = vocab_size, hidden_size

        # 初始化權重
        W_in = 0.01 * np.random.randn(V, H).astype('f')
        W_out = 0.01 * np.random.randn(V, H).astype('f')

        # 生成層
        self.in_layers = []
        for i in range(2 * window_size):  # 創建2 * window_size個Embedding 層
            layer = Embedding(W_in)  # 使用Embedding層
            self.in_layers.append(layer)
        self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5)

        # 將所有的權重和梯度整理到列表中
        layers = self.in_layers + [self.ns_loss]
        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads

        # 將單詞的分佈式表示W_in設置爲成員變量
        self.word_vecs = W_in
    # 正向傳播    
    def forward(self, contexts, target):  # 用單詞ID表示contexts, target
        h = 0
        for i, layer in enumerate(self.in_layers):
            h += layer.forward(contexts[:, i])
        h *= 1 / len(self.in_layers)
        loss = self.ns_loss.forward(h, target)
        return loss

    # 反向傳播
    def backward(self, dout=1):
        dout = self.ns_loss.backward(dout)
        dout *= 1 / len(self.in_layers)
        for layer in self.in_layers:
            layer.backward(dout)
        return None    

改進版的 CBOW 類的實現如下所示。(ch04/cbow.py)。

用單詞ID表示contexts, target的例子:

深度學習進階NLP:word2vec的高速化

可以看出,contexts 是一個二維數組,target 是一個一維數組,這樣的數據被輸入 forward(contexts, target) 中

CBOW模型的學習代碼


建立完層的初始化、正向傳播、反向傳播,接下來,我們來實現 CBOW 模型的學習部分,也就是訓練部分。(ch04/train.py)

import sys
sys.path.append('..')
import numpy as np
from common import config
# 在用GPU運行時,請打開下面的註釋(需要cupy)
# ===============================================
# config.GPU = True
# ===============================================
import pickle
from common.trainer import Trainer
from common.optimizer import Adam
from cbow import CBOW
from common.util import create_contexts_target, to_cpu, to_gpu
from dataset import ptb

# 設定超參數
window_size = 5 # 窗口大小爲 5;一般而言,當窗口大小爲 2~10、中間層的神經元個數(詞向量的維數)爲50~500時,結果會比較好。
hidden_size = 100
batch_size = 100
max_epoch = 10

# 讀入數據,使用PTB 語料庫比之前要大得多,因此學習需要很長時間(半天左右)。
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)

contexts, target = create_contexts_target(corpus, window_size)
if config.GPU:
    contexts, target = to_gpu(contexts), to_gpu(target)

# 生成模型等
model = CBOW(vocab_size, hidden_size, window_size, corpus)
optimizer = Adam()
trainer = Trainer(model, optimizer)

# 開始學習
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()

# 保存必要數據,以便後續使用
word_vecs = model.word_vecs
if config.GPU:
    word_vecs = to_cpu(word_vecs)
params = {}
params['word_vecs'] = word_vecs.astype(np.float16)
params['word_to_id'] = word_to_id
params['id_to_word'] = id_to_word
pkl_file = 'cbow_params.pkl'  # 使用pickle功能進行文件保存
with open(pkl_file, 'wb') as f: 
    pickle.dump(params, f, -1)

ch04/cbow_params.pkl中提供了學習好的參數。如果不想等學習結束,可以使用本書提供的學習好的參數。根據學習環境的不同,學習到的權重數據也不一樣。這是由權重初始化時用到的隨機初始值、mini-bath 的隨機選取,以及負採樣的隨機抽樣造成的。因爲這些隨機性,最後得到的權重在各自的環境中會不一樣。不過宏觀來看,得到的結果(趨勢)是類似的。

CBOW模型的評價


現在,我們來評價一下上一節學習到的單詞的分佈式表示。使用 most_similar() 函數,該函數是用於顯示和所給詞最接近的單詞(ch04/eval.py)。


import sys
sys.path.append('..')
from common.util import most_similar
import pickle

pkl_file = 'cbow_params.pkl'
with open(pkl_file, 'rb') as f:
    params = pickle.load(f)
    word_vecs = params['word_vecs']
    word_to_id = params['word_to_id']
    id_to_word = params['id_to_word']

querys = ['you', 'year', 'car', 'toyota']
for query in querys:
    most_similar(query, word_to_id, id_to_word, word_vecs, top=5) # 打印出最接近的5個詞

運行上述代碼,可以得到:


[query] you
 we: 0.610597074032
 someone: 0.591710150242
 i: 0.554366409779
 something: 0.490028560162
 anyone: 0.473472118378

[query] year
 month: 0.718261063099
 week: 0.652263045311
 spring: 0.62699586153
 summer: 0.625829637051
 decade: 0.603022158146

[query] car
 luxury: 0.497202396393
 arabia: 0.478033810854
 auto: 0.471043765545
 disk-drive: 0.450782179832
 travel: 0.40902107954

[query] toyota
 ford: 0.550541639328
 instrumentation: 0.510020911694
 mazda: 0.49361255765
 bethlehem: 0.474817842245
 nissan: 0.474622786045

我們看一下結果。在查詢 you 的情況下,近似單詞中出現了人稱代詞 i(= I)和 we 等。查詢 year,可以看到 month、week 等表示時間區間的具有相同性質的單詞。然後,查詢 toyota,可以得到 ford、mazda 和 nissan 等表示汽車製造商的詞彙。從這些結果可以看出,由CBOW 模型獲得的單詞的分佈式表示具有良好的性質。

CBOW模型的應用


關於 word2vec 的原理和實現,差不多都介紹完了。接下來我們看看它在實際應用中的例子。

在自然語言處理領域,單詞的分佈式表示之所以重要,原因就在於遷移學習(transfer learning)。遷移學習是指在某個領域學到的知識可以被應用於其他領域。

在解決自然語言處理任務時,一般不會使用 word2vec 從零開始學習詞向量,而是先在大規模語料庫(Wikipedia、Google News 等文本數據)上學習,然後將學習好的詞向量應用於某個單獨的任務。

比如,在文本分類、文本聚類、詞性標註和情感分析等自然語言處理任務中,第一步的單詞向量化工作就可以使用學習好的單詞的分佈式表示。在幾乎所有類型的自然語言處理任務中,單詞的分佈式表示都有很好的效果!

將單詞和文檔轉化爲固定長度的向量是非常重要的。因爲如果可以將自然語言轉化爲向量,就可以使用常規的機器學習方法(神經網絡、SVM等),如圖 4-21 所示。

深度學習進階NLP:word2vec的高速化

在圖 4-21 的流程中,單詞的分佈式表示的學習和機器學習系統的學習通常使用不同的數據集獨立進行。

比如,單詞的分佈式表示使用Wikipedia 等通用語料庫預先學習好,然後機器學習系統(SVM 等)再使用針對當前問題收集到的數據進行學習。但是,如果當前我們面對的問題存在大量的學習數據,則也可以考慮從零開始同時進行單詞的分佈式表示和機器學習系統的學習。

下面讓我們結合具體的例子來說明一下單詞的分佈式表示的使用方法。

比如我們要開發一個可以對用戶發來的郵件(吐槽等)自動進行分類的系統。根據郵件的內容將用戶情感分爲 3 類。如果可以正確地對用戶情感進行分類,就可以按序瀏覽表達不滿的用戶郵件。如此一來,或許可以發現應用的致命問題,並儘早採取應對措施,從而提高用戶的滿意度。

深度學習進階NLP:word2vec的高速化

要開發郵件自動分類系統,首先需要從收集數據(郵件)開始。

我們收集用戶發送的郵件,並人工對郵件進行標註,打上表示3類情感的標籤(positive/neutral/negative)。

標註工作結束後,用學習好的word2vec 將郵件轉化爲向量。

然後,將向量化的郵件及其情感標籤輸入某個情感分類系統(SVM 或神經網絡等)進行學習。

如本例所示,可以基於單詞的分佈式表示將自然語言處理問題轉化爲向量,這樣就可以利用常規的機器學習方法來解決問題。

詞向量的評價方法


使用 word2vec,我們得到了單詞的分佈式表示。那麼,我們應該如何評價我們得到的分佈式表示是好的呢?

此時,經常使用的評價指標有“相似度”和“類推問題”

單詞相似度的評價通常使用人工創建的“單詞相似度評價集”來評估。用 0 ~ 10 的分數人工地對單詞之間的相似度打分。然後,比較人給出的分數和 word2vec 給出的餘弦相似度,考察它們之間的相關性。

類推問題的評價是指,諸如“king : queen = man : ?”這樣的類推問題,根據正確率測量單詞的分佈式表示的優劣。比如,論文 [27] 中給出了一個類推問題的評價結果,其部分內容如圖 4-23 所示。

深度學習進階NLP:word2vec的高速化

在圖 4-23 中,以 word2vec 的模型、單詞的分佈式表示的維數和語料庫的大小爲參數進行了比較實驗,結果在右側的 3 列中。

  • Semantics列顯示的是推斷單詞含義的類推問題(像“king : queen = actor : actress”這樣詢問單詞含義的問題)的正確率

  • Syntax 列是詢問單詞形態信息的問題,比如“bad : worst = good : best”。

由圖 4-23 可知:

  • 模型不同,精度不同(根據語料庫選擇最佳的模型)

  • 語料庫越大,結果越好(始終需要大數據)

  • 單詞向量的維數必須適中(太大會導致精度變差)

但是,單詞的分佈式表示的優劣評價指標取決於待處理問題的具體情況,比如應用的類型或語料庫的內容等。也就是說,不能保證類推問題的評價高,目標應用的結果就一定好。

總結

本節我們基於簡單CBOW模型出現的計算問題,進行了如下改進:

  • 實現了 Embedding 層:保存單詞的分佈式表示,在正向傳播時,提取單詞 ID對應的向量

  • 引入負採樣:負採樣通過僅關注部分單詞實現計算的高速化。

能夠實現加速的根本原因:

  • 利用“部分”數據而不是“全部”數據

  • 使用近似計算來加速(比如負採樣,只抽取部分負例)

word2vec 的遷移學習能力非常重要,它的單詞的分佈式表示可以應用於各種各樣的自然語言處理任務,基本上是所有NLP任務的基礎。

參考文獻

[27] Pennington, Jeffrey, Richard Socher, Christopher D. Manning.Glove: Global Vectors for Word Representation[J]. EMNLP. Vol.14. 2014.


每天進步一丟丟

評估指標——準確率(Accuracy)的侷限性

準確率是分類問題中最簡單也是最直觀的評價指標,但存在明顯的缺陷。當不同類別的樣本比例非常不均衡時,佔比大的類別往往成爲影響準確率的最主要因素。


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