小白跟學系列之手把手搭建NLP經典模型(含代碼)

作者:雲不見
編輯:王萌

小白跟學系列之手把手搭建NLP經典模型(含代碼)

之前整理過齋藤康毅的深度學習神作《深度學習入門:基於Python的理論與實現》,對小白非常友好,它沒有用任何的現成框架(比如pyTorch、tensorFlow等等),而是直接用python自帶的庫手把手教你,從如何實現梯度下降開始到手磕一個CNN經典網絡,讓你不再對深度學習框架的內部機制感到神祕。

短短几年,這位大佬再度出了“續集”—《深度學習進階:自然語言處理》[1]!(可以說是NLP入門必讀的經典著作了!)

小齋這次的寫作風格和前作一樣,都是手把手教你從實現詞向量開始,像搭積木一樣,再到如何實現經典網絡NLP屆的界的經典網絡RNN、LSTM、seq2seq和Attention等NLP中重要的深度學習技術。

(太感人了,這次終於要搞懂詞向量究竟是個什麼鬼了!(逃

凡我不能創造的,我就不能理解。

-

— 理查德·費曼(致敬費曼)

話不多說,這次我不再整理的和上個系列一樣那麼詳細(繁冗)了(個人覺得沒必要,那樣整理還不如直接看書來的直接痛快。所以這一次我會摘取基礎又重要的部分,如果你想再詳細深究下去,乖,去參考原書噢!

本書用到的庫:

Numpy

-

Matplotlib

(真的是隻用了這兩個基本庫!(強

如果要用GPU加速運算的話,再加一個CuPy庫。

作者強調,自己動手的經驗、花時間思考的經驗,都是無法複製的。(所以,聽話,要自己嘗試敲1敲代碼噢!

本書第一章爲上一本書神經網絡的複習,咱直接跳到第二章 從詞向量開始

正文開始

目錄

什麼是自然語言處理?

同義詞詞典

基於計數的方法

基於計數的方法改進

小白跟學系列之手把手搭建NLP經典模型(含代碼)

必做練習

  • 語料庫的預處理。實現分詞(將文本分割成單詞,以單詞爲最小單元輸入給模型)

  • 單詞ID化(將單詞用ID來表示,相當於給每一個單詞編個代號,和我們學生編個學號一個意思,方便定位和管理唄)

  • 利用共現矩陣表示文本的詞向量。

(這些練習會在後續搭建模型的時候用得到噢!相當於我們先造積木,之後搭網絡就有素材啦!本章的詞向量表示也就是對文本進行預處理的準備工作!)

注意:全文的講解都是以

(存在一個問題 —> 找到解決辦法 —> 新的解決辦法又有什麼問題 —> 又找到解決辦法 )的思路一步一步引出各種概念和解決方案的。

我們的學習過程也是如此,市面上突然出現的各種模型也是如此,瞭解它解決了什麼問題也就知道亂七八糟的各種模型爲什麼會出現了。

什麼是自然語言處理?

  • 自然語言:就是我們平常使用的語言,如漢語、英語;

  • 自然語言處理:就是讓機器理解人類的語言,理解了人類語言才能對我們的語言進行進一步解讀和分析!(比如對人類的情感進行分析、對文本進行分類、能夠實現人機對話等等)

在沒有深度學習的時候,專家們是這樣進行詞向量的表示的:

同義詞詞典

最著名的同義詞詞典當屬WordNet [2]啦。WordNet等同義詞詞典中對大量單詞人工的定義了同義詞和層級結構關係等。

同義詞詞典存在的問題


難以順應時代的變化。語言是活的,新詞會不斷出現。

人力成本高,WordNet收錄了超過20W個單詞。

無法表示單詞的微妙差異。即使是含義相近的單詞,也有細微的差別。比如,vintage 和retro(類似復古的意思)雖然表示相同的含義,但是用法不同,而這種細微的差別在同義詞詞典中是無法表示出來的(讓人來解釋是相當困難的)。

Marty:“This is heavy (棘手).”

Dr. Brown:“In the future, things are so heavy (重)?”

— 電影《回到未來》

在電影《回到未來》中,有這樣一個場景:從1985 年穿越回來的馬蒂和生活在1955年的的博士的對話中,對“heavy”的含義有不同的理解。如果要處理這樣的單詞變化,就需要人工不停地更新同義詞詞典。

基於計數的方法(基於統計)

目標:從海量文本數據中自動提取單詞含義,減少人爲干擾。

  • 語料庫(corpus):就是我們輸入模型的大量文本,比如句子、文章等等。

這裏將用一句話作爲語料庫來闡述接下來的所有概念。

>>> text = 'you say goodbye and I say hello.'

語料庫的預處理


1、進行句子的分詞,並標記每個單詞的ID。(就像給每個學生編上學號ID一樣,方便後續指定某一個學生呀!)

>>> text = text.lower() //將所有單詞轉化爲小寫
>>> text = text.replace('.', ' .') //使句號其和前一個單詞分開
>>> text
'you say goodbye and i say hello .'
>>> words = text.split(' ') //切分句子
>>> words
['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello', '.'] //由八個詞組成的數組

首先,使用lower()方法將所有單詞轉化爲小寫,這樣可以將句子開頭的單詞也作爲常規單詞處理。然後,將空格作爲分隔符,通過split(' ')切分句子。考慮到句子結尾處的句號(.),我們先在句號前插人一個空格(即用 ' .'替換'.'),再進行分詞。

2、我們進一步給單詞標上 ID,以便使用單詞 ID 列表,方便爲後續對每個單詞進行操作。

將單詞列表轉化爲單詞 ID 列表,然後再將其轉化爲 NumPy 數組。

word_to_id = {}  //將單詞轉化爲單詞 ID
id_to_word = {}  //將單詞 ID 轉化爲單詞(鍵是單詞ID,值是單詞)
for word in words:
  if word not in word_to_id:
  //如果單詞不在 word_to_id 中,則分別向 word_to_id 和id_to_word 添加新 ID 和單詞
    new_id = len(word_to_id)
    word_to_id[word] = new_id
    id_to_word[new_id] = word

corpus = np.array([word_to_id[w] for w in words])

如果單詞不在 word_to_id 中,則分別向 word_to_id 和id_to_word 添加新 ID 和單詞

如下爲創建好了單詞 ID 和單詞的對應表


>>> id_to_word
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6:'.'}
>>> word_to_id
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}

最後,我們將單詞列表轉化爲單詞 ID 列表,然後再將其轉化爲 NumPy 數組。


>>> corpus
array([0, 1, 2, 3, 4, 1, 5, 6])

將第一步和第二步封裝爲一個preprocess() 函數,使用這個函數,可以按如下方式對語料庫進行預處理。( 代碼在common/util.py)

輸入要處理的語料庫text,輸出corpus, word_to_id, id_to_word


>>> text = 'You say goodbye and I say hello.'
>>> corpus, word_to_id, id_to_word = preprocess(text)

corpus 是單詞ID 列表,word_to_id 是單詞到單詞 ID 的字典,id_to_word 是單詞 ID 到單詞的字典。

語料庫的預處理已完成。這裏準備的 corpus、word_to_id 和 id_to_word 這 3 個變量名在本書接下來的很多地方都會用到。

接下來的目標就是使用語料庫提取單詞含義,這裏先使用基於計數的方法,也就是基於統計的方法,能夠得到詞向量!(也就是將單詞表示爲向量)

分佈式假說(distributional hypothesis)


  • 分佈式假說(distributional hypothesis):某個單詞的含義由它周圍的單詞形成。(某個人存在的價值由它的社會屬性構成。—我瞎說的)

單詞本身沒有含義,單詞含義由它所在的上下文(語境)形成。

比如“I drink beer.” “We drink wine.” , drink 的附近常有飲料出現。

另外,從“I guzzle beer.” “We guzzle wine.”可知,guzzle 和 drink 所在的語境相似。進而我們可以推測出guzzle 和 drink 是近義詞(guzzle 是“大口喝”的意思) 。

基於這一假說,我們就可以通過單詞的上下文來表示該單詞。如圖,左側和右側的 2 個單詞就是上下文。

小白跟學系列之手把手搭建NLP經典模型(含代碼)
這裏的窗口大小可以控制你需要關心多少單詞的上下文。顯而易見,關心的上下文單詞數越多,單詞的含義越準確,但是所需要的存儲量就越大,看你自己的取捨咯!這裏簡單起見,窗口大小爲1。

如何基於分佈式假設使用向量表示單詞,最直截了當的實現方法是對周圍單詞的數量進行計數。

共現矩陣(co-occurrence matrix)


  • 共現矩陣(co-occurrence matrix):用上下文共同出現的單詞次數作爲該單詞的向量。即若兩個單詞挨着出現一次,次數加一。

上面已經處理好語料庫了,接下來構建共現矩陣,也就是計算每個單詞的上下文所包含的單詞的頻數。在這個例子中,我們將窗口大小設爲 1,從單詞 ID 爲 0 的 you 開始。

單詞 you 的上下文僅有 say 這個單詞,如下圖所示。

小白跟學系列之手把手搭建NLP經典模型(含代碼)

所以單詞you可表示爲:

小白跟學系列之手把手搭建NLP經典模型(含代碼)

即可以用向量 [0, 1, 0, 0, 0, 0, 0] 表示單詞 you。其他單詞也是重複如此操作。

小白跟學系列之手把手搭建NLP經典模型(含代碼)

於是得到了共現矩陣:

小白跟學系列之手把手搭建NLP經典模型(含代碼)

接下來,我們來實際創建一下上面的共現矩陣。

將圖 2-7 的結果按原樣手動輸入。


C = np.array([
  [0, 1, 0, 0, 0, 0, 0],
  [1, 0, 1, 0, 1, 1, 0],
  [0, 1, 0, 1, 0, 0, 0],
  [0, 0, 1, 0, 1, 0, 0],
  [0, 1, 0, 1, 0, 0, 0],
  [0, 1, 0, 0, 0, 0, 1],
  [0, 0, 0, 0, 0, 1, 0], ], dtype=np.int32)

這就是共現矩陣。使用這個共現矩陣,可以獲得各個單詞的向量,如下所示。

print(C[0]) # 單詞ID爲0的向量
# [0 1 0 0 0 0 0]
print(C[4]) # 單詞ID爲4的向量
# [0 1 0 1 0 0 0] print(C[word_to_id['goodbye']]) # goodbye的向量
# [0 1 0 1 0 0 0]

我們通過共現矩陣成功地用向量表示了單詞。但手動輸入共現矩陣太麻煩,這一操作顯然可以自動化。下面,我們來實現一個能直接從語料庫生成共現矩陣的函數。

通過函數create_co_matrix()能直接從語料庫生成共現矩陣。(代碼實現在common/util.py)

其中參數 corpus 是單詞 ID 列表,參數 vocab_ size 是詞彙個數,window_size 是窗口大小。

def create_co_matrix(corpus, vocab_size, window_size=1):
  corpus_size = len(corpus)
  co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32)

  for idx, word_id in enumerate(corpus):
    for i in range(1, window_size + 1):
      left_idx = idx - i
      right_idx = idx + i

      if left_idx >= 0:
        left_word_id = corpus[left_idx]
        co_matrix[word_id, left_word_id] += 1

      if right_idx < corpus_size:
        right_word_id = corpus[right_idx]
        co_matrix[word_id, right_word_id] += 1

  return co_matrix

首先,用元素爲 0 的二維數組對 co_matrix 進行初始化。然後,針對語料庫中的每一個單詞,計算它的窗口中包含的單詞。同時,檢查窗口內的單詞是否超出了語料庫的左端和右端。

這樣一來,無論語料庫多大,都可以自動生成共現矩陣。之後,我們都將使用這個函數生成共現矩陣。

到這裏我們終於第一次成功的用向量表示單詞啦!將正式邁入文本詞向量表示的道路!

寫到這發現篇幅太長了,爲了能有更好的學習體驗,接下來共現矩陣存在的問題以及改進方式就下一篇再見啦!

(微信公衆號後臺回覆:“小白跟學”,獲取本書所用的所有代碼)

參考文獻

[1] 齋藤康毅《深度學習進階:自然語言處理》

[2] Miller, George A . WordNet: a lexical database for English[J]. Communications of the Acm, 1995, 38(11):39--41.

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