基于词向量的主题聚类挖掘
数据准备
参考 《旅游民宿基本要求与评价》 标准中的评级指标辅助定义用户评价主题,本次实验将使用基于 Word2Vec 和 KMeans 主题词聚类的方式研究顾客评论中的主题分布情况。
使用 Pandas 加载在线数据表格,并查看数据维度和第一行数据。
import pandas as pd
data = pd.read_csv('https://labfile.oss.aliyuncs.com/courses/2628/1-1.csv')
print(data.shape)
data.head(1)
数据属性如下表所示
训练词向量
# 加载词性标注模块
import jieba.posseg
# 加载词向量训练模块
from gensim import models, similarities
# 添加 notebook 可视化模块
from tqdm.notebook import tqdm
jieba 词性标注预热,使用 jieba 中的词性标注模块对输入的句子进行处理,产生每个词和对应的词性。
list(jieba.posseg.cut('垃圾饭店'))
数据预处理
批量对用户评论分词和词性标注,对每一句用户评论进行词性标注并选取其中长度大于 1 的名词,此步骤比较耗时,可以通过进度条观察处理情况。
# 收集用户评论中的长度大于 1 的名词
n_word = list()
# 将用户评论转换为 list 的形式
sentence_list = data['content'].tolist()
# 实时显示处理状态
for sentence in tqdm(sentence_list):
tmp = list()
for word, flag in jieba.posseg.cut(sentence):
if 'n' in flag and len(word) > 1:
tmp.append(word)
n_word.append(tmp)
词向量训练
Word2Vec 是 Google 的一个开源工具,可以把对文本内容的处理简化为向量空间中的向量运算,通过计算出词与词之间的余弦值,计算出两两向量在空间上的相似度,并以此来表示文本语义上的相似度。我们利用 gensim 中的 Word2Vec 建立词向量模型,对上一步提取的词进行词向量训练,训练词语在空间上的表达。
# size 为词向量维度数,min_count 为统计词频出现的最小词频数,其余参数使用默认
%time model = models.word2vec.Word2Vec(n_word, size=300, min_count=1)
词向量使用
我们看出与“环境”相近的词语。词向量受到训练语料的限制,语料越大、模型效果越好,大家自行可以使用更大的语料进行训练和使用。
# 输入测试用例
keys = '环境'
topn = 5
# most_similar 就是通过找到词语向量并计算向量余弦相似度,找到最近的 topn 的词语
model.wv.most_similar(positive=[keys], topn=topn)
尝试使用字典中的一个名词来提取对应的词向量,并打印词向量维度信息。
words = '早餐'
model.wv[words].shape
批量进行提取词向量,直接传入字典的 list 即可,我们发现字典中总共有 3119 个词语。
# 提取全部的词
words_list = list(model.wv.vocab.keys())
word_vector = model.wv[words_list]
# 打印主题词典的个数
len(word_vector)
KMeans 主题聚类
聚类是一个将数据集中在某些方面相似的数据成员进行分类组织的过程,聚类就是一种发现这种内在结构的技术。本次实验我们使用 KMeans 算法对训练出来的词向量进行聚合,聚合出来的词语簇为一个主题,它是一种迭代求解的聚类分析算法,首先将数据分为 K 组,随机选取 K 个对象作为初始的聚类中心,然后计算每个对象与各个种子聚类中心之间的距离,把每个对象分配给距离它最近的聚类中心,聚类中心以及分配给它们的对象就代表一个聚类。
聚类个数选取
KMeans 算法需要一个初始的聚类个数作为启动聚类的参数,本实验将研究聚类点个数对内部聚类结构的影响规律来寻找最佳聚类个数。
# 加载 KMeans 聚类算法模块
from sklearn.cluster import KMeans
# 加载画图模块
import matplotlib.pyplot as plt
%matplotlib inline
判断合适的 KMeans 聚类点时,最常用的是手肘法。随着聚类个数 k 的增大会大幅增加每个簇的聚合程度,故 SSE(误差平方和) 的下降幅度会很大,而当 k 到达真实聚类数时,再增加 k 所得到的聚合程度回报会迅速变小,所以 SSE 开始的下降幅度会很大,然后随着 k 值的继续增大而趋于平缓,变化关系是类似手肘的形状,而这个肘部对应的 k 值就是数据的真实聚类数。我们通过设置最大的聚类点 cluster_max,利用模型迭代产生的误差平方和,来判断合适的聚类点。通过控制变量的方式选取最佳聚类中心,通过结果趋势图看出,最佳的聚类数在 4 或者 5 附近,后续的主题个数选取可以选择其中的数值进行试验即可。需要耗费一些时间,请耐心等待。
# SSE 是所有样本的聚类误差,代表了聚类效果的好坏,sse_error_list 存放每次结果的误差平方和
sse_error_list = list()
# 设置最大的聚类中心点个数
cluster_max = 20
# 实时显示聚类训练的情况
for clusters in tqdm(range(1, cluster_max + 1)):
# 构造聚类器,只改变聚类个数,其余均使用默认配置
clf = KMeans(n_clusters=clusters)
# 开始训练模型
clf.fit(word_vector)
sse_error_list.append(clf.inertia_)
# 横坐标表示聚类个数
plt.xlabel('topic_cluster')
# 纵坐标表示误差
plt.ylabel('error')
# 对聚类点进行显示设置
plt.plot(sse_error_list, '*-')
# 对聚类结果进行可视化
plt.show()
自动化聚类个数选取
通过肘部图观察最佳的聚类个数需要比较专业的经验和人工参与,不适合自动化的聚类。我们简化此自动化选择聚类个数的过程,首先求得 SSE 的均值,利用小于均值的策略也可以选择聚类个数。
sse_error_mean = sum(sse_error_list) / len(sse_error_list)
sse_error_mean
我们可以通过使用简单的聚类个数判断策略来找到最佳聚类点。比如我们使用判断当某个聚类个数下的 SSE 的数值小于均值,即选择此聚类个数的下一个聚类个数作为最佳聚类点。
# 定义处理函数
def get_best_clusters(sse_error_list):
# 通过均值来判断肘部的点,index 从 0 开始
for index, value in enumerate(sse_error_list):
if sse_error_mean > value:
# 返回不超过均值点的下一个聚类索引即可
return index + 2
求出最佳的聚类个数。
best_topic_cluster = get_best_clusters(sse_error_list)
best_topic_cluster
训练主题聚类模型
我们使用上述自动化方式求得的聚类个数作为初始化的参数,开始对词向量进行主题聚类。
# 使用最佳的聚类个数初始化模型,其余使用默认设置
clf = KMeans(n_clusters=best_topic_cluster)
开始进行训练,并产生对每个词的主题预测的聚类类别。
topic_cluster_labels = clf.fit_predict(word_vector)
输出聚类中心的坐标,因为有 5 个聚类中心,所以显示 5 个聚类中心的向量。
cluster_centers_list = clf.cluster_centers_
len(cluster_centers_list)
通过查询每个词的向量和聚类类别,通过与聚类中心进行欧式距离计算,批量计算每个词向量到最近聚类中心点的欧式距离。
import numpy as np
# 存储计算的欧式距离
cal_EuclideanDistance = list()
for index, words in tqdm(list(enumerate(words_list))):
# 取出词的词向量
words_vec = model.wv[words]
# 取出词的聚类中心点的向量
center_vec = cluster_centers_list[topic_cluster_labels[index]]
# 计算词向量到聚类中心点的欧式距离
dist = np.sqrt(np.sum(np.square(words_vec - center_vec)))
cal_EuclideanDistance.append(dist)
将上述计算的主题词、聚类标签、与中心词的欧式距离设置好列名写入 DataFrame,并打印前 5 行。
# 设置 DataFrame 的列名
topic_cluster_data = pd.DataFrame({
'words': words_list,
'topic_cluster': topic_cluster_labels,
'euclidean_distance': cal_EuclideanDistance
})
# 显示前几行数据
topic_cluster_data.head()
主题聚合
主题的具体意义可以按照中心词进行抽象,我们下面打印聚类主题的抽象标签。
topic_cluster_data['topic_cluster'].unique()
对不同编号下的主题进行聚合,我们可以发现不同主题下的评价数量的差异,以此挖掘用户比较关注的主题。
topic_cluster_data['topic_cluster'].groupby(
topic_cluster_data['topic_cluster']).count()
分别统计不同 topic lcluster 下的前 topn 个聚类中心点最近的词语。
# 按照词与中心点的欧式距离进行降序排列
def get_top_cluster_words(topic_cluster_labels, topn=20):
# 定义主题和主题词的存储结构
cluster_result = dict()
# 分别处理每个主题下的主题词并按照主题词与主题中心的欧式距离尽心倒序处理
for topic_cluster_label in topic_cluster_labels:
topic_select = topic_cluster_data[topic_cluster_data['topic_cluster'] ==
topic_cluster_label].sort_values(
by=["euclidean_distance"],
ascending=False)
# 输出每个主题下的 topn 的主题词
cluster_result[topic_cluster_label] = topic_select['words'].tolist()[
:topn]
return cluster_result
产生主题序号,抽象表示用户评价主题。
topic_cluster_labels_list = list(range(best_topic_cluster))
topic_cluster_labels_list
计算在每一个聚类中心附近的聚类主题词,并按照欧式距离进行倒排,中心点既是排在第一个词语,通过同一主题下的主题词来总结此主题的具体意思。但是由于本实验使用的语料过小,此效果随着词向量的规模而变化,训练语料越大,主题聚类效果越好,后续还可以直接使用第三方训练好的词向量模型进行中心词向量化。
# 设定每一个主题显示 topn 的词语
topn = 30
topic_result = get_top_cluster_words(topic_cluster_labels_list, topn=topn)
# 打印每个主题下的词语
for topic_num, topic_words in topic_result.items():
print('主题 # {}- 主题词: {}'.format(topic_num, topic_words))
我们得出主题聚类的目就是扩充主题词典,并以此挖掘用户兴趣。扩充后的主题词典如下所示:
topic_words_list = {
'环境': [
'环境', '周边', '风景', '空气', '江景', '小区', '景点', '夜景', '街', '周围', '景区', '声音',
'景色'
],
'价格': ['价格', '房价', '性价比', '价位', '单价', '价钱'],
'特色': ['特色', '装潢', '布置', '建筑', '结构', '格调', '装修', '设计', '风格', '隔音'],
'设施': [
'设施', '设备', '条件', '硬件', '房间', '热水', 'WiFi', 'wife', '电梯', '阳台', '卫生间',
'洗手间', '空调', '垃圾桶', '通风'
],
'餐饮': ['餐饮', '早餐', '咖啡', '味道', '饭', '菜', '水果'],
'交通': ['交通', '车程', '地段', '路程', '停车', '客运站', '马路'],
'服务': ['服务', '态度', '前台', '服务员', '老板', '掌柜', '店家', '工作人员'],
'体验': ['体验', '整体', '感觉'],
}