用Python實現文件聚類

Ree Ray發表於2016-06-28

title

在本教程中,我會利用 Python 來說明怎樣聚類一系列的文件。我所演示的例項會識別出 top 100 電影的(來自 IMDB 列表)劇情簡介的隱藏結構。關於這個例子的詳細討論在初始版本里。本教程包括:

整個專案在我的 github repo 都可以找到。其中‘cluster_analysis ‘工作簿是一個完整的版本;‘cluster_analysis_web’ 為了建立教程則經過了刪減。歡迎下載程式碼並使用‘cluster_analysis’ 進行單步除錯(step through)。

如果你有任何問題,歡迎用推特來聯絡我 @brandonmrose

在此之前,我先在前面匯入所有需要用到的庫

出於走查的目的,想象一下我有 2 個主要的列表:

  • ‘titles’:按照排名的影片名稱
  • ‘synopses’:對應片名列表的劇情簡介

我在 github 上 po 出來的完整工作簿已經匯入了上述列表,但是為了簡潔起見,我會直接使用它們。其中最最重要的是 ‘synopses’ 列表了,‘titles’ 更多是作為了標記用的。

停用詞,詞幹化與分詞

本節我將會定義一些函式對劇情簡介進行處理。首先,我載入 NLTK 的英文停用詞列表。停用詞是類似“a”,“the”,或者“in”這些無法傳達重要意義的詞。我相信除此之外還有更好的解釋。

接下來我匯入 NLTK 中的 Snowball 詞幹分析器(Stemmer)詞幹化(Stemming)的過程就是將詞打回原形。

以下我定義了兩個函式:

  • tokenize_and_stem:對每個詞例(token)分詞(tokenizes)(將劇情簡介分割成單獨的詞或詞例列表)並詞幹化
  • tokenize_only: 分詞即可

我利用上述兩個函式建立了一個重要的字典,以防我在後續演算法中需要使用詞幹化後的詞(stems)。出於展示的目的,後面我又會將這些詞轉換回它們原本的的形式。猜猜看會怎樣,我實在想試試看!

接下來我會使用上述詞幹化/分詞和分詞函式遍歷劇情簡介列表以生成兩個詞彙表:經過詞幹化和僅僅經過分詞後。

利用上述兩個列表,我建立了一個 pandas 的 DataFrame,以詞幹化後的詞彙表作為索引,分詞後的詞為列。這麼做便於觀察詞幹化後的詞轉換回完整的詞例。以下展示詞幹化後的詞變回原詞例是一對多(one to many)的過程:詞幹化後的“run”能夠關聯到“ran”,“runs”,“running”等等。在我看來這很棒——我非常願意將我需要觀察的詞幹化過後的詞轉換回第一個聯想到的詞例。

你會注意到有些重複的地方。我可以把它清理掉,不過鑑於 DataFrame 只有 312209 項,並不是很龐大,可以用 stem-index 來觀察詞幹化後的詞。

Tf-idf 與文字相似度

下面,我定義詞頻-逆向檔案頻率(tf-idf)的向量化引數,把劇情簡介列表都轉換成 tf-idf 矩陣。

為了得到 TF-IDF 矩陣,首先計算詞在文件中的出現頻率,它會被轉換成文件-詞矩陣(dtm),也叫做詞頻(term frequency)矩陣。dtm 的例子如下圖所示:

tf-idf

接著使用 TF-IDF 權重:某些詞在某個文件中出現頻率高,在其他文中卻不常出現,那麼這些詞具有更高的 TF-IDF 權重,因為這些詞被認為在相關文件中攜帶更多資訊。

注意我下面定義的幾個引數:

  • max_df:這個給定特徵可以應用在 tf-idf 矩陣中,用以描述單詞在文件中的最高出現率。假設一個詞(term)在 80% 的文件中都出現過了,那它也許(在劇情簡介的語境裡)只攜帶非常少資訊。
  • min_df:可以是一個整數(例如5)。意味著單詞必須在 5 個以上的文件中出現才會被納入考慮。在這裡我設定為 0.2;即單詞至少在 20% 的文件中出現 。因為我發現如果我設定更小的 min_df,最終會得到基於姓名的聚類(clustering)——舉個例子,好幾部電影的簡介劇情中老出現“Michael”或者“Tom”這些名字,然而它們卻不攜帶什麼真實意義。
  • ngram_range:這個引數將用來觀察一元模型(unigrams),二元模型( bigrams) 和三元模型(trigrams)。參考n元模型(n-grams)

“terms” 這個變數只是 tf-idf 矩陣中的特徵(features)表,也是一個詞彙表。

dist 變數被定義為 1 – 每個文件的餘弦相似度。餘弦相似度用以和 tf-idf 相互參照評價。可以評價全文(劇情簡介)中文件與文件間的相似度。被 1 減去是為了確保我稍後能在歐氏(euclidean)平面(二維平面)中繪製餘弦距離。

