短文本聚类【DBSCAN】算法原理+Python代码实现+聚类结果展示

短文本聚类之DBSCAN

  • 算法原理
    • 常见的聚类算法
    • DBSCAN聚类
  • 代码实现
    • import需要的包
    • 载入数据
    • 对文本进行分词,并记录词性
    • 文本向量化--TF-IDF权重
    • 基于词性的新权重
    • DBSCAN
  • 聚类结果


算法原理

常见的聚类算法

聚类算法属于常见的无监督分类算法,在很多场景下都有应用,如用户聚类,文本聚类等。常见的聚类算法可以分成两类:

  • 以 k-means 为代表的基于分区的算法
  • 以层次聚类为代表的基于层次划分的算法

对于第一类方法,有以下几个缺点:

  1. 需要事先确定聚类的个数,当数据集比较大时,很难事先给出一个合适的值
  2. 只适用于具有凸形状的簇,不适用于具有任意形状的簇
  3. 对内存的占用资源比较大,难以推广至大规模数据集

对于第二类方法,有以下缺点:

  1. 需要确定停止分裂的条件
  2. 计算速度慢

DBSCAN聚类

A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise (Martin Ester, Hans-Peter Kriegel, Jörg Sander, Xiaowei Xu)
http://www.philippe-fournier-viger.com/spmf/DBScan.pdf

DBSCAN是一类基于密度的算法,能有效解决上述两类算法的问题。

DBSCAN的基本假设是一个集群的密度要显著高于噪声点的密度。因此,其基本思想是对于集群中的每一个点,在给定的半径范围内,相邻点的数量必须超过预先设定的某一个阈值。

因此,DBSCAN算法中包含两个重要的参数:

  • eps:聚类类别中样本的相似度衡量,与类别内样本相似度成反比。可以理解为同一个类别当中,对两个样本之间距离的最大值限定。
  • min_samples:每个聚类类别中的最小样本数,会对未分类样本数量造成影响,与未分类样本数量成正比。当相似样本数量少于该参数时,不会聚到一起。

在实际应用过程中,根据样本的大小,以及样本的大致分布,了解聚类结果会随着这两个参数如何变化之后,可以根据自己的经验对两个参数进行调整。只有两个模型参数需要调整,因此调参过程也不会太麻烦。


代码实现

import需要的包

# === import packages === #
import jieba.posseg as pseg
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
from sklearn.cluster import DBSCAN

载入数据

根据数据文件的不同存在不同的数据载入方法,我当时使用的是两种类型的数据,分别是直接包含目标短文本的txt,以json格式存储的txt。如果有用到这两种类型的文件可以参考这部分的数据载入代码,其他的请根据文件类型和数据样式自行载入。

首先是载入以json格式存储的txt文件,可以用正则表达式,也可以根据数据存储的方式提取出对应的字段。先展示一下数据的存储格式:

{"code":"200","data":{"result":[{"updateDate":1551923786433,"ensureIntentName":"新意图","corpus":"怎么查询之前的小微提醒","recommendResult":0,"remark":"","source":2,"result":2,"eventName":"","id":"b07328fc-8383-44b7-b466-15b063b8544a","state":0,"tag":"","isHandle":1,"createDate":1551669751334,"eventId":"","corpusTagId":"3335d2d8-a16e-46a2-9ed7-76739108d684","intentName":"","ensureIntent":"newIntent","recommendIntent":["setmsgnotifications"],"uploadTime":1551669751333,"w3account":"x00286769","createBy":"x00286769","intentCode":"","isBotSupport":0,"userRole":"0","welinkVersion":"3.9.13"}],"pagination":{"pageCount":1,"pageSizes":50,"pageNumber":1,"offset":0,"pageTotal":1,"pageNumbers":1,"pageSize":50}},"error":"","stack":"","message":"ok"}

我的目标是对上述数据当中,字典中key “data” 对应的字典中的 “result” 中每一个item 的 “corpus” 进行提取,于是就有了下列代码。

# === Data loading === #
data = []
corpus = []
for line in open("新意图语料.txt", 'r+', encoding='UTF-8'):
    data.append(eval(line))
for i in range(len(data)):
    tmp = data[i]['data']['result']
    for j in range(len(tmp)):
        corpus.append(tmp[j]['corpus'])

然后是载入包含目标短文本的txt,也就是说该txt直接存储了上面的 “corpus” 对应的内容,但是每一行的内容都加上了双引号和逗号,就通过strip把这些不需要的部分去掉了,最后得到所有 “corpus” 组成的list。

for line in open("未识别语料.txt", 'r+'):
    line = line.strip('\n')
    line = line.strip('\t')
    line = line.rstrip(',')
    line = line.lstrip('"')
    line = line.rstrip('"')
    corpus.append(line)

