如何對非結構化文本數據進行特徵工程操作?這裡有妙招!
本文是英特爾數據科學家 Dipanjan Sarkar 在 Medium 上發布的「特徵工程」博客續篇。在本系列的前兩部分中,作者介紹了連續數據的處理方法和離散數據的處理方法。本文則開始了一個新的主題,非結構化文本數據的傳統處理方法。AI 研習社對原文(http://mrw.so/3OjXRn)進行了編譯。
文本數據通常是由表示單詞、句子,或者段落的文本流組成。由於文本數據非結構化(並不是整齊的格式化的數據表格)的特徵和充滿雜訊的本質,很難直接將機器學習方法應用在原始文本數據中。在本文中,我們將通過實踐的方法,探索從文本數據提取出有意義的特徵的一些普遍且有效的策略,提取出的特徵極易用來構建機器學習或深度學習模型。
研究動機
想要構建性能優良的機器學習模型,特徵工程必不可少。有時候,可能只需要一個優秀的特徵,你就能贏得 Kaggle 挑戰賽的勝利!對於非結構化的文本數據來說,特徵工程更加重要,因為我們需要將文本流轉化為機器學習演算法能理解的數字表示。即使現在有高級的自動化特徵工程,在把它們當作「黑盒子」應用之前,我們仍有必要去了解不同特徵工程策略背後的核心思想。永遠記住,「如果有人給了你一套修房子的工具,你應該知道什麼時候該用電鑽,什麼時候該用鎚子!」
理解文本數據
我們雖然能夠獲得具有結構數據屬性的文本數據,但它們為結構化數據,並不在今天的討論範圍之內。
在本文中,我們討論以單詞、短語、句子和整個文檔的形式展現的文本流。從本質上講,文本確實有一些句法結構,比如單片語成了短語,短語組成了句子,句子又組合成了段落。然而,與結構化數據集中固定的數據維度相比,文本文檔沒有固定的結構,因為單詞有眾多的選擇,每個句子的長度也是可變的。本文就是一個很典型的案例。
特徵工程的策略
下面是一些流行且有效的處理文本數據的策略,這些方法也能應用在下游的機器學習系統中,用於提取有用的特徵。大家可以在 GitHub(http://mrw.so/1Kyr6M) 中查看本文使用的所有代碼。
首先載入一些基本的依賴關係和設置:
下面是文檔中的語料庫,本文大部分內容都是基於該數據集的分析。語料庫(http://mrw.so/3l3BUu)通常是屬於一個或多個主題的文檔的集合。
corpus = ["The sky is blue and beautiful.",
"Love this blue and beautiful sky!",
"The quick brown fox jumps over the lazy dog.",
"A king"s breakfast has sausages, ham, bacon, eggs, toast and beans",
"I love green eggs, ham, sausages and bacon!",
"The brown fox is quick and the blue dog is lazy!",
"The sky is very blue and the sky is very beautiful today",
"The dog is lazy but the brown fox is quick!"
]
labels = ["weather", "weather", "animals", "food", "food", "animals", "weather", "animals"]
corpus = np.array(corpus)
corpus_df = pd.DataFrame({"Document": corpus,
"Category": labels})
corpus_df = corpus_df[["Document", "Category"]]
corpus_df
本文中應用的語料庫案例
可以看到,我們已經從語料庫中提取出幾個不同類別的文檔。在討論特徵工程之前,一如往常,首先得做數據預處理,刪除一些不必要的字元、符號和標記。
文本預處理
有很多種對文本數據進行清洗和預處理的方法。下面我將重點介紹在自然語言處理(NLP)流程中大量使用的方法。
刪除標籤:文本中通常會包含一些不必要的內容,比如 HTML 標籤,這在分析文本時並沒有太多價值。BeautifulSoup 庫提供了清理標籤的函數。
清理重音字元:在許多文本語料庫中,特別是在處理英文時,通常會遇到重音字元 / 字母。因此我們要確保將這些字元轉換為標準的 ASCII 字元。一個簡單的例子就是將 é 轉換成 e。
拓展縮寫:在英文中,縮寫基本上是單詞或者音節的縮減版。縮減版通常是刪除某些單詞或者短語中特定的字母和聲音而來。舉例來說,do not 和 don"t , I would 和 I"d。將縮寫單詞轉換為完整的原始形式有助於文本的標準化。
刪除特殊字元:特殊字元和非字母數字的符號通常會增加額外雜訊。通常,可以通過簡單的正則表達式來實現這一點。
詞幹提取和詞性還原:可以利用詞幹創造新的辭彙,例如通過附加前綴和後綴等詞綴來創造新的單詞。這被稱為詞性變化。詞幹提取是將這個過程反過來。一個簡單的例子是單詞:WATCHES, WATCHING, 和 WATCHED,這些單詞都把 WATCH 作為詞根。詞性還原與詞幹提取很相似,通過移除詞綴以得到單詞的基本形式。然而在詞性還原里,單詞的基本形式是詞根(root word),而不是詞幹(root stem)。其不同之處在於詞根(root word)總是字典上正確的詞(即出現在詞典中),但詞幹並不是這樣。
去除無用詞:在從文本中構建有意義的特徵時,沒有意義的詞被稱為無用詞。如果你在一個語料庫中做一個簡單的詞頻分析,這些無用詞通常會以最大的頻率出現。像 a , an 這樣的詞被認為是無用詞。但是實際上並沒有明確通用的無用詞表,我們通常使用 nltk 的標準英語無用詞表。大家也可以根據特定的需要添加無用詞。
除此之外,還可以使用其他的標準操作,比如標記化、刪除多餘的空格、文本大寫轉換為小寫,以及其他更高級的操作,例如拼寫更正、語法錯誤更正、刪除重複字元等。
由於本文的重點是特徵工程,我們將構建一個簡單的文本預處理程序,其重點是刪除特殊字元、多餘的空格、數字、無用詞以及語料庫的大寫轉小寫。
wpt = nltk.WordPunctTokenizer()
def normalize_document(doc):
# lower case and remove special characterswhitespaces
doc = re.sub(r"[^a-zA-Zs]", "", doc, re.I|re.A)
doc = doc.lower()
doc = doc.strip()
# tokenize document
tokens = wpt.tokenize(doc)
# filter stopwords out of document
filtered_tokens = [token for token in tokens if token not in stop_words]
# re-create document from filtered tokens
doc = " ".join(filtered_tokens)
return doc
normalize_corpus = np.vectorize(normalize_document)
一旦搭建好基礎的預處理流程,我們就可以將它應用在語料庫中了。
norm_corpus = normalize_corpus(corpus)
norm_corpus
Output
------
array(["sky blue beautiful", "love blue beautiful sky",
"quick brown fox jumps lazy dog",
"kings breakfast sausages ham bacon eggs toast beans",
"love green eggs ham sausages bacon",
"brown fox quick blue dog lazy",
"sky blue sky beautiful today",
"dog lazy brown fox quick"],
dtype="
上面的輸出結果應該能讓大家清楚的了解樣本文檔在預處理之後的樣子。現在我們來開始特徵工程吧!
詞袋模型(Bag of Word)
這也許是非結構化文本中最簡單的向量空間表示模型。向量空間是表示非結構化文本(或其他任何數據)的一種簡單數學模型,向量的每個維度都是特定的特徵 / 屬性。詞袋模型將每個文本文檔表示為數值向量,其中維度是來自語料庫的一個特定的詞,而該維度的值可以用來表示這個詞在文檔中的出現頻率、是否出現(由 0 和 1 表示),或者加權值。將這個模型叫做詞袋模型,是因為每個文檔可以看作是裝著單詞的袋子,而無須考慮單詞的順序和語法。
from sklearn.feature_extraction.text import CountVectorizer
cv = CountVectorizer(min_df=0., max_df=1.)
cv_matrix = cv.fit_transform(norm_corpus)
cv_matrix = cv_matrix.toarray()
cv_matrix
可以看到,文檔已經被轉換為數字向量,這樣每個文檔都由上述特徵矩陣中的一個向量(行)表示。下面的代碼有助於以一種更易理解的格式來表示這一點。
# get all unique words in the corpus
vocab = cv.get_feature_names()
# show document feature vectors
pd.DataFrame(cv_matrix, columns=vocab)
詞袋模型的文檔特徵向量
上面的表格應該更能助於理解!可以清楚地看到,特徵向量中每個列(維度)都代表一個來自語料庫的單詞,每一行代表一個文檔。單元格中的值表示單詞(由列表示)出現在特定文檔(由行表示)中的次數。因此,如果一個文檔語料庫是由 N 個單片語成,那麼這個文檔可以由一個N 維向量表示。
N 元詞袋模型(Bag of N-Gram Model)
一個單詞只是一個標記,通常被稱為單元(unigram)或者一元(1-gram)。我們已經知道,詞袋模型不考慮單詞的順序。但是如果我們也想要考慮序列中出現的短語或者辭彙集合呢?N 元模型能夠幫我們實現這一點。N-Gram 是來自文本文檔的單詞記號的集合,這些記號是連續的,並以序列的形式出現。二元表示階數為二的 N-Gram,也就是兩個單詞。同理三元表示三個單詞。N 元詞袋模型是普通詞袋模型的一種拓展,使得我們可以利用基於 N 元的特徵。下面的示例展示了文檔中二元的特徵向量。
# you can set the n-gram range to 1,2 to get unigrams as well as bigrams
bv = CountVectorizer(ngram_range=(2,2))
bv_matrix = bv.fit_transform(norm_corpus)
bv_matrix = bv_matrix.toarray()
vocab = bv.get_feature_names()
pd.DataFrame(bv_matrix, columns=vocab)
使用二元詞袋模型的特徵向量
在上面的例子中,每個二元特徵由兩個單片語成,其中的值表示這個二元片語在文檔中出現的次數。
TF-IDF 模型
在大型語料庫中使用詞袋模型可能會出現一些潛在的問題。由於特徵向量是基於詞的頻率,某些單詞可能會在文檔中頻繁出現,這可能會在特徵集上掩蓋掉其他單詞。TF-IDF 模型試圖通過縮放或者在計算中使用歸一化因子來解決這個問題。TF-IDF 即 Term Frequency-Inverse Document Frequency,在計算中結合了兩種度量:詞頻(Term Frequency)和逆文檔頻率(Inverse Document Frequency)。這種技術是為搜索引擎中查詢排序而開發的,現在它是信息檢索和 NLP 領域中不可或缺的模型。
在數學上,TF-IDF 可以定義為:tfidf = tf x idf,也可以進一步拓展為下面的表示:
在這裡,tfidf(w, D)表示單詞w在文檔D中的 TF-IDF 分數。Tf(w,D)項表示單詞w在文檔D中的詞頻,這個值可以從詞袋模型中獲得。idf(w,D)項是單詞w的逆文檔頻率,可以由語料庫中所有文檔的總數量C除以單詞w的文檔頻率df(w)的 log 值得到,其中文檔頻率是指語料庫中文檔出現單詞w的頻率。這種模型有多種變種,但是給出的最終結果都很相似。下面在語料庫中使用這個模型吧!
from sklearn.feature_extraction.text import TfidfVectorizer
tv = TfidfVectorizer(min_df=0., max_df=1., use_idf=True)
tv_matrix = tv.fit_transform(norm_corpus)
tv_matrix = tv_matrix.toarray()
vocab = tv.get_feature_names()
pd.DataFrame(np.round(tv_matrix, 2), columns=vocab)
基於 TF-IDF 模型的文檔特徵向量
基於 TF-IDF 的特徵向量與原始的詞袋模型相比,展示出了縮放和歸一化的特性。想要進一步深入了解該模型的讀者可以參考 Text Analytics with Python(http://mrw.so/2bZDIe) 的 181 頁。
文檔相似性
文檔相似性是使用從詞袋模型或者 tf-idf 模型中提取出的特徵,基於距離或者相似度度量判斷兩個文檔相似程度的過程。
因此,可以使用在上一部分中提到的 tf-idf 模型提取出的特徵,用其來生成新的特徵。這些特徵在搜索引擎、文檔聚類以及信息檢索等領域發揮著重要作用。
語料庫中的配對文檔相似性需要計算語料庫中每兩個文檔對的文檔相似性。因此,如果一個語料庫中有 C 個文檔,那麼最終會得到一個 C*C 的矩陣,矩陣中每個值代表了該行和該列的文檔對的相似度分數。可以用幾種相似度和距離度量計算文檔相似度。其中包括餘弦距離 / 相似度、歐式距離、曼哈頓距離、BM25 相似度、jaccard 距離等。在我們的分析中,我們將使用最流行和最廣泛使用的相似度度量:餘弦相似度,並根據 TF-IDF 特徵向量比較文檔對的相似度。
similarity_matrix = cosine_similarity(tv_matrix)
similarity_df = pd.DataFrame(similarity_matrix)
similarity_df
文檔對的相似性矩陣 (餘弦相似度)
餘弦相似度給出了表示兩個文檔特徵向量之間角度的餘弦值的度量。兩個文檔特徵向量之間的角度越低,兩個文檔的相似度就越高,如下圖所示:
仔細觀察相似度矩陣可以清楚地看出,文檔(0,1 和 6),(2,5 和 7)之間非常相似,文檔 3 和 4 略微相似。這表明了這些相似的文檔一定具有一些相似特徵。這是分組或聚類的一個很好的案例,可以通過無監督的學習方法來解決,特別是當需要處理數百萬文本文檔的龐大語料庫時。
具有相似特徵的文檔聚類
聚類是利用無監督學習的方法,將數據點 (本場景中即文檔) 分類到組或者 cluster 中。我們將在這裡利用一個無監督的層次聚類演算法,通過利用我們之前生成的文檔相似性特徵,將我們的玩具語料庫中的類似文檔聚合到一起。有兩種類型的層次聚類方法,分別是凝聚方法(agglomerative)和分裂方法(divisive)。這裡將會使用凝聚聚類演算法,這是一種自下而上(bottom up)的層次聚類演算法,最開始每個文檔的單詞都在自己的類中,根據測量數據點之間的距離度量和連接準則(linkage criterion),將相似的類連續地合併在一起。下圖展示了一個簡單的描述。
連接準則決定了合併策略。常用的連接準則有 Ward, Complete linkage, Average linkage 等等。這些標準在將一對 cluster 合併在一起(文檔中低層次的類聚類成高層次的)時是非常有用的,這是通過最優化目標函數實現的。我們選擇 Ward 最小方差作為連接準則,以最小化總的內部聚類方差。由於已經有了相似特徵,我們可以直接在樣本文檔上構建連接矩陣。
Z = linkage(similarity_matrix, "ward")
pd.DataFrame(Z, columns=["DocumentCluster 1", "DocumentCluster 2",
"Distance", "Cluster Size"], dtype="object")
我們語料庫的連接矩陣
如果仔細查看連接矩陣,可以看到連接矩陣的每個步驟(行)都告訴了我們哪些數據點(或者 cluster)被合併在一起。如果有 n 個數據點,那麼連接矩陣 Z 將是(n-1)*4 的形狀,其中 Z[i] 表示在步驟 i 合併了哪些 cluster。每行有四個元素,前兩個元素是數據點或 cluster 的名稱,第三個元素是前兩個元素(數據點或 cluster)之間的距離,最後一個元素是合併完成後 cluster 中元素 / 數據點的總數。大家可以參考 scipy 文檔(http://mrw.so/2wJn1V),其中有詳細解釋。
下面,把這個矩陣看作一個樹狀圖,以更好地理解元素!
plt.figure(figsize=(8, 3))
plt.title("Hierarchical Clustering Dendrogram")
plt.xlabel("Data point")
plt.ylabel("Distance")
dendrogram(Z)
plt.axhline(y=1.0, c="k", ls="--", lw=0.5)
可以看到每個數據點是如何從一個單獨的簇開始,慢慢與其他數據點合併形成集群的。從顏色和樹狀圖的更高層次來看,如果考慮距離度量為 1.0(由虛線表示)或者更小,可以看出模型已經正確識別了三個主要的聚類。利用這個距離,我們可以得到集群的標籤。
max_dist = 1.0
cluster_labels = fcluster(Z, max_dist, criterion="distance")
cluster_labels = pd.DataFrame(cluster_labels, columns=["ClusterLabel"])
pd.concat([corpus_df, cluster_labels], axis=1)
可以清楚地看到,我們的演算法已經根據分配給它們的標籤,正確識別了文檔中的三個不同類別。這應該能夠給大家一個關於如何使用 TF-IDF 特徵來建立相似度特徵的思路。大家可以用這種處理流程來進行聚類。
主題模型
也可以使用一些摘要技術從文本文檔中提取主題或者基於概念的特徵。主題模型圍繞提取關鍵主題或者概念。每個主題可以表示為文檔語料庫中的一個詞袋或者一組詞。總之,這些術語表示特定的話題、主題或概念,憑藉這些單詞所表達的語義含義,可以輕鬆將每個主題與其他主題區分開來。這些概念可以從簡單的事實、陳述到意見、前景。主題模型在總結大量文本來提取和描繪關鍵概念時非常有用。它們也可用於從文本數據中捕捉潛在的特徵。
主題建模有很多種方法,其中大多涉及到某種形式的矩陣分解。比如隱含語義索引(Latent Semantic Indexing, LSI)就使用了奇異值分解。這裡將使用另一種技術:隱含狄利克雷分布(Latent Dirichlet Allocation, LDA),它使用了生成概率模型,其中每個文檔由幾個主題組合而成,每個術語或單詞可以分配給某個主題。這與基於 pLSI(probabilistic LSI)的模型很類似。在 LDA 的情況下,每個隱含主題都包含一個狄利克雷先驗。
這項技術背後的數學原理相當複雜,所以我會試著總結一下,而不是羅列很多讓人厭倦的細節。我建議讀者可以看看 Christine Doig 的一個優秀的演講(http://mrw.so/4vDtQL),深入了解一下。
上圖中的黑色框表示利用前面提到的參數,從 M 個文檔中提取 K 個主題的核心演算法。下面的步驟是對演算法的解釋。
初始化必要的參數。
隨機初始化文檔,將每個單詞分配到 K 個主題中去。
按照如下方法迭代
對於每個文檔 D:
a) 對於文檔中的單詞 W:
i. 對於主題 T:
計算 P(T|D), 表示文檔 D 中單詞分配給 T 主題的比例。
計算 P(W|T),表示在所有文檔中,主題 T 包含單詞 W 的比例。
ii. 通過計算概率 P(T|D)*P(W|T) 重新分配單詞 W 的主題 T。
運行幾個迭代之後,就能獲得混合了每個文檔的主題,然後就可以根據指向某個主題的單詞生成文檔的主題。像 gensim 或者 scikit-learn 這樣的框架,使得我們能夠利用 LDA 模型來生成主題。
大家應該記住,當 LDA 應用於文檔 - 單詞矩陣(TF-IDF 或者詞袋特徵矩陣)時,它會被分解為兩個主要部分:
文檔 - 主題矩陣,也就是我們要找的特徵矩陣
主題 - 單詞矩陣,能夠幫助我們查看語料庫中潛在的主題
使用 scikit-learn 可以得到如下的文檔 - 主題矩陣。
from sklearn.decomposition import LatentDirichletAllocation
lda = LatentDirichletAllocation(n_topics=3, max_iter=10000, random_state=0)
dt_matrix = lda.fit_transform(cv_matrix)
features = pd.DataFrame(dt_matrix, columns=["T1", "T2", "T3"])
features
可以清楚地看到哪些文檔對上述輸出中的三個主題貢獻最大,可以通過如下的方式查看主題及其組成部分。
tt_matrix = lda.components_
for topic_weights in tt_matrix:
topic = [(token, weight) for token, weight in zip(vocab, topic_weights)]
topic = sorted(topic, key=lambda x: -x[1])
topic = [item for item in topic if item[1] > 0.6]
print(topic)
print()
可以看到,由於組成術語不同,很容易區分這三個主題。第一個在討論天氣,第二個關於食物,最後一個關於動物。主題建模的主題數量選擇是一門完整的課題,既是一門藝術,也是一門科學。獲得最優主題數量的方法有很多,這些技術既複雜又繁瑣,這裡就不展開討論了。
使用主題模型特徵的文檔聚類
這裡使用 LDA 法從詞袋模型特徵構建主題模型特徵。現在,我們可以利用獲得的文檔單詞矩陣,使用無監督的聚類演算法,對文檔進行聚類,這與我們之前使用的相似度特徵進行聚類類似。
這次我們使用非常流行的基於分區的聚類方法——K-means 聚類,根據文檔主題模型特徵表示,進行聚類或分組。在 K-means 聚類法中,有一個輸入參數 K,它制定了使用文檔特徵輸出的聚類數量。這種聚類方法是一種基於中心的聚類方法,試圖將這些文檔聚類為等方差的類。這種方法通過最小化類內平方和來創建聚類。選擇出最優的 K 的方法有很多,比如誤差平方和度量,輪廓係數(Silhouette Coefficients)和 Elbow method。
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=0)
km.fit_transform(features)
cluster_labels = km.labels_
cluster_labels = pd.DataFrame(cluster_labels, columns=["ClusterLabel"])
pd.concat([corpus_df, cluster_labels], axis=1)
從上面的輸出中可以看到,文檔的聚類分配完全正確。
未來會涉及到的高級策略
在這篇文章沒有涉及近期出現的一些關於文本數據特徵工程的高級方法,包括利用深度學習模型來提取單詞特徵的方法。我們將在本系列的下一部分中深入探討這些模型,並詳細介紹 Word2Vec(http://mrw.so/1e3O2d) 和 GloVe(http://mrw.so/1s38eg) 等流行的單詞嵌入模型,敬請期待!
總結
這些例子應該能有助於大家理解文本數據特徵工程的一些通用策略。本文中介紹的是基於數學概念、信息檢索和自然語言處理的傳統策略,這些久經考驗的方法在各種數據集和問題上都表現優異。在下一篇文章中,我將詳細介紹如何利用深度學習模型進行文本數據特徵工程。
對連續數據特徵工程感興趣的讀者,請查看本系列第一部分!
對離散數據特徵工程感興趣的讀者,請查看本系列第二部分!
本文中所使用的所有代碼和數據集都可以從 GitHub(http://mrw.so/1Kyr6M) 中訪問。代碼也可以作為 Jupyter(http://mrw.so/1iQMTh) 筆記本使用。
NLP 工程師入門實踐班:基於深度學習的自然語言處理
三大模塊,五大應用,手把手快速入門 NLP
海外博士講師,豐富項目經驗
演算法 + 實踐,搭配典型行業應用
隨到隨學,專業社群,講師在線答疑
新人福利
關注 AI 研習社(okweiwu),回復1領取
【超過 1000G 神經網路 / AI / 大數據,教程,論文】
深度學習在文本分類中的應用


※深度學習對話系統實戰篇-簡單 chatbot 代碼實現
※自己動手寫深度學習模型之全連接神經網路
TAG:AI研習社 |