笔记转载于GitHub项目:https://github.com/NLP-LOVE/Introduction-NLPpython
正所谓物以类聚,人以群分。人们在获取数据时须要整理,将类似的数据归档到一块儿,自动发现大量样本之间的类似性,这种根据类似性归档的任务称为聚类。git
聚类github
聚类(cluster analysis )指的是将给定对象的集合划分为不一样子集的过程,目标是使得每一个子集内部的元素尽可能类似,不一样子集间的元素尽可能不类似。这些子集又被称为簇(cluster),通常没有交集。算法
通常将聚类时簇的数量视做由使用者指定的超参数,虽然存在许多自动判断的算法,但它们每每须要人工指定其余超参数。数组
根据聚类结果的结构,聚类算法也能够分为划分式(partitional )和层次化(hierarchieal两种。划分聚类的结果是一系列不相交的子集,而层次聚类的结果是一棵树, 叶子节点是元素,父节点是簇。本章主要介绍划分聚类。ide
文本聚类函数
文本聚类指的是对文档进行聚类分析,被普遍用于文本挖掘和信息检索领域。学习
文本聚类的基本流程分为特征提取和向量聚类两步, 若是能将文档表示为向量,就能够对其应用聚类算法。这种表示过程称为特征提取,而一旦将文档表示为向量,剩下的算法就与文档无关了。这种抽象思惟不管是从软件工程的角度,仍是从数学应用的角度都十分简洁有效。测试
词袋模型优化
词袋(bag-of-words )是信息检索与天然语言处理中最经常使用的文档表示模型,它将文档想象为一个装有词语的袋子, 经过袋子中每种词语的计数等统计量将文档表示为向量。好比下面的例子:
人 吃 鱼。 美味 好 吃!
统计词频后以下:
人=1 吃=2 鱼=1 美味=1 好=1
文档通过该词袋模型获得的向量表示为[1,2,1,1,1],这 5 个维度分别表示这 5 种词语的词频。
通常选取训练集文档的全部词语构成一个词表,词表以外的词语称为 oov,不予考虑。一旦词表固定下来,假设大小为 N。则任何一个文档均可以经过这种方法转换为一个N维向量。词袋模型不考虑词序,也正由于这个缘由,词袋模型损失了词序中蕴含的语义,好比,对于词袋模型来说,“人吃鱼”和“鱼吃人”是同样的,这就不对了。
不过目前工业界已经发展出很好的词向量表示方法了: word2vec/bert 模型等。
词袋中的统计指标
词袋模型并不是只是选取词频做为统计指标,而是存在许多选项。常见的统计指标以下:
定义由 n 个文档组成的集合为 S,定义其中第 i 个文档 di 的特征向量为 di,其公式以下:
\[ d_{i}=\left(\operatorname{TF}\left(t_{1}, d_{i}\right), \operatorname{TF}\left(t_{2}, d_{i}\right), \cdots, \operatorname{TF}\left(t_{j}, d_{i}\right), \cdots, \operatorname{TF}\left(t_{m}, d_{i}\right)\right) \]
其中 tj表示词表中第 j 种单词,m 为词表大小, TF(tj, di) 表示单词 tj 在文档 di 中的出现次数。为了处理长度不一样的文档,一般将文档向量处理为单位向量,即缩放向量使得 ||d||=1。
一种简单实用的聚类算法是k均值算法(k-means),由Stuart Lloyd于1957年提出。该算法虽然没法保证必定可以获得最优聚类结果,但实践效果很是好。基于k均值算法衍生出许多改进算法,先介绍 k均值算法,而后推导它的一个变种。
基本原理
形式化啊定义 k均值算法所解决的问题,给定 n 个向量 d1 到 dn,以及一个整数 k,要求找出 k 个簇 S1 到 Sk 以及各自的质心 C1 到 Ck,使得下式最小:
\[ \text { minimize } \mathcal{I}_{\text {Euclidean }}=\sum_{r=1}^{k} \sum_{d_{i} \in S_{r}}\left\|\boldsymbol{d}_{i}-\boldsymbol{c}_{r}\right\|^{2} \]
其中 ||di - Cr|| 是向量与质心的欧拉距离,I(Euclidean) 称做聚类的准则函数。也就是说,k均值以最小化每一个向量到质心的欧拉距离的平方和为准则进行聚类,因此该准则函数有时也称做平方偏差和函数。而质心的计算就是簇内数据点的几何平均:
\[ \begin{array}{l}{s_{i}=\sum_{d_{j} \in S_{i}} d_{j}} \\ {c_{i}=\frac{s_{i}}{\left|S_{i}\right|}}\end{array} \]
其中,si 是簇 Si 内全部向量之和,称做合成向量。
生成 k 个簇的 k均值算法是一种迭代式算法,每次迭代都在上一步的基础上优化聚类结果,步骤以下:
k均值算法虽然没法保证收敛到全局最优,但可以有效地收敛到一个局部最优势。对于该算法,初级读者重点须要关注两个问题,即初始质心的选取和两点距离的度量。
初始质心的选取
因为 k均值不保证收敏到全局最优,因此初始质心的选取对k均值的运行结果影响很是大,若是选取不当,则可能收敛到一个较差的局部最优势。
一种更高效的方法是, 将质心的选取也视做准则函数进行迭代式优化的过程。其具体作法是,先随机选择第一个数据点做为质心,视做只有一个簇计算准则函数。同时维护每一个点到最近质心的距离的平方,做为一个映射数组 M。接着,随机取准则函数值的一部分记做。遍历剩下的全部数据点,若该点到最近质心的距离的平方小于0,则选取该点添加到质心列表,同时更新准则函数与 M。如此循环屡次,直至凑足 k 个初始质心。这种方法可行的原理在于,每新增一个质心,都保证了准则函数的值降低一个随机比率。 而朴素实现至关于每次新增的质心都是彻底随机的,准则函数的增减没法控制。孰优孰劣,一目了然。
考虑到 k均值是一种迭代式的算法, 须要反复计算质心与两点距离,这部分计算一般是效瓶颈。为了改进朴素 k均值算法的运行效率,HanLP利用种更快的准则函数实现了k均值的变种。
更快的准则函数
除了欧拉准则函数,还存在一种基于余弦距离的准则函数:
\[ \text { maximize } \mathcal{I}_{\mathrm{cos}}=\sum_{r=1}^{k} \sum_{d_{i} \in S_{r}} \cos \left(\boldsymbol{d}_{i}, \boldsymbol{c}_{r}\right) \]
该函数使用余弦函数衡量点与质心的类似度,目标是最大化同簇内点与质心的类似度。将向量夹角计算公式代人,该准则函数变换为:
\[ \mathcal{I}_{\mathrm{cos}}=\sum_{r=1}^{k} \sum_{d_{i} \in S_{r}} \frac{d_{i} \cdot c_{r}}{\left\|c_{r}\right\|} \]
代入后变换为:
\[ \mathcal{I}_{\cos }=\sum_{r=1}^{k} \frac{S_{r} \cdot c_{r}}{\left\|c_{r}\right\|}=\sum_{r=1}^{k} \frac{\left|S_{r}\right| c_{r} \cdot c_{r}}{\left\|c_{r}\right\|}=\sum_{r=1}^{k}\left|S_{r}\right|\left\|c_{r}\right\|=\sum_{r=1}^{k}\left\|s_{r}\right\| \]
也就是说,余弦准则函数等于 k 个簇各自合成向量的长度之和。比较以前的准则函数会发如今数据点从原簇移动到新簇时,I(Euclidean) 须要从新计算质心,以及两个簇内全部点到新质心的距离。而对于I(cos),因为发生改变的只有原簇和新簇两个合成向量,只需求二者的长度便可,计算量一会儿减少很多。
基于新准则函数 I(cos),k均值变种算法流程以下:
实现
在 HanLP 中,聚类算法实现为 ClusterAnalyzer,用户能够将其想象为一个文档 id 到文档向量的映射容器。
此处以某音乐网站中的用户聚类为案例讲解聚类模块的用法。假设该音乐网站将 6 位用户点播的歌曲的流派记录下来,而且分别拼接为 6 段文本。给定用户名称与这 6 段播放历史,要求将这 6 位用户划分为 3 个簇。实现代码以下:
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))
结果以下:
[[李四, 钱二], [王五, 赵一], [张三, 马六]]
经过 k均值聚类算法,咱们成功的将用户按兴趣分组,得到了“人以群分”的效果。
聚类结果中簇的顺序是随机的,每一个簇中的元素也是无序的,因为 k均值是个随机算法,有小几率获得不一样的结果。
该聚类模块能够接受任意文本做为文档,而不须要用特殊分隔符隔开单词。
基本原理
重复二分聚类(repeated bisection clustering) 是 k均值算法的效率增强版,其名称中的bisection是“二分”的意思,指的是反复对子集进行二分。该算法的步骤以下:
每次产生的簇由上到下造成了一颗二叉树结构。
正是因为这个性质,重复二分聚类算得上一种基于划分的层次聚类算法。若是咱们把算法运行的中间结果存储起来,就能输出一棵具备层级关系的树。树上每一个节点都是一个簇,父子节点对应的簇知足包含关系。虽然每次划分都基于 k均值,因为每次二分都仅仅在一个子集上进行,输人数据少,算法天然更快。
在步骤1中,HanLP采用二分后准则函数的增幅最大为策略,每产生一个新簇,都试着将其二分并计算准则函数的增幅。而后对增幅最大的簇执行二分,重复屡次直到知足算法中止条件。
自动判断聚类个数k
读者可能以为聚类个数 k 这个超参数很难准确估计。在重复二分聚类算法中,有一种变通的方法,那就是经过给准则函数的增幅设定阈值 β 来自动判断 k。此时算法的中止条件为,当一个簇的二分增幅小于 β 时再也不对该簇进行划分,即认为这个簇已经达到最终状态,不可再分。当全部簇都不可再分时,算法终止,最终产生的聚类数量就再也不须要人工指定了。
实现
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.repeatedBisection(3)) # 重复二分聚类 print(analyzer.repeatedBisection(1.0)) # 自动判断聚类数量k
运行结果以下:
[[李四, 钱二], [王五, 赵一], [张三, 马六]] [[李四, 钱二], [王五, 赵一], [张三, 马六]]
与上面音乐案例得出的结果一致,但运行速度要快很多。
本次评测选择搜狗实验室提供的文本分类语料的一个子集,我称它为“搜狗文本分类语料库迷你版”。该迷你版语料库分为5个类目,每一个类目下1000 篇文章,共计5000篇文章。运行代码以下:
from pyhanlp import * import zipfile import os from pyhanlp.static import download, remove_file, HANLP_DATA_PATH def test_data_path(): """ 获取测试数据路径,位于$root/data/test,根目录由配置文件指定。 :return: """ data_path = os.path.join(HANLP_DATA_PATH, 'test') if not os.path.isdir(data_path): os.mkdir(data_path) return data_path ## 验证是否存在 MSR语料库,若是没有自动下载 def ensure_data(data_name, data_url): root_path = test_data_path() dest_path = os.path.join(root_path, data_name) if os.path.exists(dest_path): return dest_path if data_url.endswith('.zip'): dest_path += '.zip' download(data_url, dest_path) if data_url.endswith('.zip'): with zipfile.ZipFile(dest_path, "r") as archive: archive.extractall(root_path) remove_file(dest_path) dest_path = dest_path[:-len('.zip')] return dest_path sogou_corpus_path = ensure_data('搜狗文本分类语料库迷你版', 'http://file.hankcs.com/corpus/sogou-text-classification-corpus-mini.zip') ## =============================================== ## 如下开始聚类 ClusterAnalyzer = 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))
运行结果以下:
kmeans F1=83.74 repeated bisection F1=85.58
评测结果以下表:
算法 | F1 | 耗时 |
---|---|---|
k均值 | 83.74 | 67秒 |
重复二分聚类 | 85.58 | 24秒 |
对比两种算法,重复二分聚类不只准确率比 k均值更高,并且速度是 k均值的 3 倍。然而重复二分聚类成绩波动较大,须要多运行几回才可能得出这样的结果。
无监督聚类算法没法学习人类的偏好对文档进行划分,也没法学习每一个簇在人类那里究竟叫什么。
HanLP何晗--《天然语言处理入门》笔记:
https://github.com/NLP-LOVE/Introduction-NLP
项目持续更新中......
目录
章节 |
---|
第 1 章:新手上路 |
第 2 章:词典分词 |
第 3 章:二元语法与中文分词 |
第 4 章:隐马尔可夫模型与序列标注 |
第 5 章:感知机分类与序列标注 |
第 6 章:条件随机场与序列标注 |
第 7 章:词性标注 |
第 8 章:命名实体识别 |
第 9 章:信息抽取 |
第 10 章:文本聚类 |
第 11 章:文本分类 |
第 12 章:依存句法分析 |
第 13 章:深度学习与天然语言处理 |