注意 dist 可以用以評估任意兩個或多個劇情簡介間的相似度。

K-means 聚類

下面開始好玩的部分。利用 tf-idf 矩陣,你可以跑一長串聚類演算法來更好地理解劇情簡介集裡的隱藏結構。我首先用 k-means 演算法。這個演算法需要先設定聚類的數目(我設定為 5)。每個觀測物件(observation)都會被分配到一個聚類,這也叫做聚類分配(cluster assignment)。這樣做是為了使組內平方和最小。接下來,聚類過的物件通過計算來確定新的聚類質心(centroid)。然後,物件將被重新分配到聚類,在下一次迭代操作中質心也會被重新計算,直到演算法收斂。

跑了幾次這個演算法以後我發現得到全域性最優解(global optimum)的機率要比區域性最優解(local optimum)大。

利用 joblib.dump pickle 模型(model),一旦演算法收斂,過載模型並分配聚類標籤(labels)。

下面,我建立了一個字典,包含片名,排名,簡要劇情,聚類分配,還有電影型別(genre)(排名和型別是從 IMDB 上爬下來的)。

為了方便起見,我將這個字典轉換成了 Pandas DataFrame。我是 Pandas 的腦殘粉,我強烈建議你瞭解一下它驚豔的功能。這些我下面就會使用到,但不會深入。

clusters 4clusters 0 的排名最低,說明它們包含的影片在 top 100 列表中相對沒那麼棒。

在這選取 n(我選 6 個) 個離聚類質心最近的詞對聚類進行一些好玩的索引(indexing)和排列(sorting)。這樣可以更直觀觀察聚類的主要主題。

聚類中的前幾項:

聚類 0 中的單詞: family, home, mother, war, house, dies,

聚類 0 中的片名: Schindler’s List, One Flew Over the Cuckoo’s Nest, Gone with the Wind, The Wizard of Oz, Titanic, Forrest Gump, E.T. the Extra-Terrestrial, The Silence of the Lambs, Gandhi, A Streetcar Named Desire, The Best Years of Our Lives, My Fair Lady, Ben-Hur, Doctor Zhivago, The Pianist, The Exorcist, Out of Africa, Good Will Hunting, Terms of Endearment, Giant, The Grapes of Wrath, Close Encounters of the Third Kind, The Graduate, Stagecoach, Wuthering Heights,

聚類 1 中的單詞: police, car, killed, murders, driving, house,

聚類 1 中的片名: Casablanca, Psycho, Sunset Blvd., Vertigo, Chinatown, Amadeus, High Noon, The French Connection, Fargo, Pulp Fiction, The Maltese Falcon, A Clockwork Orange, Double Indemnity, Rebel Without a Cause, The Third Man, North by Northwest,

聚類 2 中的單詞: father, new, york, new, brothers, apartments,

聚類 2 中的片名: The Godfather, Raging Bull, Citizen Kane, The Godfather: Part II, On the Waterfront, 12 Angry Men, Rocky, To Kill a Mockingbird, Braveheart, The Good, the Bad and the Ugly, The Apartment, Goodfellas, City Lights, It Happened One Night, Midnight Cowboy, Mr. Smith Goes to Washington, Rain Man, Annie Hall, Network, Taxi Driver, Rear Window,

聚類 3 中的單詞: george, dance, singing, john, love, perform,

聚類 3 中的片名: West Side Story, Singin’ in the Rain, It’s a Wonderful Life, Some Like It Hot, The Philadelphia Story, An American in Paris, The King’s Speech, A Place in the Sun, Tootsie, Nashville, American Graffiti, Yankee Doodle Dandy,

聚類 4 中的單詞: killed, soldiers, captain, men, army, command,

聚類 4 中的片名: The Shawshank Redemption, Lawrence of Arabia, The Sound of Music, Star Wars, 2001: A Space Odyssey, The Bridge on the River Kwai, Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb, Apocalypse Now, The Lord of the Rings: The Return of the King, Gladiator, From Here to Eternity, Saving Private Ryan, Unforgiven, Raiders of the Lost Ark, Patton, Jaws, Butch Cassidy and the Sundance Kid, The Treasure of the Sierra Madre, Platoon, Dances with Wolves, The Deer Hunter, All Quiet on the Western Front, Shane, The Green Mile, The African Queen, Mutiny on the Bounty,

多維尺度分析(Multidimensional scaling)

利用下面多維尺度分析(MDS)的程式碼將距離矩陣轉化為一個二維陣列。我並不想假裝我很瞭解MDS,不過這個演算法很管用。另外可以用 特徵降維(principal component analysis) 來完成這個任務。

視覺化文件聚類

