相似文本聚类与调参

📢作者: 小小明-代码实体
📢博客主页:https://blog.csdn.net/as604049322
📢欢迎点赞 👍 收藏 ⭐留言 📝 欢迎讨论!

相似文本聚类与调参

之前我在《批量模糊匹配的三种方法》一文中讲述了如何匹配最相似文本的方法,其中使用Gensim进行批量模糊匹配,是使用了稀疏的词向量计算相似度,速度相对前面的方法极快。

去年我有使用sklearn做过文本聚类,今天我就给大家演示一下如何在一大堆文本中自动寻找出相似的文本进行聚类,主要思路有:

  1. 将每个文本进行分词
  2. 根据词频或TF-IDF生成词向量
  3. 使用DBSCAN聚类算法对词向量矩阵计算余弦相似度并连接聚类

之前使用Gensim计算出的词频向量无法直接作为sklearn库的输入,需要进行如下转换为专门的稀疏矩阵对象:

from scipy import sparse

data, rows, cols = [], [], []
for i, row in enumerate(data_corpus):
    for e, c in row:
        rows.append(i)
        cols.append(e)
        data.append(c)
data = sc.csr_matrix((data, (rows, cols)))

为了方便,今天词向量和聚类算法都使用sklearn库,不再使用Gensim库。

首先我们读取测试数据:

import pandas as pd
import numpy as np
import jieba

df = pd.read_csv("所有客户.csv", encoding="gbk")

中文分词

分词的好坏会直接影响后续词向量的表现,我们可以根据数据集的情况自定义词汇,例如我根据前20条数据的情况增加相应的药店品牌:

words = ["元岗", "铭心堂", "金健民", "祺和", "钜富", "杏园春", "天平民康"]
for word in words:
    jieba.add_word(word)

如果我们需要分词的数据集存在停用词,可以使用如下代码将所有非中文字符(不含几个生僻字)删除:

df.user = df.user.str.replace("[^一-龟]+", "", regex=True)

然后执行分词并查看结果:

data_split_word = df.user.apply(jieba.lcut).apply(" ".join)
data_split_word.head(20)
0             [珠海, 广药, 康鸣, 医药, 有限公司]
1                   [深圳市, 宝安区, 中心医院]
2              [中山, 火炬, 开发区, 伴康, 药店]
3                [中山市, 同方, 医药, 有限公司]
4         [广州市, 天河区, 元岗, 金健民, 医药, 店]
5            [广州市, 天河区, 元岗, 居健堂, 药房]
6             [广州市, 天河区, 元岗, 润佰, 药店]
7             [广州市, 天河区, 元岗, 协心, 药房]
8             [广州市, 天河区, 元岗, 心怡, 药店]
9            [广州市, 天河区, 元岗, 永亨堂, 药店]
10        [广州市, 天河区, 员村, 德晖, 中, 西药店]
11    [广州市, 天河区, 员村, 东兴, 堂昌, 乐园, 药店]
12        [广州市, 天河区, 员村, 合家欢, 大, 药房]
13        [广州市, 天河区, 员村, 慷乐, 医药, 商店]
14        [广州市, 天河区, 员村, 为民, 永康, 药店]
15        [广州市, 天河区, 珠江, 新城, 钜富, 药店]
16        [广州市, 天河区, 珠江, 新城, 祺和, 药店]
17       [广州市, 天河区, 珠江, 新城, 杏园春, 药店]
18            [广州市, 天河, 沙河, 云芝, 中药店]
19               [广州市, 天河, 天平民康, 药店]
Name: user, dtype: object

当前前20条数据勉强算是分词达标。

当然明显药房和药店这些词的含义相同,如果我需要进一步减少影响,完全可以批量将其替换为一致的词汇。

创建词频向量

然后我们需要使用sklearn建立词频向量:

from sklearn.feature_extraction.text import CountVectorizer

count = CountVectorizer()
title_vec = count.fit_transform(data_split_word)

注意:如果我们需要使用TF-IDF模型当某个词在多个文本中出现时降低其权重,直接将 CountVectorizer修改为 TfidfVectorizer即可。

title_vec是个稀疏矩阵: <43487x24882 sparse matrix of type '<class 'numpy.int64'>' with 224221 stored elements in Compressed Sparse Row format><!--43487x24882-->

可以看看稀疏矩阵的结果:

print(title_vec[:2])
  (0, 17283)    1
  (0, 10626)    1
  (0, 11177)    1
  (0, 5944) 1
  (0, 13505)    1
  (1, 16416)    1
  (1, 9475) 1
  (1, 2833) 1

