[譯] Pytorch 官方教程 詞嵌入:編碼詞語與語義

原文傳送門:WORD EMBEDDINGS: ENCODING LEXICAL SEMANTICS

[譯] Pytorch 官方教程 詞嵌入:編碼詞語與語義

WORD EMBEDDINGS: ENCODING LEXICAL SEMANTICS

詞嵌入指的是:將用戶詞表中每一個單詞與一個實數對應,並嵌入到一個密度向量中。在自然語言處理中,絕大多數情況研究的特徵都是單詞。然而,應該怎樣在計算機中表示一個單詞呢?一種方案是存儲單詞每個字母對應的ascii碼,但這隻能讓我們知道我們存儲的這個單詞由哪些字母組成,並不能表達這個單詞具體的含義(或許能從詞綴推斷出詞性、從大小寫推斷出屬性,但這遠遠不夠)。而且,憑什麼可以將這些表示組合在一起?

我們設計的神經網絡往往是V|V|(詞彙表的長度)維的,但是卻需要從神經網絡中得到低維、密集的輸出(譬如需要預測的標籤只有幾個),我們要怎樣從將數據從高維空間轉移到低維空間呢?

不妨試試用one-hot編碼來代替ascii碼?即,對某個單詞w,按以下方式表示:
[0,0,,1,,0,0]|V| elements \overbrace{\left[ 0, 0, \dots, 1, \dots, 0, 0 \right]}^\text{|V| elements}
其中,每個單詞都只含有1個’1’(剩餘爲0),而且每個單詞的’1’的位置都不同。

上述方法不僅佔用了巨量的空間,而且還有一個致命的弊端:這種方法將每個單詞標記爲獨立的實體,認爲他們之間沒有關聯。

我們真正想要的,是使用一些標記方法,來標記詞與詞之間的相似關係。難以理解?且看以下示例。

假設我們正在建立一個語言模型,假設有以下語句:

  • The mathematician ran to the store.
  • The physicist ran to the store.
  • The mathematician solved the open problem.

在我們的訓練數據中。

現在,我們讀取到了一個在訓練數據中從未見過的語句:

  • The physicist solved the open problem.

我們的語言模型在這一語句上也許能表現不錯,但如果能夠將以下兩個事實也考慮進去,相信將會更好:

  • 我們在訓練語句中,發現mathematician以及physicist在句中是相同的成分,換句話說,他們有着一定的語義關係。
  • 在新語句中,雖然看到的是physicist,但是我們認爲mathematician在句中的成分是和physicist一致的。

由此,推斷出physicist在新語句中的擬合度很好。

這正是上述標記詞與詞之間的相似關係的真實含義:相似關係指的是語義上的相似,而不是指字符拼寫上相似。

這一技術在訓練數據和未知的數據之間建立了聯繫,能有效克服語言學(文本)數據的稀疏性。當然,這基於一個基本假設:出現在相似上下文之中的單詞在語義上相關。這也被稱爲分佈式假設


稠密詞嵌入

Getting Dense Word Embeddings

如何獲得詞嵌入呢?換句話說,我們具體要如何將語義的相似性進行編碼?

或許我們可以考慮一些語義屬性。例如,is able to run屬性:mathematicians 和 physicists 都能 “run”,因此二者在該屬性上都標記爲高分。再考慮一些屬性,並且爲這些屬性對不同的單詞進行打分。

如果將每個屬性視爲一個維度,那麼我們能將每個單詞表示爲一個向量(詞嵌入向量):
qmathematician=[2.3can run,9.4likes coffee,5.5majored in Physics,] q_\text{mathematician} = \left[ \overbrace{2.3}^\text{can run}, \overbrace{9.4}^\text{likes coffee}, \overbrace{-5.5}^\text{majored in Physics}, \dots \right]

qphysicist=[2.5can run,9.1likes coffee,6.4majored in Physics,] q_\text{physicist} = \left[ \overbrace{2.5}^\text{can run}, \overbrace{9.1}^\text{likes coffee}, \overbrace{6.4}^\text{majored in Physics}, \dots \right]

由此,我們得到了度量單詞相似性的一個方法:
Similarity(physicist,mathematician)=qphysicistqmathematician \text{Similarity}(\text{physicist}, \text{mathematician}) = q_\text{physicist} \cdot q_\text{mathematician}
按照向量長度標準化後:
Similarity(physicist,mathematician)=qphysicistqmathematicianqphysicistqmathematician=cos(ϕ) \text{Similarity}(\text{physicist}, \text{mathematician}) = \frac{q_\text{physicist} \cdot q_\text{mathematician}} {\| q_\text{physicist} \| \| q_\text{mathematician} \|} = \cos (\phi)
其中,ϕ\phi 標記兩個向量的夾角。這樣一來,極度相似的單詞(他們的詞嵌入向量指向幾乎相同的方向)的similarity值爲1,極度不相似的單詞(他們的詞嵌入向量指向幾乎相反的方向)的similarity值爲-1,

現在回顧one-hot編碼,可以將其看成詞嵌入向量的一種特殊情況,每個單詞的相似度爲0,且爲每個單詞單獨分配了一個唯一的語義屬性。

