聚类算法属于常见的无监督分类算法,在很多场景下都有应用,如用户聚类,文本聚类等。常见的聚类算法可以分成两类:
对于第一类方法,有以下几个缺点:
对于第二类方法,有以下缺点:
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算法中包含两个重要的参数:
在实际应用过程中,根据样本的大小,以及样本的大致分布,了解聚类结果会随着这两个参数如何变化之后,可以根据自己的经验对两个参数进行调整。只有两个模型参数需要调整,因此调参过程也不会太麻烦。
# === 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权重。
# === 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]
得到了文本的向量化表示之后就可以将其投喂到模型当中了,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])
# 聚类结果展示(部分)
['社保卡', '社保卡', '社保卡。', '社保卡办理', '社保卡', '社保卡', '社保卡挂失', '社保卡。', '社保卡', '领取社保卡。']
['五险一金', '五险一金。', '五险一金。', '五险一金介绍', '看看二月份五险一金情况']
['打开汇钱。', '打开汇钱。', '我要汇钱', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。']
['车辆通行证。', '车辆通行证。', '我要办车辆通行证。', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证。', '车辆通行证', '车辆通行证。', '车辆通行证。', '车辆通行证']
['邮件附件权限', '等等邮件附件权限。', '邮件附件权限', '邮件附件权限', '邮件附件权限', '邮件附件权限', '您好,请问怎样申请图片查看权限和邮件附件查看权限?']