2021年1月24日 星期日

[NLP] 自然語言處理觀念整理-1 (前處理與觀念)

自然語言處理觀念整理-1

評估.資料量不足作法, 原文提取與清理, 預處理, 原文表示法


標準的NLP流程


深度學習於自然語言處理上的難題
  • 雖然DL已於NLP取得非常大的進步, 但並非所有問題都必須使用深度學習來解決, 更多是混合式ML or DL + rule base來解決問題, 以下列出幾點要點來幫助評估
  • NLP使用ML如單純貝葉氏可以解釋正負面字眼對情緒分類的影響但LSTM難以被解釋, 而CNN處理影像問題則有很多可以解釋的技術
  • 小樣本適合用傳統的ML, DL有更多運算元, 更強的表達能力,容易overfitting
  • NLP難以用生成的方式創造樣本, 但影像卻很容易
  • 特定領域model不容易在別的領域通用, 要將領域專門化, 或許用簡單的model即可, 不用DL
  • DL處理NLP成本很高, 上標籤成本非常昂貴, 有可能花費更多成本但效果只和ML差不多還可能更差
  • 原文表示法的好壞影響勝過演算法好壞的影響
  • bert gpt 因為看過大量資料, 斷詞變得比較沒必要
    但可能會因為跑不動, 必須用以前的技術
  • 模型小資料小 加上 knowledge graph加強
    資料超大就可以取代knowledge graph

資料量不足作法
  • 同義詞替換:從句子隨機選出K個不是停用詞的字, 使用同義詞替換
  • 使用翻譯(google translate):翻成另一個語言再翻回來
  • 替換個體 : 地名, 人名替換
  • 雙字對調 : 把句子切割成許多雙字, 然後顛倒
  • 在資料中加入雜訊:行動手機鍵盤上容易打錯的英文字故意打錯也丟進去training
  • 套件:easy data augmentation, NLPAug, Snorkel

原文提取與清理
目的是移除資料中所有非文字的資訊(EX: pdf, html, image ......), 並轉換成所需編碼格式, 會有以下幾個步驟, 不同語言流程可能不一樣:
  • HTML解析與清洗: 相關套件如BeautifulSoup
  • Unicode正規化: 特殊字元或表情處理
  • 拼寫糾正: 相關套件如pyenchant

預處理
詳細再對資料取回來的資料進行預處理, 不同語言需要不同套件協助處理, 如果是情緒分析, 需要進行停用詞, 小寫, 數字移除...等工作, 但如果是email提取行事曆事件, 還要加入parsing, 不要移除停用詞與進行詞幹提取
  • 句子分割:原文變句子, 套件如NLTK
  • 斷詞(tokenization): 句子單元化, 套件如NLTK與jieba
  • 斷詞能更好的表達意思, 當然每個字都放進去也可以, 但斷詞斷不好會有反效果, 專有領域例如法律金融, 先斷詞會更好, 一般口語領域, 不斷詞也可以
  • 過濾停用詞
  • 詞幹提取(stemming):cars > car, adjustable > adjust, 可以減少特徵空間
  • 詞形還原(lemmatization):was > be, better > good, 可以減少特徵空間
  • 原文正規化:例如轉小寫(casing), 簡繁互轉, 數字轉文字(9 > nine), 展開縮寫
  • 語言偵測:套件如Polyglot
  • 語言混合處理:統一轉成英語拼音
  • 詞性標注(pos tagging)
  • 共指消解(coreference resolution): 找出關係

  • 詞幹提取(stemming):cars > car, adjustable > adjust, 可以減少特徵空間
  • 詞形還原(lemmatization):was > be, better > good, 可以減少特徵空間
Stemming 與 Lemmatization的目的就是及將這些不同的表示型態歸一化,藉此來降低文本的複雜度。
處理中文文本比較不會處理這一塊,而在非中文的語言上,例如英文語句中,同一個單詞在拼法上可能隨著時態、單複數、主被動等狀況不同,ex: speaking 與 speak,然而其所要表達的語意並沒有太大的不同。
其優點為能夠大幅降低詞數量,在建立 BOW (bag of word,在後面的文章會慢慢介紹到,一個文章或句子的表示型態) 時可以降低表示文本的維度,以降低資料複雜度,加快語言模型訓練速度,ex: speak 與 speaking 想表達的語意相當接近,如果將兩者都轉為 speak。
缺點其實就是喪失的訊息,以上述 speaking / speak 為例,有可能某些預測需要利用到時態的訊息,當我們將兩者都轉為 speak 輸入模型時, speak-ing 中 ing 的訊息也被我們移除。




文字表示法
文字資料逼須先轉換成模型看得懂的表示法, 並能正確地描敘句子含義, 優秀的原文表示法必須提取下列幾點:
  1. 將句子拆成詞彙單元, 如單字.詞素.子句
  2. 推倒個詞素單字含義
  3. 了解句法結構
  4. 了解句子的上下文
下面提供許多種方式
範例文件 documents = ["Dog bites man.", "Man bites dog.", "Dog eats meat.", "Man eats food."]

基本向量化方法(分布表示法)
  • one-hot:
假設單字dog=1, bits=2, man=3, meat=4, food=5, eats=6, 句子就會變成 D1=[[100000][010000][001000]]
  1. 缺點向量大小與詞彙量成正比, 大多的文檔過於龐大, 造成向量稀疏, 稀疏容易over fiting
  2. 每個句子長度也不同
  3. 單字之間無相似性, 描述單字之間意義能力很差
  4. 如果訓練詞彙沒有這個字, 就不會有結果
  5. 已很少人使用它
  • bag of word
假設單字dog=1, bits=2, man=3, meat=4, food=5, eats=6, 句子就會變成 D1=[1 1 1 0 0 0]
  1. 容易理解與實作
  2. 稀疏問題
  3. 長度固定
  4. 如果訓練詞彙沒有這個字, 就不會有結果
  5. 捨棄單字出現順序, 我打你跟你打我不一樣分不出來
  6. I run , I ran, I ate 在bow向量都是等距的, 
  7. 如果不考慮詞頻(例如情緒分析), count_vect = CountVectorizer(binary=True)

from
sklearn.feature_extraction.text import CountVectorizer #look at the documents list print("Our corpus: ", processed_docs) count_vect = CountVectorizer() #Build a BOW representation for the corpus bow_rep = count_vect.fit_transform(processed_docs) #Look at the vocabulary mapping print("Our vocabulary: ", count_vect.vocabulary_) #see the BOW rep for first 2 documents print("BoW representation for 'dog bites man': ", bow_rep[0].toarray()) print("BoW representation for 'man bites dog: ",bow_rep[1].toarray()) #Get the representation using this vocabulary, for a new text temp = count_vect.transform(["dog and dog are friends"]) print("Bow representation for 'dog and dog are friends':", temp.toarray())
  • n-garm: 
前兩個方法都將單字視為獨立的單位, 他沒有子句或單字順序的概念, bag of n gram, 將原文拆成包含許多n個連續字的段落, 可以描敘上下文, 舉例2-gram = { dog bites, bits man, man bits, bits eats ......  } , D1 = [1,1,0,0,0,0,0,0]
  1. 有上下文的描敘
  2. 如果訓練詞彙沒有這個字, 就不會有結果
  3. 簡單的問題會有好的效果
  4. ngram_range=(1,3) = unigram and bigram and trigram
from sklearn.feature_extraction.text import CountVectorizer

#Ngram vectorization example with count vectorizer and uni, bi, trigrams
count_vect = CountVectorizer(ngram_range=(1,3))

#Build a BOW representation for the corpus
bow_rep = count_vect.fit_transform(processed_docs)

#Look at the vocabulary mapping
print("Our vocabulary: ", count_vect.vocabulary_)

#see the BOW rep for first 2 documents
print("BoW representation for 'dog bites man': ", bow_rep[0].toarray())
print("BoW representation for 'man bites dog: ",bow_rep[1].toarray())

#Get the representation using this vocabulary, for a new text
temp = count_vect.transform(["dog and dog are friends"])

print("Bow representation for 'dog and dog are friends':", temp.toarray())
  • TFIDF: 
前面的做法都把每個單字視為一樣重要, TFIDF方法可找出關鍵字, 每個單字計算TFIDF後, d1 = [0.136, 0.17, 0.136 0 0 0]
  1. 有考慮單字重要程度
  2. 如果訓練詞彙沒有這個字, 就不會有結果

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer()
bow_rep_tfidf = tfidf.fit_transform(processed_docs)

#IDF for all words in the vocabulary
print("IDF for all words in the vocabulary",tfidf.idf_)
print("-"*10)
#All words in the vocabulary.
print("All words in the vocabulary",tfidf.get_feature_names())
print("-"*10)

#TFIDF representation for all documents in our corpus 
print("TFIDF representation for all documents in our corpus\n",bow_rep_tfidf.toarray()) 
print("-"*10)

temp = tfidf.transform(["dog and man are friends"])
print("Tfidf representation for 'dog and man are friends':\n", temp.toarray())

散式表示法
基本向量化方法都有一些缺點(如下)
  1. 他們都是離散的原子單位, 無法表示各單位關係
  2. 資料稀疏表示法, 當資料多, 計算效率極低
  3. 無法處理oov(out of vacabulary)的問題
近年來發展出使用神經網路建立密集低維的單字和原文表示法, 可以產生幾乎沒有0的向量, 其向量空間稱為散式表示法
  • word embedding(也稱密集文字向量):
embedding是將文集裡面的單字集合的分布表示法向量空間對映到散式表示法向量空間的機制, 是低維度的浮點數向量, word embedding訓練出來的空間差異很大, 所以每次都必須重新訓練實作方式可參考:
  1. 將原文語義單元化, 轉換成單字索引向量
  2. 填補原文序列, 讓所有向量都有相同長度
  3. 把每個單字索引對映embedding向量
  4. embedding矩陣可以使用pretrained word embedding, 或使用文集的embedding訓練
  5. embedding是個龐大的模型, 部屬時必須考慮到將其載入memory的問題

