用Python实现文档聚类

在本教程中,我会利用 Python 来说明怎样聚类一系列的文档。我所演示的实例会识别出 top 100 电影的(来自 IMDB 列表)剧情简介的隐藏结构。关于这个例子的详细讨论在初始版本里。本教程包括:

  • 对所有剧情简介分词(tokenizing)和词干化(stemming)
  • 利用 tf-idf 将语料库转换为向量空间(vector space)
  • 计算每个文档间的余弦距离(cosine distance)用以测量相似度
  • 利用 k-means 算法进行文档聚类
  • 利用多维尺度分析(multidimensional scaling)对语料库降维
  • 利用 matplotlib 和 mpld3 绘制输出的聚类
  • 对语料库进行Ward 聚类算法生成层次聚类(hierarchical clustering)
  • 绘制 Ward 树状图(Ward dendrogram)
  • 利用 隐含狄利克雷分布(LDA) 进行主题建模

整个项目在我的 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 权重,因为这些词被认为在相关文档中携带更多信息。

注意我下面定义的几个参数:

  • 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 绘图我不想讨论太多,但我尽可能提供一些有用的注释。

绘制的聚类分布图看起来不错,但是重叠在一起的标签真是亮瞎了眼。因为之前使用过 D3.js,所以我知道有个解决方案是基于浏览器和 javascript 交互的。所幸我最近偶然发现了 mpld3,是基于 matplotlib 的 D3 封装。Mpld3 主要可以让你使用 matplotlib 的语法实现网页交互。它非常容易上手,当你遇到感兴趣的内容,鼠标停驻的时候,利用高效的接口可以添加气泡提示。

另外,它还提供了缩放和拖动这么炫的功能。以下的 javascript 片段主要自定义了缩放和拖动的位置。别太担心,实际上你用不到它,但是稍后导出到网页的时候有利于格式化。你唯一想要改变的应该是借助 x 和 y 的 attr 来改变工具栏的位置。

下面是对于交互式散点图的实际操作。我同样不会深入这个问题因为是直接从 mpld3 的例程移植过来的。虽然我用 pandas 对聚类进行了归类,但它们一一迭代后会分布在散点图上。和原生 D3 相比,用 mpld3 来做这项工作并且嵌入到 python 的工作簿中简单多了。如果你看了我网站上的其它内容,你就知道我有多么爱 D3 了。但以后一些基本的交互我可能还是会用 mpld3。

记住 mpld3 还可以自定义 CSS,像我设计的字体,坐标轴还有散点图左边的间距(margin)。

(译者按:因为无法插入 js,所以对原 post 截图)

文档层次聚类

到目前为止我已经成功用 k-means 算法将文档聚类并绘制了结果,下面我想尝试其它聚类算法。我选择了 Ward 聚类算法 ,因为它可以进行层次聚类。Ward 聚类属于凝聚(agglomerative)聚类算法,亦即在每个处理阶段,聚类间两点距离最小的会被合并成一个聚类。我用之前计算得到的余弦距离矩阵(dist)来计算 linkage_matrix,等会我会把它绘制在树状图中。

值得注意的是这个算法返回了 3 组主要的聚类,最大聚类又被分成了 4 个主要的子聚类。其中红色标注的聚类包含了多部“Killed, soldiers, captain”主题下的影片。BraveheartGladiator* 是我最喜欢的两部片子,它们都在低层(low-level)的聚类里。

隐含狄利克雷分布

本节的重点放在如何利用 隐含狄利克雷分布(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
['men', 'kill', 'soldier', 'order', 'patient', 'night', 'priest', 'becom', 'new', 'speech', 'friend', 'decid', 'young', 'ward', 'state', 'front', 'would', 'home', 'two', 'father'] ["n't", 'go', 'fight', 'doe', 'home', 'famili', 'car', 'night', 'say', 'next', 'ask', 'day', 'want', 'show', 'goe', 'friend', 'two', 'polic', 'name', 'meet'] ['ask', 'meet', 'kill', 'say', 'friend', 'car', 'love', 'famili', 'arriv', "n't", 'home', 'two', 'go', 'father', 'money', 'call', 'polic', 'apart', 'night', 'hous'] ['kill', 'soldier', 'order', 'men', 'shark', 'attempt', 'offic', 'son', 'command', 'attack', 'water', 'friend', 'ask', 'fire', 'arriv', 'wound', 'die', 'battl', 'death', 'fight'] ['kill', 'water', 'two', 'plan', 'away', 'set', 'boat', 'vote', 'way', 'home', 'run', 'ship', 'would', 'destroy', 'guilti', 'first', 'attack', 'go', 'use', 'forc']
1
2
3
4
5
6
7
8
9
['men', 'kill', 'soldier', 'order', 'patient', 'night', 'priest', 'becom', 'new', 'speech', 'friend', 'decid', 'young', 'ward', 'state', 'front', 'would', 'home', 'two', 'father']
 
["n't", 'go', 'fight', 'doe', 'home', 'famili', 'car', 'night', 'say', 'next', 'ask', 'day', 'want', 'show', 'goe', 'friend', 'two', 'polic', 'name', 'meet']
 
['ask', 'meet', 'kill', 'say', 'friend', 'car', 'love', 'famili', 'arriv', "n't", 'home', 'two', 'go', 'father', 'money', 'call', 'polic', 'apart', 'night', 'hous']
 
['kill', 'soldier', 'order', 'men', 'shark', 'attempt', 'offic', 'son', 'command', 'attack', 'water', 'friend', 'ask', 'fire', 'arriv', 'wound', 'die', 'battl', 'death', 'fight']
 
['kill', 'water', 'two', 'plan', 'away', 'set', 'boat', 'vote', 'way',

你可能感兴趣的:(python,神经网络,算法,clustering,python)