同時,我們稱新定義的向量是稠密的,也就是說這些向量的元素絕大多數不爲0。

然而, 這些新定義的向量有一個大問題:我們能爲相似性的度量,定義成千上萬個不同的語義屬性,那麼到底如何爲不同的屬性打分呢?

深度學習的核心思想是使計算機利用神經網絡自動學習特徵的含義,而不是需要程序員來手動設計。那麼,爲何不將詞嵌入作爲模型參數,讓詞嵌入在訓練過程中自動更新迭代呢?

這正是我們要做的:我們將設計一些神經網絡在原則上能夠進行學習的潛在語義屬性。注意,詞嵌入可能是難以解釋甚至不可解釋的。也就是說,雖然在上述例子中,我們能通過手工標記的方法知道mathematicians以及physicists都喜歡咖啡,但是如果我們用一個神經網絡去學習這些詞嵌入,並且發現mathematicians以及physicists的詞嵌入在某個維度上都有着很高的分數,我們也不清楚這個維度到底意味着什麼(即不明該維度的語義)。他們在潛在語義屬性上相似,但這對我們來說不可解釋。

總而言之,詞嵌入表示的是單詞語義,能夠高效地編碼與手頭任務相關的語義信息。除了語義信息,還能夠嵌入其他的信息:詞性標籤、語義分析樹、甚至任意信息!特徵嵌入的思想是這一領域的核心。


Pytorch中的詞嵌入

Word Embeddings in Pytorch

(部分內容已省略)

在定義詞嵌入時,我們需要爲每一個單詞定義一個索引,這些索引將會成爲一個查詢表中的鍵值。

換句話說,詞嵌入將被存儲爲一個$|V|×D $的矩陣,D爲詞嵌入的維度。我們爲某一單詞定義的索引i即表示該單詞在詞嵌入矩陣中的第i行。下文中,從單詞到其索引的映射將被命名爲 word_to_ix

Pytorch中爲詞嵌入提供服務的模塊是 torch.nn.Embeddingtorch.nn.Embedding接受兩個參數:詞彙表長度、詞嵌入維度

# Author: Robert Guthrie
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)
word_to_ix = {"hello": 0, "world": 1}
embeds = nn.Embedding(2, 5)  # 2 words in vocab, 5 dimensional embeddings(詞彙表長度爲2,嵌入維度爲5)
lookup_tensor = torch.tensor([word_to_ix["hello"]], dtype=torch.long)
hello_embed = embeds(lookup_tensor)
print(hello_embed)

Out:

tensor([[ 0.6614, 0.2669, 0.0617, 0.6213, -0.4519]],
grad_fn=)


示例:N-Gram 語言模型

An Example: N-Gram Language Modeling

回顧n-gram模型,給定一個單詞序列 w,我們需要計算:
P(wiwi1,wi2,,win+1) P(w_i | w_{i-1}, w_{i-2}, \dots, w_{i-n+1} )
其中 wiw_i 是w中的第i個單詞。

在本例中,我們將計算一些訓練樣本的損失函數然後使用反向傳播來更新模型參數

CONTEXT_SIZE = 2
EMBEDDING_DIM = 10

# We will use Shakespeare Sonnet 2
# 使用莎士比亞的《十四行詩》2
test_sentence = """When forty winters shall besiege thy brow,
And dig deep trenches in thy beauty's field,
Thy youth's proud livery so gazed on now,
Will be a totter'd weed of small worth held:
Then being asked, where all thy beauty lies,
Where all the treasure of thy lusty days;
To say, within thine own deep sunken eyes,
Were an all-eating shame, and thriftless praise.
How much more praise deserv'd thy beauty's use,
If thou couldst answer 'This fair child of mine
Shall sum my count, and make my old excuse,'
Proving his beauty by succession thine!
This were to be new made when thou art old,
And see thy blood warm when thou feel'st it cold.""".split()
# we should tokenize the input, but we will ignore that for now
# 我們本應該需要標記輸入文本,但此次我們忽略這一工作

# build a list of tuples.  Each tuple is ([ word_i-2, word_i-1 ], target word)
# 建立一個元組列表,每個元組是一個([ word_i-2, word_i-1 ], target word)
trigrams = [([test_sentence[i], test_sentence[i + 1]], test_sentence[i + 2])
            for i in range(len(test_sentence) - 2)]

# print the first 3, just so you can see what they look like
print(trigrams[:3])

Out:

[([‘When’, ‘forty’], ‘winters’), ([‘forty’, ‘winters’], ‘shall’), ([‘winters’, ‘shall’], ‘besiege’)]

vocab = set(test_sentence)
word_to_ix = {word: i for i, word in enumerate(vocab)}

class NGramLanguageModeler(nn.Module):
    def __init__(self, vocab_size, embedding_dim, context_size):
    super(NGramLanguageModeler, self).__init__()
    self.embeddings = nn.Embedding(vocab_size, embedding_dim)
    self.linear1 = nn.Linear(context_size * embedding_dim, 128)
    self.linear2 = nn.Linear(128, vocab_size)

