處理文字資料(上):詞袋

朝南煙發表於2022-06-03

我們討論過表示資料屬性的兩種型別的特徵:連續特徵與分類特徵,前者用於描述數量,後者是固定列表中的元素。
第三種型別的特徵:文字

  • 文字資料通常被表示為由字元組成的字串。

1、用字串表示的資料型別

文字通常只是資料集中的字串,但並非所有的字串特徵都應該被當作文字來處理。

字串特徵有時可以表示分類變數。在檢視資料之前,我們無法知道如何處理一個字串特徵。

⭐四種型別的字串資料:

  • 1、分類資料

    • 分類資料(categorical data)是來自固定列表的資料。
  • 2、可以在語義上對映為類別的自由字串

    • 你向使用者提供的不是一個下拉選單,而是一個文字框,讓他們填寫自己最喜歡的顏色。
    • 許多人的回答可能是像 “黑色” 或 “藍色” 之類的顏色名稱。其他人可能會出現筆誤,使用不同的單詞拼寫(比如 “gray” 和 “grey” ),或使用更加形象的具體名稱 (比如 “午夜藍色”)。
    • 可能最好將這種資料編碼為分類變數,你可以利用最常見的條目來選擇類別,也可以自定義類別,使使用者回答對應用有意義。
  • 3、結構化字串資料

    • 手動輸入值不與固定的類別對應,但仍有一些內在的結構(structure),比如地址、人名或地名、日期、電話號碼或其他識別符號。
  • 4、文字資料

    • 例子包括推文、聊天記錄和酒店評論,還包括莎士比亞文集、維基百科的內容或古騰堡計劃收集的 50 000 本電子書。所有這些集合包含的資訊大多是由單片語成的句子。

2、示例應用:電影評論的情感分析

作為本章的一個執行示例,我們將使用由史丹佛研究員 Andrew Maas 收集的 IMDb (Internet Movie Database,網際網路電影資料庫)網站的電影評論資料集。

資料集連結:http://ai.stanford.edu/~amaas/data/sentiment/

這個資料集包含評論文字,還有一個標籤,用於表示該評論是 “正面的”(positive)還是 “負面的” (negative)。

IMDb 網站本身包含從 1 到 10 的打分。為了簡化建模,這些評論打分被歸納為一個二分類資料集,評分大於等於 7 的評論被標記為 “正面的”,評分小於等於 4 的評論被標記為 “負面的”,中性評論沒有包含在資料集中。

將資料解壓之後,資料集包括兩個獨立資料夾中的文字檔案,一個是訓練資料,一個是測試資料。每個資料夾又都有兩個子資料夾,一個叫作 pos,一個叫作 neg。

pos 資料夾包含所有正面的評論,每條評論都是一個單獨的文字檔案,neg 資料夾與之類似。scikit-learn 中有一個輔助函式可以載入用這種資料夾結構儲存的檔案,其中每個子資料夾對應於一個標籤,這個函式叫作 load_files。我們首先將 load_files 函式應用於訓練資料:

  from sklearn.datasets import load_files
  from sklearn.model_selection import train_test_split


  reviews_train = load_files("../../datasets/aclImdb/train/")
  # load_files 返回一個 Bunch 物件,其中包含訓練文字和訓練標籤

  #載入資料
  text_train,y_train = reviews_train.data,reviews_train.target

  #檢視資料
  print("type of text_train: {}".format(type(text_train)))
  print("length of text_train: {}".format(len(text_train)))
  print("text_train[6]:\n{}".format(text_train[6]))

  '''
  ```
  type of text_train: <class 'list'>
  length of text_train: 25000
  text_train[6]:
  b"This movie has a special way of telling the story, at first i found it rather odd as it jumped through time and I had no idea whats happening.<br /><br />Anyway the story line was although simple, but still very real and touching. You met someone the first time, you fell in love completely, but broke up at last and promoted a deadly agony. Who hasn't go through this? but we will never forget this kind of pain in our life. <br /><br />I would say i am rather touched as two actor has shown great performance in showing the love between the characters. I just wish that the story could be a happy ending."
  ```
  '''

