PyTorch專欄(十八): 詞嵌入,編碼形式的辭彙語義

  • 2019 年 11 月 5 日
  • 筆記

作者 | News 編輯 | 奇予紀

出品 | 磐創AI團隊出品

【磐創AI 導讀】:查看關於本專欄歷史文章,請點擊文末[閱讀全文]。查看本章歷史文章,請點擊下方藍色字體進入相應鏈接閱讀。

專欄目錄:

第五章:PyTorch之文本篇

詞嵌入:編碼形式的辭彙語義

詞嵌入是一種由真實數字組成的稠密向量,每個向量都代表了單詞表裡的一個單詞。在自然語言處理中,總會遇到這樣的情況:特徵全是單詞!

但是,如何在電腦上表述一個單詞呢?你在電腦上存儲的單詞的 ASCII 碼,但是它僅僅代表單詞怎麼拼寫,沒有說明單詞的內在含義(你也許能夠從詞綴中了解它的詞性,或者從大小寫中得到一些屬性,但僅此而已)。更重要的是,你能把這些 ASCII 碼字元組合成什麼含義?

代表辭彙表、輸入數據是

維的情況下,我們往往想從神經網路中得到數據密集的結果,但是結果只有很少的幾個維度(例如,預測的數據只有幾個標籤時)。我們如何從大的數據維度空間中得到稍小一點的維度空間?

放棄使用 ASCII 碼字元的形式表示單詞,換用 one-hot encoding 會怎麼樣了?好吧,

這個單詞就能這樣表示:

其中,1表示的獨有位置,其他位置全是0。其他的詞都類似,在另外不一樣的位置有一個1代表它,其他位置也都是0。這種表達除了佔用巨大的空間外,還有個很大的缺陷。它只是簡單的把詞看做一個單獨個體,認為它們之間毫無聯繫。我們真正想要的是能夠表達單詞之間一些相似的含義。為什麼要這樣做呢?來看下面的例子:

假如我們正在搭建一個語言模型,訓練數據有下面一些句子:

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

現在又得到一個沒見過的新句子:

  • The physicist solved the open problem.

我們的模型可能在這個句子上表現的還不錯,但是,如果利用了下面兩個事實,模型會表現更佳:

  • 我們發現數學家和物理學家在句子里有相同的作用,所以在某種程度上,他們有語義的聯繫。
  • 當看見物理學家在新句子中的作用時,我們發現數學家也有起著相同的作用。