我們可以把任一文字mapping到向量, 但問題再沒有任何結構, 文字之間意思很接近的有可能被嵌到很遠, 深度學習難以理解這種雜亂且非結構化的空間, 我們希望可以理解文字結構, 相同的字應該在空間中很接近

Keras 提供embedding layer, 有各種預訓練的word embedding向量可以下載使用, 著名的如word2vec與Glove, 可以參考這篇這篇
也可以自己訓練word2vec模型, 將字轉成向量, 可以參考這篇

  • word2vec:
word2vec會根據單字的上下文學會再一個共同向量空間裡面表示他們, 但要自己訓練非常的昂貴, 以下是使用預訓練好的word2vec找尋同義詞的範例:
import warnings #This module ignores the various types of warnings generated
warnings.filterwarnings("ignore") 

import os #This module provides a way of using operating system dependent functionality

import psutil #This module helps in retrieving information on running processes and system resource utilization
process = psutil.Process(os.getpid())
from psutil import virtual_memory
mem = virtual_memory()

import time #This module is used to calculate the time
from gensim.models import Word2Vec, KeyedVectors
pretrainedpath = '/tmp/input/GoogleNews-vectors-negative300.bin.gz'

#Load W2V model. This will take some time, but it is a one time effort! 
pre = process.memory_info().rss
print("Memory used in GB before Loading the Model: %0.2f"%float(pre/(10**9))) #Check memory usage before loading the model
print('-'*10)

start_time = time.time() #Start the timer
ttl = mem.total #Toal memory available

w2v_model = KeyedVectors.load_word2vec_format(pretrainedpath, binary=True) #load the model
print("%0.2f seconds taken to load"%float(time.time() - start_time)) #Calculate the total time elapsed since starting the timer
print('-'*10)

print('Finished loading Word2Vec')
print('-'*10)

post = process.memory_info().rss
print("Memory used in GB after Loading the Model: {:.2f}".format(float(post/(10**9)))) #Calculate the memory used after loading the model
print('-'*10)

print("Percentage increase in memory usage: {:.2f}% ".format(float((post/pre)*100))) #Percentage increase in memory after loading the model
print('-'*10)

print("Numver of words in vocablulary: ",len(w2v_model.vocab)) #Number of words in the vocabulary.
Memory used in GB before Loading the Model: 0.19
----------
103.58 seconds taken to load
----------
Finished loading Word2Vec
----------
Memory used in GB after Loading the Model: 5.04
----------
Percentage increase in memory usage: 2587.87% 
----------
Numver of words in vocablulary:  3000000
#Let us examine the model by knowing what the most similar words are, for a given word!
w2v_model.most_similar('beautiful')
[('gorgeous', 0.8353004455566406),
 ('lovely', 0.810693621635437), ('stunningly_beautiful', 0.7329413890838623) ...(省略)]
  • word2vec兩種架構版本:
  1. CBOW: 用上下文單字來預測中心字, CBOW也可以用算出來的向量帶代表整個上下文, 來代表比單字更大的範圍
  2. SkipGram: 用中心字預測上下字


  • OOV(out of vocabulary)問題:
  1. 用大型文集訓練就可以降低機率
  2. 專有領域還是必須獨立處理
  3. fasttext使用類似word2vec結構同時學習單字字元n-gram的embedding, 並將單字的embedding向量視為他成分字元n-gram的聚合體, 可以下載fasttext訓練好的模型, 在gensim的fasttext包裝器中載入使用, 缺點是模型太大會有工程問題
  • 單字字元以外的散式表示法:
  1. word2vec可以學習單字表示法, 整合產生原文表示法
  2. fasttext可以學習字元表示法,整合產生單字表示法和原文表示法
  3. doc2vec: 使用gensim實作, 考慮單字上下文,  除了單字向量, 還會學習段落向量, 有DM DBOW兩種, doc2vec為第一種可以產生全文的embedding表示法, 可將任意長度的原編碼成固定.低維度.密集的向量, 這裡有推薦系統使用doc2vec尋找相似原文的範例
  • 通用原文表示法:
  1. 上面原文表示法一個單字都有固定的表示法, 但同樣的單字在不同上下文的意思可能不同
  2. 可以使用遷移學習,如ELMo, BERT(有transformer, RNN建構而來), 來學習embedding, 再用特定任務專用資料進行微調, 但速度慢
  3. BERT: ransformer的encoder, GPT太大所以就會再回頭用bert, bert偏向一般性的知識, 要在fine tune成domain的知識, 需要文章級的訓練資料, bert gpt 因為看過大量資料, 斷詞變得比較沒必要, 但可能會因為跑不動, 必須用以前的技術
其他議題
  • 所有原文表示法都有偏見, 取決於他們訓練資料可以看到什麼
  • 預訓練embedding通常是大型檔案, 工程實現時必須注意
  • 每次有新的NLP模型時, 必須考慮如何在生產環境中使用他, 考慮商業需求與基礎建設的限制
  • embedding視覺化: t-SNE, tensorboard


Ref:
  • 自然語言處理最佳實務, O'Reilly

沒有留言:

張貼留言