对文本进行分词,并记录词性

调用结巴词库对语料进行分词,并记录分词结果中每个词的词性。我的数据集在处理之后得到了5316条短文本,分词得到20640个不重复的词汇及其对应的词性,并建立了两者之间的字典联系。

# === Record the text cut and POS === #
part_of_speech = []
word_after_cut = []
cut_corpus_iter = corpus.copy()
cut_corpus = corpus.copy()
for i in range(len(corpus)):
    cut_corpus_iter[i] = pseg.cut(corpus[i])  # 5316
    cut_corpus[i] = ""
    for every in cut_corpus_iter[i]:
        cut_corpus[i] = (cut_corpus[i] + " " + str(every.word)).strip()
        part_of_speech.append(every.flag)  # 20640
        word_after_cut.append(every.word)  # 20640
word_pos_dict = {word_after_cut[i]: part_of_speech[i] for i in range(len(word_after_cut))} 

文本向量化–TF-IDF权重

使用TF-IDF对文本进行向量化,得到文本的TF-IDF权重。

# === Get the TF-IDF weights === #
Count_vectorizer = CountVectorizer()
transformer = TfidfTransformer()  # 用于统计每个词语的tf-idf权值
tf_idf = transformer.fit_transform(Count_vectorizer.fit_transform(cut_corpus))
# (5316,2039)第一个fit_transform是计算tf-idf 第二个fit_transform是将文本转为词频矩阵
word = Count_vectorizer.get_feature_names()  # 2039,获取词袋模型中的所有词语
weight = tf_idf.toarray()  # (5316,2039)将tf-idf矩阵抽取出来,元素w[i][j]表示j词在i类文本中的tf-idf权重

基于词性的新权重

前面得到了分词的结果,并对词性进行了记录,接下来可以针对不同词汇的词性吗,给与其TF-IDF权重以不同的乘数,这样可以突出某些类型的词汇的重要性,在一定程度上有助于聚类的效果。

具体的乘数构造规则可以根据需求自行调整。

# === Get new weight with POS considered === #
word_weight = [1 for i in range(len(word))]
for i in range(len(word)):
    if word[i] not in word_pos_dict.keys():
        continue
    if word_pos_dict[word[i]] == 'n':
        word_weight[i] = 1.2
    elif word_pos_dict[word[i]] == "vn":
        word_weight[i] = 1.1
    elif word_pos_dict[word[i]] == "m":
        word_weight[i] = 0
    else:  # 权重调整可以根据实际情况进行更改
        continue
word_weight = np.array(word_weight)
new_weight = weight.copy()
for i in range(len(weight)):
    for j in range(len(word)):
        new_weight[i][j] = weight[i][j] * word_weight[j]

DBSCAN

得到了文本的向量化表示之后就可以将其投喂到模型当中了,eps和min_samples都是可以调整的参数。

# === Fit the DBSCAN model and get the classify labels === #
DBS_clf = DBSCAN(eps=1, min_samples=4)
DBS_clf.fit(new_weight)
print(DBS_clf.labels_)

聚类结果

DBSCAN模型实现聚类之后,聚类的结果会存储在 labels_ 中,将 labels_ 与原来的文本一一对应,可以得到最终的聚类结果:

# === Define the function of classify the original corpus according to the labels === #
def labels_to_original(labels, original_corpus):
    assert len(labels) == len(original_corpus)
    max_label = max(labels)
    number_label = [i for i in range(0, max_label + 1, 1)]
    number_label.append(-1)
    result = [[] for i in range(len(number_label))]
    for i in range(len(labels)):
        index = number_label.index(labels[i])
        result[index].append(original_corpus[i])
    return result
labels_original = labels_to_original(DBS_clf.labels_, corpus)
for i in range(5):
	print(labels_original[i])
# 聚类结果展示(部分)	
['社保卡', '社保卡', '社保卡。', '社保卡办理', '社保卡', '社保卡', '社保卡挂失', '社保卡。', '社保卡', '领取社保卡。']
['五险一金', '五险一金。', '五险一金。', '五险一金介绍', '看看二月份五险一金情况']
['打开汇钱。', '打开汇钱。', '我要汇钱', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。']
['车辆通行证。', '车辆通行证。', '我要办车辆通行证。', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证。', '车辆通行证', '车辆通行证。', '车辆通行证。', '车辆通行证']
['邮件附件权限', '等等邮件附件权限。', '邮件附件权限', '邮件附件权限', '邮件附件权限', '邮件附件权限', '您好,请问怎样申请图片查看权限和邮件附件查看权限?']

你可能感兴趣的:(笔记,自然语言处理,coding)