?
你可以看到,text_train 是一個長度為 25 000 的列表,其中每個元素是包含一條評論的字串。我們列印出索引編號為 1 的評論。你還可以看到,評論中包含一些 HTML 換行符。雖然這些符號不太可能對機器學習模型產生很大影響,但最好在繼續下一步之前清洗資料並刪除這種格式:

  import numpy as np

  text_train = [doc.replace(b"<br />", b" ") for doc in text_train]
  #收集資料集時保持正類和反類的平衡,這樣所有正面字串和負面字串的數量相等:
  print("Samples per class (training): {}".format(np.bincount(y_train)))

  #我們用同樣的方式載入測試資料集:
  reviews_test = load_files("../../datasets/aclImdb/test/")
  text_test, y_test = reviews_test.data, reviews_test.target

  print("Number of documents in test data: {}".format(len(text_test)))
  print("Samples per class (test): {}".format(np.bincount(y_test)))

  text_test = [doc.replace(b"<br />", b" ") for doc in text_test]

  '''
  ```
  Samples per class (training): [12500 12500]
  Number of documents in test data: 25000
  Samples per class (test): [12500 12500]
  ```
  '''

我們要解決的任務如下:給定一條評論,我們希望根據該評論的文字內容對其分配一個 “正面的” 或 “負面的” 標籤。
這是一項標準的二分類任務。
但是,文字資料並不是機器學習模型可以處理的格式。
我們需要將文字的字串表示轉換為數值表示,從而可以對其應用機器學習演算法。

3、將文字資料表示為詞袋

用於機器學習的文字表示有一種最簡單的方法,也是最有效且最常用的方法,就是使用詞袋(bag-of-words)表示。

  • 使用這種表示方式時,我們捨棄了輸入文字中的大部分結構,如章節、段落、句子和格式,只計算語料庫中每個單詞在每個文字中的出現頻次。
  • 捨棄結構並僅計算單詞出現次數,這會讓腦海中出現將文字表示為 “袋” 的畫面

對於文件語料庫,計算詞袋錶示包括以下三個步驟。

  • (1)分詞(tokenization)。
    • 將每個文件劃分為出現在其中的單詞,比如按空格和標點劃分。
  • (2)構建詞表(vocabulary building)。
    • 收集一個詞表,裡面包含出現在任意文件中的所有詞, 並對它們進行編號(比如按字母順序排序)。
  • (3)編碼(encoding)。
    • 對於每個文件,計算詞表中每個單詞在該文件中的出現頻次。

3.1、將詞袋應用於玩具資料集

詞袋錶示是在 CountVectorizer 中實現的,它是一個變換器(transformer)。

我們首先將它應用於一個包含兩個樣本的玩具資料集,來看一下它的工作原理:

  bards_words =["The fool doth think he is wise,",
                "but the wise man knows himself to be a fool"]

  #匯入CountVectorizer並將其例項化
  from sklearn.feature_extraction.text import CountVectorizer

  vect = CountVectorizer()
  vect.fit(bards_words)

  #擬合 CountVectorizer 包括訓練資料的分詞與詞表的構建,我們可以通過 vocabulary_ 屬性來訪問詞表:

  print("Vocabulary size: {}".format(len(vect.vocabulary_)))
  print("Vocabulary content:\n {}".format(vect.vocabulary_))

  '''
  ```
  Vocabulary size: 13
  Vocabulary content:
   {'the': 9, 'fool': 3, 'doth': 2, 'think': 10, 'he': 4, 'is': 6, 'wise': 12, 'but': 1, 'man': 8, 'knows': 7, 'himself': 5, 'to': 11, 'be': 0}
  ```
  '''

  #我們可以呼叫 transform 方法來建立訓練資料的詞袋錶示:

  bag_of_words = vect.transform(bards_words)
  print("bag_of_words:{}".format(repr(bag_of_words)))

  '''
  ```
  bag_of_words:<2x13 sparse matrix of type '<class 'numpy.int64'>'
  	with 16 stored elements in Compressed Sparse Row format>
  ```
  '''

?

詞袋錶示儲存在一個 SciPy 稀疏矩陣中,這種資料格式只儲存非零元素。

這個矩陣的形狀為 2×13,每行對應於兩個資料點之一,每個特徵對應於詞表中的一個單詞。
這裡使用稀疏矩陣,是因為大多數文件都只包含詞表中的一小部分單詞,也就是說,特徵陣列中的大部分元素都為 0。
想想看,與所有英語單詞(這是詞表的建模物件)相比,一篇電影評論中可能出現多少個不同的單詞。
儲存所有 0 的代價很高,也浪費記憶體。

3.2、將詞袋應用於電影評論