SciPy 稀疏矩阵:https://www.runoob.com/scipy/scipy-sparse-matrix.html

DBSCAN原理简介

相似文本聚类与调参

DBSCAN的核心参数是eps和min_samples,eps表示上图圆的半径,如果两个词向量的距离小于eps就会被连接在一起,而一个点能否成为中心点取决于圆内的点是否超过min_samples。

如何计算两个词向量之间的距离呢?sklearn内置了各种公式。

各类距离公式可查阅:https://xiao-xiaoming.github.io/DataMiningGuide/#/chapter-2

下面编写一个方法用于测试各种公式计算距离:

from sklearn.metrics import pairwise_distances

def calc_distance(i, j, metric="l1"):
    x, y = title_vec[i].toarray(), title_vec[j].toarray()
    print(i, j, data_split_word[i], "|", data_split_word[j],
          pairwise_distances(x, y, metric=metric)[0][0])

先测试曼哈顿距离:

for i, j in combinations(range(3, 11), 2):
    calc_distance(i, j)
3 4 &#x4E2D;&#x5C71;&#x5E02; &#x540C;&#x65B9; &#x533B;&#x836F; &#x6709;&#x9650;&#x516C;&#x53F8; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x91D1;&#x5065;&#x6C11; &#x533B;&#x836F; &#x5E97; 7.0
3 5 &#x4E2D;&#x5C71;&#x5E02; &#x540C;&#x65B9; &#x533B;&#x836F; &#x6709;&#x9650;&#x516C;&#x53F8; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5C45;&#x5065;&#x5802; &#x836F;&#x623F; 9.0
3 6 &#x4E2D;&#x5C71;&#x5E02; &#x540C;&#x65B9; &#x533B;&#x836F; &#x6709;&#x9650;&#x516C;&#x53F8; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6DA6;&#x4F70; &#x836F;&#x5E97; 9.0
3 7 &#x4E2D;&#x5C71;&#x5E02; &#x540C;&#x65B9; &#x533B;&#x836F; &#x6709;&#x9650;&#x516C;&#x53F8; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x534F;&#x5FC3; &#x836F;&#x623F; 9.0
3 8 &#x4E2D;&#x5C71;&#x5E02; &#x540C;&#x65B9; &#x533B;&#x836F; &#x6709;&#x9650;&#x516C;&#x53F8; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5FC3;&#x6021; &#x836F;&#x5E97; 9.0
3 9 &#x4E2D;&#x5C71;&#x5E02; &#x540C;&#x65B9; &#x533B;&#x836F; &#x6709;&#x9650;&#x516C;&#x53F8; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6C38;&#x4EA8;&#x5802; &#x836F;&#x5E97; 9.0
3 10 &#x4E2D;&#x5C71;&#x5E02; &#x540C;&#x65B9; &#x533B;&#x836F; &#x6709;&#x9650;&#x516C;&#x53F8; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5458;&#x6751; &#x5FB7;&#x6656; &#x4E2D; &#x897F;&#x836F;&#x5E97; 9.0
4 5 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x91D1;&#x5065;&#x6C11; &#x533B;&#x836F; &#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5C45;&#x5065;&#x5802; &#x836F;&#x623F; 4.0
4 6 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x91D1;&#x5065;&#x6C11; &#x533B;&#x836F; &#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6DA6;&#x4F70; &#x836F;&#x5E97; 4.0
4 7 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x91D1;&#x5065;&#x6C11; &#x533B;&#x836F; &#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x534F;&#x5FC3; &#x836F;&#x623F; 4.0
4 8 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x91D1;&#x5065;&#x6C11; &#x533B;&#x836F; &#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5FC3;&#x6021; &#x836F;&#x5E97; 4.0
4 9 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x91D1;&#x5065;&#x6C11; &#x533B;&#x836F; &#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6C38;&#x4EA8;&#x5802; &#x836F;&#x5E97; 4.0
4 10 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x91D1;&#x5065;&#x6C11; &#x533B;&#x836F; &#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5458;&#x6751; &#x5FB7;&#x6656; &#x4E2D; &#x897F;&#x836F;&#x5E97; 6.0
5 6 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5C45;&#x5065;&#x5802; &#x836F;&#x623F; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6DA6;&#x4F70; &#x836F;&#x5E97; 4.0
5 7 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5C45;&#x5065;&#x5802; &#x836F;&#x623F; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x534F;&#x5FC3; &#x836F;&#x623F; 2.0
5 8 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5C45;&#x5065;&#x5802; &#x836F;&#x623F; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5FC3;&#x6021; &#x836F;&#x5E97; 4.0
5 9 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5C45;&#x5065;&#x5802; &#x836F;&#x623F; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6C38;&#x4EA8;&#x5802; &#x836F;&#x5E97; 4.0
5 10 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5C45;&#x5065;&#x5802; &#x836F;&#x623F; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5458;&#x6751; &#x5FB7;&#x6656; &#x4E2D; &#x897F;&#x836F;&#x5E97; 6.0
6 7 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6DA6;&#x4F70; &#x836F;&#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x534F;&#x5FC3; &#x836F;&#x623F; 4.0
6 8 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6DA6;&#x4F70; &#x836F;&#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5FC3;&#x6021; &#x836F;&#x5E97; 2.0
6 9 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6DA6;&#x4F70; &#x836F;&#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6C38;&#x4EA8;&#x5802; &#x836F;&#x5E97; 2.0
6 10 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6DA6;&#x4F70; &#x836F;&#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5458;&#x6751; &#x5FB7;&#x6656; &#x4E2D; &#x897F;&#x836F;&#x5E97; 6.0
7 8 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x534F;&#x5FC3; &#x836F;&#x623F; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5FC3;&#x6021; &#x836F;&#x5E97; 4.0
7 9 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x534F;&#x5FC3; &#x836F;&#x623F; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6C38;&#x4EA8;&#x5802; &#x836F;&#x5E97; 4.0
7 10 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x534F;&#x5FC3; &#x836F;&#x623F; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5458;&#x6751; &#x5FB7;&#x6656; &#x4E2D; &#x897F;&#x836F;&#x5E97; 6.0
8 9 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5FC3;&#x6021; &#x836F;&#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6C38;&#x4EA8;&#x5802; &#x836F;&#x5E97; 2.0
8 10 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x5FC3;&#x6021; &#x836F;&#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5458;&#x6751; &#x5FB7;&#x6656; &#x4E2D; &#x897F;&#x836F;&#x5E97; 6.0
9 10 &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5143;&#x5C97; &#x6C38;&#x4EA8;&#x5802; &#x836F;&#x5E97; | &#x5E7F;&#x5DDE;&#x5E02; &#x5929;&#x6CB3;&#x533A; &#x5458;&#x6751; &#x5FB7;&#x6656; &#x4E2D; &#x897F;&#x836F;&#x5E97; 6.0

