2023年9月13日 星期三

NLP 學習筆記 : 文本表示法 (text representation)

最近從母校借到下面這本書 :


此書為 Oreilly 2020 年出版的 "Practical Natural Language Processing: A Comprehensive Guide to Building Real-World Nlp Systems" 一書的中文版, 參考 :


此書將 NLP 理論與實務之空隙巧妙地補起來, 這幾天看了其中的第三章 "原文表示法" (應該翻譯成文本為宜), 主要是介紹傳統的文本數值表示法 : 基本向量法, 詞袋法 (Word of Bag), 詞頻法 (TF 與 TF-IDF), 以及 NLP 任務常用的詞嵌入法 (word embedding) 等等. 

第三章範例程式網址如下 :


閱讀摘要與測試紀錄 :
  1. 對任何機器學習問題而言, 特徵擷取都是很重要的步驟, 無論建模的演算法多棒, 傳入糟糕的特徵就會得到糟糕結果.
  2. 文本資料不能直接餵給 NLP 演算法, 必須先對文本進行特徵工程, 亦即將文本資料轉換成數值形式 (稱為文本表示法, text representation) 才行, 無論資料是文本, 圖像, 或語音, 都必須轉成數值才能餵給演算法, 因為機器學習模型是以數值形式做運算的. 
  3. 用數值來表示圖像與語音都很簡單 (圖像用 RGB 像素矩陣,  語音用取樣序列) , 但要用數值來表示文本卻不像語音與圖像那樣簡單.
  4. 四種文本表示法 :
    (1). 基本向量法 (basic vectorization approaches)
    (2). 分散式表示法 (distributed representations)
    (3). 通用語言表示法 (universal language representation)
    (4). 人工製作特徵法 (handcrafted features)
    在 NLP 實務中, 好的文本表示法比演算法更重要, 與其將普普的文本表示法餵給一流的演算法, 還不如將優秀的文本表示餵給普普的演算法. 
  5. 一個優秀的文本表示法必須能擷取下列四種資訊 :
    (1). 將句子拆成基本的詞彙單元 (lexical unit), 例如詞彙, 單字, 與子句. 
    (2). 推論每個詞彙單元的涵義 (meaning)
    (3). 理解句子的語法 (syntactic) 結構
    (4). 理解句子的上下文脈絡 (context) 關係
  6. 基本向量法基於向量空間模型, 用數值組成的向量來表示文本中的單元 (字元, 音素, 單字, 子句, 句子), 最簡單的作法是使用識別碼 (identifier) 組成的向量來表示文本單元, 例如用單字在文本單字表 (vocabulary) 中的索引編號當作識別碼來組成句子的向量. 向量化的文本可透過計算其相似度 (similarity) 來推論語意關係, 相似度越高表示兩個文本語義越相近, 常用的相似度計算有兩種 :
    (1). 餘弦相似度 : 兩個向量夾角越小, 餘弦值越大, 語意越相近
    (2). 歐幾里德距離 : 兩個向量歐氏距離越小, 語意越相近
  7. 基本向量法是將文本中的所有單字編成一個字彙表, 然後將文本中的單字依序以它在字彙表中的唯一索引作為識別碼進行取代後組成向量, 例如下列含有四個句子的迷你語料庫文本 :

    S1 : "dog bites man"
    S2 : "man bites dog"
    S3 : "dog eats meat"
    S4 : "man eats food"

    此文本的字彙表 V 共包含 6 個單字 (前置處理已轉換為小寫並去除標點) :
    V=['dog', 'bites', 'man', 'meat', 'food', 'eats']
    如果直接用索引來表示這四個句子 : 

    S1 : [0, 1, 2]  "dog bites man"
    S2 : [2, 1, 0]  "man bites dog"
    S3 : [0, 5, 3]  "dog eats meat"
    S4 : [2, 5, 4]  "man eats food"

    但在 NLP 任務中, 由於 0 通常保留給 OOV (Out Of Vocabulary, 字彙表中查不到的字), 所以索引通常從 1 起算, 所以向量改為如下 :

    S1 : [1, 2, 3]  "dog bites man"
    S2 : [3, 2, 1]  "man bites dog"
    S3 : [1, 6, 4]  "dog eats meat"
    S4 : [3, 6, 5]  "man eats food"

  8. 獨熱編碼  (one-hot encoding) 向量 :
    文本向量還可以進一步用編碼來組成向量, 其中一種是獨熱編碼法, 它的優點是很容易理解與實作, 做法是將每一個單字用一個長度為字彙表度, 只含有一個 1 元素, 其餘元素為 0 的一維向量來表示, 其中 1 出現的位置標示著該字在字彙表中的索引, 例如上面的四句文本中, 'dog' 是字彙表的第一個單字, 其獨熱編碼為 [1, 0, 0, 0, 0, 0], 而最後一個單字 'eats' 編碼為 [0, 0, 0, 0, 0, 1], 此文本用用獨熱編碼可表示為 :

    S1 : [[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0]]  "dog bites man"
    S2 : [[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0]]  "man bites dog"
    S3 : [[1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1], [0, 0, 0, 1, 0, 0]]  "dog eats meat"
    S4 : [[0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 0]]  "man eats food"

    這樣就變成二維向量了. 可以用下列改寫過的函式來產生文本的獨熱編碼向量, 書中原程式碼有錯誤 (如黃底所示), 修正如下 :

    def get_onehot_vector(somestring):     # 傳入清理過的句子字串
        onehot_encoded = []                         # 儲存單字獨熱編碼向量的空文本串列
        for word in somestring.split():          # 將句子以空格拆分後迭代每個單字
            temp = [0]*len(V)                         # 建立與字彙表 V 等長的 0 向量 temp
            if word in V:                                  # 若單字在字彙表中
                temp[V.index(word)] = 1          # 將單字在字彙表之索引位置設為 1
            onehot_encoded.append(temp)     # 將獨熱向量放入文本串列末尾
        return onehot_encoded                      # 傳回文本向量

    其中傳入參數 somestring 是處理過的句子字串 (標點符號已去除, 全部單字轉成小寫), 例如 'dog bites man'; V 為字彙表 (串列) :

    >>> V=['dog', 'bites', 'man', 'eats', 'meat', 'food']
    >>> get_onehot_vector('dog bites man')
    [[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0]]
    >>> get_onehot_vector('man bites dog')
    [[0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0]]
    >>> get_onehot_vector('dog eats meat')
    [[1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 1, 0]]
    >>> get_onehot_vector('man eats food')
    [[0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 1]]

  9. 獨熱編碼的缺點 :
    • one-hot 向量大小與詞彙量成正比, 且只有一個 1 其餘為 0, 此種稀疏表示法在儲存, 運算, 與學習上的效率很低, 且容易過擬合 (ocer fitting).
    • one-hot 表示法的最小單位是單字, 字與字之間彼此獨立, 但它沒有提供單字之間的相關性資訊, 因此缺乏描述單字之間意義的能力
    • one-hot 表示法無法處理 OOV (out of vocabulary) 問題, 若語料函有新單字, 唯一的辦法是重新訓練模型以擴充字彙表. 
  10. 詞袋法 (Bag of Words, BoW) :
    詞袋法單純用單字在文本中出現的次數來表示句子, 完全忽略單字在句子中的順序與上下文. 此法是將字彙表 V 中的單字依序用 1 至字彙表長度 |V| 的整數當索引, 每個文本用一個有 |V| 個元素的一維向量表示, 其元素值為索引所對應之單字在文本中出現的次數. 以上述含有四個文本 (句子) 的迷你語料庫為例 :

    S1 : "dog bites man"
    S2 : "man bites dog"
    S3 : "dog eats meat"
    S4 : "man eats food"

    字彙表 V=['dog', 'bites', 'man', 'eats', 'meat', 'food'], 這四個文本用詞袋法表示如下 :

    S1 : [1, 1, 1, 0, 0, 0]  "dog bites man"
    S2 : [1, 1, 1, 0, 0, 0]  "man bites dog"
    S3 : [1, 0, 0, 1, 1, 0]  "dog eats meat"
    S4 : [0, 0, 1, 1, 0, 1]  "man eats food"

    其中 S1 與 S2 所用單字相同, 只是詞序不同, 但詞袋向量卻一樣, 可見詞袋法無法區別詞序. 

    Scikit-learn 套件中有一個 CountVectorizer 模組可以處理 BoW 表示法, 書中範例測試如下 :

    >>> from sklearn.feature_extraction.text import CountVectorizer  
    >>> count_vect = CountVectorizer()
    >>> processed_docs = ['dog bites man', 'man bites dog', 'dog eats meat', 'man eats food']
    >>> bow_rep = count_vect.fit_transform(processed_docs)
    >>> print("Our vocabulary: ", count_vect.vocabulary_)
    Our vocabulary:  {'dog': 1, 'bites': 0, 'man': 4, 'eats': 2, 'meat': 5, 'food': 3}
    >>> print("BoW representation for 'dog bites man': ", bow_rep[0].toarray())
    BoW representation for 'dog bites man':  [[1 1 0 0 1 0]]
    >>> print("BoW representation for 'man bites dog: ",bow_rep[1].toarray())
    BoW representation for 'man bites dog:  [[1 1 0 0 1 0]]
    >>> print("BoW representation for 'man bites dog: ",bow_rep[2].toarray())
    BoW representation for 'man bites dog:  [[0 1 1 0 0 1]]
    >>> print("BoW representation for 'man bites dog: ",bow_rep[3].toarray())
    BoW representation for 'man bites dog:  [[0 0 1 1 1 0]]

    由於字彙表中的單字順序與上面不同, 所建立的向量也不同. S1 與 S2 所含單字相同, 詞袋向量也相同 (儘管詞序不同).

    接下來測試 OOV 情況, 呼叫 transform() 方法來將文本轉換成詞袋向量 :

    >>> temp = count_vect.transform(["dog and dog are friends"])
    >>> print("Bow representation for 'dog and dog are friends':", temp.toarray())
    Bow representation for 'dog and dog are friends': [[0 2 0 0 0 0]]

    此例 dog 出現兩次, 故對應之索引值為值為 2, 而 and, are, friends 三字不在字彙表中, 所以向量中無法表示其出現頻率. 


沒有留言 :