def forward(self, inputs):
    embeds = self.embeddings(inputs).view((1, -1))
    out = F.relu(self.linear1(embeds))
    out = self.linear2(out)
    log_probs = F.log_softmax(out, dim=1)
    return log_probs


losses = []
loss_function = nn.NLLLoss()
model = NGramLanguageModeler(len(vocab), EMBEDDING_DIM, CONTEXT_SIZE)
optimizer = optim.SGD(model.parameters(), lr=0.001)

for epoch in range(10):
    total_loss = 0
    for context, target in trigrams:
        # Step 1. Prepare the inputs to be passed to the model (i.e, turn the words
        # into integer indices and wrap them in tensors)
        # 1. 送入模型前的準備工作(如:將單詞轉化爲對應的索引,並張量化)
        context_idxs = torch.tensor([word_to_ix[w] for w in context], dtype=torch.long)

        # Step 2. Recall that torch *accumulates* gradients. Before passing in a
        # new instance, you need to zero out the gradients from the old instance
        # 2. Pytorch 能積累梯度,因此在傳入新實例之前需要將舊梯度清零
        model.zero_grad()

        # Step 3. Run the forward pass, getting log probabilities over next words
        # 3. 前向傳播,求下一個單詞的對數概率
        log_probs = model(context_idxs)

        # Step 4. Compute your loss function. (Again, Torch wants the target
        # word wrapped in a tensor)
        # 4. 計算損失函數(Pytorch要求將目標單詞轉換爲張量)
        loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))

        # Step 5. Do the backward pass and update the gradient
        # 5. 反向傳播,更新梯度
        loss.backward()
        optimizer.step()

        # Get the Python number from a 1-element Tensor by calling tensor.item()
        # 使用tensor.item()方法來從單一元素張量中取得數據
		total_loss += loss.item()
	losses.append(total_loss)

print(losses)  # The loss decreased every iteration over the training data!(誤差每次迭代都下降)

Out:

[518.5035681724548, 516.1092879772186, 513.7281634807587, 511.3589131832123, 509.00272035598755, 506.6583275794983, 504.32336044311523, 501.9988875389099, 499.68528985977173, 497.3804793357849]


練習:計算詞嵌入:連續詞袋模型

Exercise: Computing Word Embeddings: Continuous Bag-of-Words

連續詞袋模型(CBOW)是自然語言處理中非常常用的一種語言模型。CBOW通過參考目標單詞上下文幾個單詞來對目標單詞進行預測。與語言建模不同的是,CBOW並非順序模型,也不必一定是概率化的。CBOW通常被用來快速訓練詞嵌入,並且用這些訓練得到的詞嵌入來初始化那些更爲複雜的詞嵌入模型,通常這一過程也被稱爲預訓練詞嵌入。CBOW絕大多數情況下能夠使模型表現提升數個百分點。

以下定義CBOW模型:

給定目標單詞 wiw_i 以及左右兩邊大小爲 N 的上下文窗口:wi1,,wiNw_{i-1},\dots, w_{i-N}wi+1,,wi+Nw_{i+1},\dots, w_{i+N},將上述這些上下文單詞記做 cc ,CBOW模型嘗試最小化以下內容:
logp(wiC)=logSoftmax(A(wCqw)+b) -\log p(w_i | C) = -\log \text{Softmax}(A(\sum_{w \in C} q_w) + b)
其中,qwq_w是單詞w的詞嵌入。

使用Pytorch,完善下面的類,實現這一模型。

提示:

  • 考慮需要定義哪些參數/變量
  • 明確每步操作要求的形狀,需要變換張量形狀時,使用.view()方法
CONTEXT_SIZE = 2  # 2 words to the left, 2 to the right(考慮目標單詞左右2個單詞)
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules
called a program. People create programs to direct processes. In effect,
we conjure the spirits of the computer with our spells.""".split()

# By deriving a set from `raw_text`, we deduplicate the array
#通過集合化,將元素唯一化
vocab = set(raw_text)
vocab_size = len(vocab)

word_to_ix = {word: i for i, word in enumerate(vocab)}
data = []
for i in range(2, len(raw_text) - 2):
    context = [raw_text[i - 2], raw_text[i - 1], raw_text[i + 1], raw_text[i + 2]]
    target = raw_text[i]
    data.append((context, target))
print(data[:5])

Out:

[([‘We’, ‘are’, ‘to’, ‘study’], ‘about’), ([‘are’, ‘about’, ‘study’, ‘the’], ‘to’), ([‘about’, ‘to’, ‘the’, ‘idea’], ‘study’), ([‘to’, ‘study’, ‘idea’, ‘of’], ‘the’), ([‘study’, ‘the’, ‘of’, ‘a’], ‘idea’)]

class CBOW(nn.Module):

    def __init__(self):
        pass
    
    def forward(self, inputs):
        pass
    
# create your model and train.  here are some functions to help you make
# the data ready for use by your module

def make_context_vector(context, word_to_ix):
    idxs = [word_to_ix[w] for w in context]
    return torch.tensor(idxs, dtype=torch.long)

make_context_vector(data[0][0], word_to_ix)  # example

該練習答案後續更新。


[Translation] 2020/4 Karl

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