處理文本數據(上):詞袋

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

  • 文本數據通常被表示為由字元組成的字元串。

1、用字元串表示的數據類型

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

字元串特徵有時可以表示分類變數。在查看數據之前,我們無法知道如何處理一個字元串特徵。

⭐四種類型的字元串數據:

  • 1、分類數據

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

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

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

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

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

作為本章的一個運行示例,我們將使用由斯坦福研究員 Andrew Maas 收集的 IMDb (Internet Movie Database,互聯網電影資料庫)網站的電影評論數據集。

數據集鏈接://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
    ```
    '''