本節中,我會演示怎樣利用 matplotlib 和 mpld3(將 matplotlib 封裝成 D3.js)來實現文件聚類的視覺化。

首先,我定義了一些字典,讓聚類的編號和聚類繪色,聚類名稱一一對應。其中聚類對應的名稱是從離聚類質心最近的單詞中挑選出來的。

下面我會用 matplotlib 來繪製彩色的帶標籤的觀測物件(影片,片名)。關於 matplotlib 繪圖我不想討論太多,但我儘可能提供一些有用的註釋。

k-means

繪製的聚類分佈圖看起來不錯,但是重疊在一起的標籤真是亮瞎了眼。因為之前使用過 D3.js,所以我知道有個解決方案是基於瀏覽器和 javascript 互動的。所幸我最近偶然發現了 mpld3,是基於 matplotlib 的 D3 封裝。Mpld3 主要可以讓你使用 matplotlib 的語法實現網頁互動。它非常容易上手,當你遇到感興趣的內容,滑鼠停駐的時候,利用高效的介面可以新增氣泡提示。

另外,它還提供了縮放和拖動這麼炫的功能。以下的 javascript 片段主要自定義了縮放和拖動的位置。別太擔心,實際上你用不到它,但是稍後匯出到網頁的時候有利於格式化。你唯一想要改變的應該是藉助 x 和 y 的 attr 來改變工具欄的位置。

下面是對於互動式散點圖的實際操作。我同樣不會深入這個問題因為是直接從 mpld3 的例程移植過來的。雖然我用 pandas 對聚類進行了歸類,但它們一一迭代後會分佈在散點圖上。和原生 D3 相比,用 mpld3 來做這項工作並且嵌入到 python 的工作簿中簡單多了。如果你看了我網站上的其它內容,你就知道我有多麼愛 D3 了。但以後一些基本的互動我可能還是會用 mpld3。

記住 mpld3 還可以自定義 CSS,像我設計的字型,座標軸還有散點圖左邊的間距(margin)。

mpld3

(譯者按:因為無法插入 js,所以對原 post 截圖)

文件層次聚類

到目前為止我已經成功用 k-means 演算法將文件聚類並繪製了結果,下面我想嘗試其它聚類演算法。我選擇了 Ward 聚類演算法 ,因為它可以進行層次聚類。Ward 聚類屬於凝聚(agglomerative)聚類演算法,亦即在每個處理階段,聚類間兩點距離最小的會被合併成一個聚類。我用之前計算得到的餘弦距離矩陣(dist)來計算 linkage_matrix,等會我會把它繪製在樹狀圖中。

值得注意的是這個演算法返回了 3 組主要的聚類,最大聚類又被分成了 4 個主要的子聚類。其中紅色標註的聚類包含了多部“Killed, soldiers, captain”主題下的影片。BraveheartGladiator* 是我最喜歡的兩部片子,它們都在低層(low-level)的聚類裡。

ward

隱含狄利克雷分佈

本節的重點放在如何利用 隱含狄利克雷分佈(LDA)發掘 top 100 影片劇情簡介中的隱藏結構。LDA 是概率主題模型(probabilistic topic model),即假定文件由許多主題(topics)組成,而文件中的每個單詞都可以歸入某個主題。這兒有篇高大上的概論(overview),是關於概率主題模型的,作者是領域內的大牛之一——David Blei,在這裡可以下載 Communications of the ACM。另外,Blei 也是 LDA 論文作者之一。

在這裡我用 Gensim 包 來實現 LDA。其中劇情簡介的預處理會有些不一樣。我首先定義了個函式把專有名詞給去掉。

因為上述函式功能實現基於大寫的特性,很容易就把句子首個單詞也去掉了。所以我又寫了下面的這個函式,用到了 NLTK 的詞性標註器。然而,讓所有劇情簡介跑這個函式耗時太長了,所以我還是決定繼續用回上述的函式。

現在我要對真正的文字(去除了專有名詞,經過分詞,以及去除了停用詞)進行處理了。

下面我用 Gensim 進行特有的轉化; 我把一些極端(extreme)的單詞也給去掉了(詳情見內部註釋)。

下面執行實際模型。我將 passes 設定為 100 來保證收斂,但你可以看到我的機器花了 13 分鐘來完成這些。因為我將文字分得太細,所以基本上每步(pass)都會用到所有劇情簡介。我應該繼續優化這個問題。Gensim 支援並行(parallel)運算,當我處理更大的語料庫時,我非常樂意進行深入探索。

每個主題都由一系列的詞定義,連同一定的概率。

下面,我將每個主題轉換成了包含前 20 個詞的詞彙表。當我使用 k-means 演算法得出的 war/family 主題和更清晰的 war/epic 主題比較,你可以觀察主題分解後的相似性。

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

用Python實現文件聚類 用Python實現文件聚類

相關文章