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


目录

[TOC]

1. 算法原理

1.1 常见的聚类算法

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

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

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

1)需要事先确定聚类的个数,当数据集比较大时,很难事先给出一个合适的值;

2)只适用于具有凸形状的簇,不适用于具有任意形状的簇;

3)对内存的占用资源比较大,难以推广至大规模数据集;

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

1)需要确定停止分裂的条件

2)计算速度慢

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)

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]) 
# 聚类结果展示(部分)    
['社保卡', '社保卡', '社保卡。', '社保卡办理', '社保卡', '社保卡', '社保卡挂失', '社保卡。', '社保卡', '领取社保卡。']
['五险一金', '五险一金。', '五险一金。', '五险一金介绍', '看看二月份五险一金情况']
['打开汇钱。', '打开汇钱。', '我要汇钱', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。', '我要汇钱。']
['车辆通行证。', '车辆通行证。', '我要办车辆通行证。', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证', '车辆通行证。', '车辆通行证', '车辆通行证。', '车辆通行证。', '车辆通行证']
['邮件附件权限', '等等邮件附件权限。', '邮件附件权限', '邮件附件权限', '邮件附件权限', '邮件附件权限', '您好,请问怎样申请图片查看权限和邮件附件查看权限?']

文章作者: Leon
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Leon !
评论
 上一篇
DBSCAN算法python实现(附完整数据集和代码) DBSCAN算法python实现(附完整数据集和代码)
目录[TOC] 1. 算法思路DBSCAN算法的核心是“延伸”。先找到一个未访问的点p,若该点是核心点,则创建一个新的簇C,将其邻域中的点放入该簇,并遍历其邻域中的点,若其邻域中有点q为核心点,则将q的邻域内的点也划入簇C,直到C不再扩展。
下一篇 
机器学习系列之决策树算法(08):梯度提升树算法LightGBM 机器学习系列之决策树算法(08):梯度提升树算法LightGBM
1. LightGBM简介GBDT (Gradient Boosting Decision Tree) 是机器学习中一个长盛不衰的模型,其主要思想是利用弱分类器(决策树)迭代训练以得到最优模型,该模型具有训练效果好、不易过拟合等优点。GBD
  目录