目录
[TOC]
1. 算法原理
1.1 常见的聚类算法
聚类算法属于常见的无监督分类算法,在很多场景下都有应用,如用户聚类,文本聚类等。常见的聚类算法可以分成两类:
- 以 k-means 为代表的基于分区的算法
- 以层次聚类为代表的基于层次划分的算法
对于第一类方法,有以下几个缺点:
1)需要事先确定聚类的个数,当数据集比较大时,很难事先给出一个合适的值;
2)只适用于具有凸形状的簇,不适用于具有任意形状的簇;
3)对内存的占用资源比较大,难以推广至大规模数据集;
对于第二类方法,有以下缺点:
1)需要确定停止分裂的条件
2)计算速度慢
1.2 DBSCAN聚类
DBSCAN是一类基于密度的算法,能有效解决上述两类算法的问题。
DBSCAN的基本假设是一个集群的密度要显著高于噪声点的密度。因此,其基本思想是对于集群中的每一个点,在给定的半径范围内,相邻点的数量必须超过预先设定的某一个阈值。
因此,DBSCAN算法中包含两个重要的参数:
eps:聚类类别中样本的相似度衡量,与类别内样本相似度成反比。可以理解为同一个类别当中,对两个样本之间距离的最大值限定。
min_samples:每个聚类类别中的最小样本数,会对未分类样本数量造成影响,与未分类样本数量成正比。当相似样本数量少于该参数时,不会聚到一起。
在实际应用过程中,根据样本的大小,以及样本的大致分布,了解聚类结果会随着这两个参数如何变化之后,可以根据自己的经验对两个参数进行调整。只有两个模型参数需要调整,因此调参过程也不会太麻烦。
2. 代码实现
2.1 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
2.2 载入数据
根据数据文件的不同存在不同的数据载入方法,我当时使用的是两种类型的数据,分别是直接包含目标短文本的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)
2.3 对文本进行分词,并记录词性
调用结巴词库对语料进行分词,并记录分词结果中每个词的词性。我的数据集在处理之后得到了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))}
2.4 文本向量化–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权重
2.5 基于词性的新权重
前面得到了分词的结果,并对词性进行了记录,接下来可以针对不同词汇的词性码,给与其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]
2.6 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_)
3. 聚类结果
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])
# 聚类结果展示(部分)
['社保卡', '社保卡', '社保卡。', '社保卡办理', '社保卡', '社保卡', '社保卡挂失', '社保卡。', '社保卡', '领取社保卡。']
['五险一金', '五险一金。', '五险一金。', '五险一金介绍', '看看二月份五险一金情况']
['打开汇钱。', '打开汇钱。', '我要汇钱', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。']
['车辆通行证。', '车辆通行证。', '我要办车辆通行证。', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证。', '车辆通行证', '车辆通行证。', '车辆通行证。', '车辆通行证']
['邮件附件权限', '等等邮件附件权限。', '邮件附件权限', '邮件附件权限', '邮件附件权限', '邮件附件权限', '您好,请问怎样申请图片查看权限和邮件附件查看权限?']
4 附件:完整代码
# === 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
# === 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'])
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)
# === 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))}
# === 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]
# === 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_)
# === 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])
# 聚类结果展示(部分)
['社保卡', '社保卡', '社保卡。', '社保卡办理', '社保卡', '社保卡', '社保卡挂失', '社保卡。', '社保卡', '领取社保卡。']
['五险一金', '五险一金。', '五险一金。', '五险一金介绍', '看看二月份五险一金情况']
['打开汇钱。', '打开汇钱。', '我要汇钱', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。']
['车辆通行证。', '车辆通行证。', '我要办车辆通行证。', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证。', '车辆通行证', '车辆通行证。', '车辆通行证。', '车辆通行证']
['邮件附件权限', '等等邮件附件权限。', '邮件附件权限', '邮件附件权限', '邮件附件权限', '邮件附件权限', '您好,请问怎样申请图片查看权限和邮件附件查看权限?']