上一節我們詳細介紹了詞袋處理過程,下面我們將其應用於電影評論情感分析的任務。
前面我們將 IMDb 評論的訓練資料和測試資料載入為字串列表(text_train 和 text_ test),現在我們將處理它們

  vect = CountVectorizer().fit(text_train)
  X_train = vect.transform(text_train)
  print("X_train:\n{}".format(repr(X_train)))

  '''
  ```
  X_train:
  <25000x74849 sparse matrix of type '<class 'numpy.int64'>'
  	with 3431196 stored elements in Compressed Sparse Row format>
  ```
  '''

X_train 是訓練資料的詞袋錶示,其形狀為 25 000×74 849,這表示詞表中包含 74 849 個 元素。資料同樣被儲存為 SciPy 稀疏矩陣。我們來更詳細地看一下這個詞表。訪問詞表的另一種方法是使用向量器(vectorizer)的 get_feature_name 方法,它將返回一個列表,每個元素對應於一個特徵:

  feature_names = vect.get_feature_names()

  #特徵數
  print("Number of features: {}".format(len(feature_names)))
  #前20個特徵
  print("First 20 features:\n{}".format(feature_names[:20]))
  #中間的特徵
  print("Features 20010 to 20030:\n{}".format(feature_names[20010:20030]))
  #間隔2000列印一個特徵
  print("Every 2000th feature:\n{}".format(feature_names[::2000]))


  '''
  ```
  Number of features: 74849
  First 20 features:
  ['00', '000', '0000000000001', '00001', '00015', '000s', '001', '003830', '006', '007', '0079', '0080', '0083', '0093638', '00am', '00pm', '00s', '01', '01pm', '02']
  Features 20010 to 20030:
  ['dratted', 'draub', 'draught', 'draughts', 'draughtswoman', 'draw', 'drawback', 'drawbacks', 'drawer', 'drawers', 'drawing', 'drawings', 'drawl', 'drawled', 'drawling', 'drawn', 'draws', 'draza', 'dre', 'drea']
  Every 2000th feature:
  ['00', 'aesir', 'aquarian', 'barking', 'blustering', 'bête', 'chicanery', 'condensing', 'cunning', 'detox', 'draper', 'enshrined', 'favorit', 'freezer', 'goldman', 'hasan', 'huitieme', 'intelligible', 'kantrowitz', 'lawful', 'maars', 'megalunged', 'mostey', 'norrland', 'padilla', 'pincher', 'promisingly', 'receptionist', 'rivals', 'schnaas', 'shunning', 'sparse', 'subset', 'temptations', 'treatises', 'unproven', 'walkman', 'xylophonist']
  ```
  '''

詞表的前 10 個元素都是數字。
所有這些數字都出現在評論中的某處,因此被提取為單詞。
大部分數字都沒有一目瞭然的語義,除了 “007”,在 電影的特定語境中它可能指的是詹姆斯 • 邦德(James Bond)這個角色。
從無意義的 “單詞” 中挑出有意義的有時很困難。
進一步觀察這個詞表,我們發現許多以 “dra” 開頭的英語單詞。

  • 對於 “draught”、“drawback” 和 “drawer”,其單數和複數形式都包含在詞表中,並且作為不同的單詞。這些單詞具有密切相關的語義,將它們作為不同的單詞進行計數(對應於不同的特徵)可能不太合適。

在嘗試改進特徵提取之前,我們先通過實際構建一個分類器來得到效能的量化度量。

  • 我們將訓練標籤儲存在 y_train 中,訓練資料的詞袋錶示儲存在 X_train 中,因此我們可以在這個資料上訓練一個分類器。

  • 對於這樣的高維稀疏資料,類似 LogisticRegression 的線性模型通常效果最好。

    from sklearn.model_selection import GridSearchCV
    
    #網格搜尋
    param_grid = {'C': [0.001, 0.01, 0.1, 1, 10]}
    grid = GridSearchCV(LogisticRegression(), param_grid, cv=5)
    grid.fit(X_train, y_train)
    
    print("Best cross-validation score: {:.2f}".format(grid.best_score_))
    print("Best parameters: ", grid.best_params_)
    
    #測試集上檢視泛化效能
    X_test = vect.transform(text_test)
    print("Test score: {:.2f}".format(grid.score(X_test, y_test)))
    
    '''
    ```
    Best cross-validation score: 0.89
    Best parameters:  {'C': 0.1}
    Test score: 0.88
    ```
    '''

相關文章