然後我們就推測,物理學家在上面的句子里也類似於數學家嗎?這就是我們所指的相似性理念:指的是語義相似,而不是簡單的拼寫相似。 這就是一種通過連接我們發現的和沒發現的一些內容相似點、用於解決語言數據稀疏性的技術。這個例子依賴於一個基本的語言假設:那些在相似語句中出現的單詞,在語義上也是相互關聯的。這就叫做distributional hypothesis(分散式假設)(相關鏈接:https://en.wikipedia.org/wiki/Distributional_semantics)。

1. Getting Dense Word Embeddings(密集詞嵌入)

我們如何解決這個問題呢?也就是,怎麼編碼單詞中的語義相似性?也許我們會想到一些語義屬性。

舉個例子,我們發現數學家和物理學家都能跑,所以也許可以給含有「能跑」語義屬性的單詞打高分,考慮一下其他的屬性,想像一下你可能會在這些屬性上給普通的單詞打什麼分。

如果每個屬性都表示一個維度,那我們也許可以用一個向量表示一個單詞,就像這樣:

那麼,我們就這可以通過下面的方法得到這些單詞之間的相似性:

儘管通常情況下需要進行長度歸一化:

是兩個向量的夾角。這就意味著,完全相似的單詞相似度為1。完全不相似的單詞相似度為-1。

你可以把本章開頭介紹的 one-hot 稀疏向量看做是我們新定義向量的一種特殊形式,那裡的單詞相似度為0,現在我們給每個單詞一些獨特的語義屬性。這些向量數據密集,也就是說它們數字通常都非零。

但是新的這些向量存在一個嚴重的問題:你可以想到數千種不同的語義屬性,它們可能都與決定相似性有關,而且,到底如何設置不同屬性的值呢?深度學習的中心思想是用神經網路來學習特徵的表示,而不是程式設計師去設計它們。所以為什麼不把詞嵌入只當做模型參數,而是通過訓練來更新呢?這就才是我們要確切做的事。我們將用神經網路做一些潛在語義屬性,但是原則上,學習才是關鍵。注意,詞嵌入可能無法解釋。

就是說,儘管使用我們上面手動製作的向量,能夠發現數學家和物理學家都喜歡喝咖啡的相似性,如果我們允許神經網路來學習詞嵌入,那麼就會發現數學家和物理學家在第二維度有個較大的值,它所代表的含義很不清晰。它們在一些潛在語義上是相似的,但是對我們來說無法解釋。

2. Pytorch中的詞嵌入

在我們舉例或練習之前,這裡有一份關於如何在Pytorch和常見的深度學習中使用詞嵌入的簡要介紹。與製作 one-hot 向量時對每個單詞定義一個特殊的索引類似,當我們使用詞向量時也需要為每個單詞定義一個索引。這些索引將是查詢表的關鍵點。意思就是,詞嵌入被被存儲在一個

的向量中,其中

是詞嵌入的維度。詞被被分配的索引 i,表示在向量的第i行存儲它的嵌入。

在所有的程式碼中,從單詞到索引的映射是一個叫 word_to_ix 的字典。

能使用詞嵌入的模組是torch.nn.Embedding,這裡面有兩個參數:辭彙表的大小和詞嵌入的維度。

索引這張表時,你必須使用torch.LongTensor(因為索引是整數,不是浮點數)。

# 作者: 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  lookup_tensor = torch.tensor([word_to_ix["hello"]], dtype=torch.long)  hello_embed = embeds(lookup_tensor)  print(hello_embed)
  • 輸出結果:
tensor([[ 0.6614,  0.2669,  0.0617,  0.6213, -0.4519]],         grad_fn=<EmbeddingBackward>)

3.例子:N-Gram語言模型

回想一下,在 n-gram 語言模型中,給定一個單詞序列向量,我們要計算的是:

是單詞序列的第 i 個單詞。在本例中,我們將在訓練樣例上計算損失函數,並且用反向傳播演算法更新參數。

CONTEXT_SIZE = 2  EMBEDDING_DIM = 10  # 我們用莎士比亞的十四行詩 Sonnet 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()  # 應該對輸入變數進行標記,但暫時忽略。  # 創建一系列的元組,每個元組都是([ 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)]  # 輸出前3行,先看下是什麼樣子。  print(trigrams[:3])    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:            # 步驟 1. 準備好進入模型的數據 (例如將單詞轉換成整數索引,並將其封裝在變數中)          context_idxs = torch.tensor([word_to_ix[w] for w in context], dtype=torch.long)            # 步驟 2. 回調torch累乘梯度          # 在傳入一個新實例之前,需要把舊實例的梯度置零。          model.zero_grad()            # 步驟 3. 繼續運行程式碼,得到單詞的log概率值。          log_probs = model(context_idxs)            # 步驟 4. 計算損失函數(再次注意,Torch需要將目標單詞封裝在變數里)。          loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))            # 步驟 5. 反向傳播更新梯度          loss.backward()          optimizer.step()            # 通過調tensor.item()得到單個Python數值。          total_loss += loss.item()      losses.append(total_loss)  print(losses)  # 用訓練數據每次迭代,損失函數都會下降。
  • 輸出結果:
[(['When', 'forty'], 'winters'), (['forty', 'winters'], 'shall'), (['winters', 'shall'], 'besiege')]  [523.1487259864807, 520.6150465011597, 518.0996162891388, 515.6003141403198, 513.1156675815582, 510.645352602005, 508.1888840198517, 505.74565410614014, 503.314866065979, 500.8949146270752]

4.練習:計算連續詞袋模型的詞向量

連續詞袋模型(CBOW)在NLP深度學習中使用很頻繁。

它是一個模型,嘗試通過目標詞前後幾個單詞的文本,來預測目標詞。這有別於語言模型,因為CBOW不是序列的,也不必是概率性的。CBOW常用於快速地訓練詞向量,得到的嵌入用來初始化一些複雜模型的嵌入。通常情況下,這被稱為預訓練嵌入。它幾乎總能幫忙把模型性能提升幾個百分點。

CBOW 模型如下所示:給定一個單詞

代表兩邊的滑窗距,如

,並將所有的上下文詞統稱為

,CBOW 試圖最小化

其中

是單詞

的嵌入。

在 Pytorch 中,通過填充下面的類來實現這個模型,有兩條需要注意:

  • 考慮下你需要定義哪些參數。
  • 確保你知道每步操作後的結構,如果想重構,請使用.view()
CONTEXT_SIZE = 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()    # 通過對`raw_text`使用set()函數,我們進行去重操作  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])    class CBOW(nn.Module):        def __init__(self):          pass        def forward(self, inputs):          pass    # 創建模型並且訓練。這裡有些函數幫你在使用模組之前製作數據。    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

輸出結果:

[(['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')]