相似文本聚类与调参

从目前的数据来看,可以认为曼哈顿距离在4以内的认为相似。

for i, j in combinations(range(3, 11), 2):
    calc_distance(i, j, "l2")

而欧几里得距离下,可以认为距离在2以内的相似:

相似文本聚类与调参

而余弦相似度距离下或许应该设置距离0.4以内认为相似:

相似文本聚类与调参

DBSCAN聚类

今天我们就使用余弦相似度进行聚类:

from sklearn.cluster import DBSCAN

model = DBSCAN(eps=0.4, min_samples=5, metric="cosine")
model.fit(title_vec)
df['label'] = model.labels_

结果中标签 -1未找到相似文本的节点,标签0往往表示大量的数据都被聚类到了这里面:

print("未被聚类:", df.query("label==-1").shape[0])
print("都被聚在一起:", df.query("label==0").shape[0])
print("正常的聚类:", df.query("label not in (-1,0)").shape[0])
print("产生类别数:", df.label.nunique()-2)
&#x672A;&#x88AB;&#x805A;&#x7C7B;&#xFF1A; 5727
&#x90FD;&#x88AB;&#x805A;&#x5728;&#x4E00;&#x8D77;&#xFF1A; 35840
&#x6B63;&#x5E38;&#x7684;&#x805A;&#x7C7B;&#xFF1A; 1920
&#x4EA7;&#x751F;&#x7C7B;&#x522B;&#x6570;&#xFF1A; 203

我们可以不断调整eps和metric得到更准确的结果。下面我们预览一下成功聚类的部分的聚类结果:

for label, df_split in df.query("label not in (-1,0)").groupby("label"):
    print(label, df_split.user.to_list())
    if label > 50:
        break

相似文本聚类与调参

个人觉得结果还算达标~

当然,这种文本聚类问题不可能100%结果完全准确,这需要大家根据数据集的情况,细化到每个步骤去调整,最好能够自定义距离计算公式(metric参数支持传入自动化函数)~

相关参考:

使用sklearn处理经纬度的三种距离计算与地图可视化
https://xxmdmst.blog.csdn.net/article/details/117307759

Original: https://blog.csdn.net/as604049322/article/details/126150893
Author: 小小明-代码实体
Title: 相似文本聚类与调参

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/618020/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球