(纯属为了记录自己学习的点滴过程,引用资料都附在参考列表)
1 基本概念
聚类(cluster analysis )指的是将给定对象的集合划分为不同子集的过程,目标是使得每个子集内部的元素尽量相似,不同子集间的元素尽量不相似。
- 注意,单词的颗粒度(分词、新词提取、关键词提取) < 短语的颗粒度(短语提取) < 句子的颗粒度(关键句提起) < 文章颗粒度(聚类),这些无监督任务一直体现着高内聚、低耦合的原则。
2 问题
对多个文档,在文档颗粒上进行聚类;
3 解决思路
3.1 基本思想
文本聚类的基本流程分为特征提取和向量聚类两步, 如果能将文档表示为向量,就可以对其应用聚类算法。这种表示过程称为特征提取,而一旦将文档表示为向量,剩下的算法就与文档无关了。这种抽象思维无论是从软件工程的角度,还是从数学应用的角度都十分简洁有效。
3.2 文档特征提取
词袋(bag-of-words )是信息检索与自然语言处理中最常用的文档表示模型,它将文档想象为一个装有词语的袋子, 通过袋子中每种词语的计数等统计量将文档表示为向量。比如下面的例子:
人 吃 鱼。
美味 好 吃!
统计词频后如下:
人=1
吃=2
鱼=1
美味=1
好=1
文档经过该词袋模型得到的向量表示为[1,2,1,1,1],这 5 个维度分别表示这 5 种词语的词频。
一般选取训练集文档的所有词语构成一个词表,词表之外的词语称为 oov,不予考虑。一旦词表固定下来,假设大小为 N。则任何一个文档都可以通过这种方法转换为一个N维向量。比如“人 吃 大 鱼”这个文档,它的词频统计为:
人=1
吃=1
鱼=1
美味=0
好=0
那么它的词袋向量就是[1,1,1,0,0],其中后两个维度上的词语没有出现,所以都是0。而“大”这个词属于OOV,散落在词袋之外,所以不影响词袋向量。
词袋模型不考虑词序,也正因为这个原因,词袋模型损失了词序中蕴含的语义,比如,对于词袋模型来讲,“人吃鱼”和“鱼吃人”是一样的,这就荒谬了,但在实际工程中,词袋模型依然是一个很难打败的基线模型。
词袋中的统计量
- 布尔词频: 词频非零的话截取为1,否则为0,适合长度较短的数据集
- TF-IDF: 适合主题较少的数据集
- 词向量: 如果词语本身也是某种向量的话,则可以将所有词语的词向量求和作为文档向量。适合处理 OOV 问题严重的数据集。
- 词频向量: 适合主题较多的数据集
3.3 k均值聚类
详细推导见《统计学习方法》;
3.4 层次聚类
《统计学习方法》中只介绍了自下而上的聚合层次聚类,没有介绍自上而下的分裂层次聚类,而本节实现中使用的是分裂层次聚类。
4 实现
4.1 聚类算法实现
该聚类模块可以接受任意文本作为文档,而不需要用特殊分隔符隔开单词。
# -*- coding:utf-8 -*-from pyhanlp import *ClusterAnalyzer = JClass('com.hankcs.hanlp.mining.cluster.ClusterAnalyzer')if __name__ == '__main__':analyzer = ClusterAnalyzer()analyzer.addDocument("赵一", "流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 流行, 蓝调, 蓝调, 蓝调, 蓝调, 蓝调, 蓝调, 摇滚, 摇滚, 摇滚, 摇滚")analyzer.addDocument("钱二", "爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲")analyzer.addDocument("张三", "古典, 古典, 古典, 古典, 民谣, 民谣, 民谣, 民谣")analyzer.addDocument("李四", "爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 爵士, 金属, 金属, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲, 舞曲")analyzer.addDocument("王五", "流行, 流行, 流行, 流行, 摇滚, 摇滚, 摇滚, 嘻哈, 嘻哈, 嘻哈")analyzer.addDocument("马六", "古典, 古典, 古典, 古典, 古典, 古典, 古典, 古典, 摇滚")print(analyzer.kmeans(3))print(analyzer.repeatedBisection(3))print(analyzer.repeatedBisection(1.0)) # 自动判断聚类数量k
运行结果:
[[王五, 赵一], [马六, 张三], [李四, 钱二]]
[[李四, 钱二], [王五, 赵一], [张三, 马六]]
[[李四, 钱二], [王五, 赵一], [张三, 马六]]
4.2 Evaluation
聚类任务常用的一种评测手段是沿用分类任务的F1F_1F1,将一些人工分类好的文档去掉标签交给聚类分析器,统计结果中有多少同类别的文档属于同一个簇。具体计算公式如下:
P(i,j)=nijnjR(i,j)=nijniF1(i,j)=2×P(i,j)×R(i,j)P(i,j)+R(i,j)\begin{aligned} & P(i, j) = \frac{n_{ij}}{n_j} \\ & R(i, j) = \frac{n_{ij}}{n_i} \\ & F_1(i, j) = \frac{2 \times P(i,j) \times R(i, j)}{P(i,j) + R(i,j)} \end{aligned} P(i,j)=njnijR(i,j)=ninijF1(i,j)=P(i,j)+R(i,j)2×P(i,j)×R(i,j)
nijn_{ij}nij:表示簇j中属于类别iii的文档;
njn_jnj:表示簇jjj中文档总数;
nin_ini:表示类别iii中文档总数;
对整个评测任务而言,它的综合F1F_1F1是所有类目上分值的加权平均,如下公式所述:
F1=∑ininmaxj(F1(i,j))F_1 = \sum\limits_{i}\frac{n_i}{n}\max\limits_j(F_1(i, j)) F1=i∑nnijmax(F1(i,j))
其中n=∑inin = \sum\limits_i n_in=i∑ni。
本次评测选择搜狗实验室提供的文本分类语料的一个子集,我称它为“搜狗文本分类语料库迷你版”。该迷你版语料库分为5个类目,每个类目下1000 篇文章,共计5000篇文章。运行代码如下:
# -*- coding:utf-8 -*-from pyhanlp import *
from tests.demos.demo_text_classification import sogou_corpus_pathClusterAnalyzer = JClass('com.hankcs.hanlp.mining.cluster.ClusterAnalyzer')if __name__ == '__main__':for algorithm in "kmeans", "repeated bisection":print("%s F1=%.2f\n" % (algorithm, ClusterAnalyzer.evaluate(sogou_corpus_path, algorithm) * 100))
运行结果:
...
加载中...
[教育]...100.00% 1000 篇文档
[汽车]...100.00% 1000 篇文档
[健康]...100.00% 1000 篇文档
[军事]...100.00% 1000 篇文档
[体育]...100.00% 1000 篇文档
耗时 13399 ms 加载了 5 个类目,共 5000 篇文档
kmeans聚类中...耗时 83538 ms 完毕。
kmeans F1=72.22...
加载中...
[教育]...100.00% 1000 篇文档
[汽车]...100.00% 1000 篇文档
[健康]...100.00% 1000 篇文档
[军事]...100.00% 1000 篇文档
[体育]...100.00% 1000 篇文档
耗时 9857 ms 加载了 5 个类目,共 5000 篇文档
repeated bisection聚类中...耗时 45148 ms 完毕。
repeated bisection F1=80.40
5 参考文献
- 何晗《自然语言处理入门》;
- 宗成庆《统计自然语言处理》;
- 李航《统计学习方法》;
6 需要解决的问题
- 理解k-meas聚类、层次聚类更多数学细节;