![](https://i-blog.csdnimg.cn/blog_migrate/81f3b7338daf0349dfb0a2ee243f29e1.jpeg)
在第一篇内容中,我们介绍了 LDA 主题模型,这一篇,我们将介绍经典的 KMeans 聚类算法在文本上的表现。为了方便和前面 LDA 主题模型对比,我们依然使用同一份数据集,对数据的前期处理保持一致。
二、KMeans 聚类
2.1 加载数据集
df = pd.read_csv('/content/drive/My Drive/cnews.train.txt', delimiter="\t", header=None, names=["label", "data"])
print(df.label.unique())
['体育' '娱乐' '家居' '房产' '教育' '时尚' '时政' '游戏' '科技' '财经']
2.2 数据清洗
中间几个步骤略,与上一篇处理方式完全一致。
data_words_bigrams = make_bigrams(train_st_text)
为了方便后续的处理,我们将分词后的数组拼接起来
corpus = [' '.join(line) for line in data_words_bigrams]
['马晓旭 意外 受伤 国奥 警惕 无奈 大雨 格外 青睐 殷家 傅亚雨 沈阳 报道 来到 沈阳 国奥队 依然 摆脱 雨水 困扰 下午 国奥队 日常 训练 再度 大雨 干扰 无奈_之下 队员 慢跑 分钟 草草收场 上午 国奥队 奥体中心 外场 训练 阴沉沉 气象预报 显示 当天 下午 沈阳 大雨 幸好 队伍 上午 训练 干扰 下午 点当 球队 抵达 训练场 大雨 几个 小时 丝毫 停下来 试一试 态度 球队 当天 下午 例行_训练 分钟 天气 转好 迹象 保护 球员 国奥队 中止 当天 训练 全队 返回 酒店 训练 足球队 来说 稀罕 奥运会 即将 全队 变得 娇贵 沈阳 一周 训练 国奥队 保证 现有 球员 不再 出现意外 伤病 情况 影响 正式 比赛 这一 阶段 控制 训练 受伤 控制 感冒 疾病 队伍 放在 位置 抵达 沈阳 后卫 冯萧霆 训练 冯萧霆 长春 患上_感冒 参加 塞尔维亚 热身赛 队伍 介绍 冯萧霆 发烧_症状 两天 静养 休息 感冒 恢复 训练 冯萧霆 例子 国奥队 对雨中 训练 显得 特别 谨慎 担心 球员 受凉 引发 感冒 非战斗 减员 女足 队员 马晓旭 热身赛 受伤 导致 无缘 奥运 前科 沈阳 国奥队 格外 警惕 训练 嘱咐 队员 动作 再出 事情 工作人员 长春_沈阳 雨水 一路 伴随 国奥队 长春 几次 训练 大雨 搅和 没想到 沈阳 碰到 事情 国奥 球员 雨水 青睐 不解']
2.3 文本向量化
文本的向量化表示采用三种方式:使用 IDF 权重的哈希向量化表示、不使用 IDF 权重的哈希向量化表示以及 TFIDF 向量化表示,由于文本词量较大,因此在做 hash 处理的时候,我们把特征数设定为 25 万,TFIDF 中我们没设定,使用全部词量。
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfTransformer
# Perform an IDF normalization on the output of HashingVectorizer
hasher = HashingVectorizer(n_features=250000, alternate_sign=False, norm=None)
vectorizer_hash_idf = make_pipeline(hasher, TfidfTransformer())
vectorizer_hash = HashingVectorizer(n_features=250000, alternate_sign=False, norm='l2')
vectorizer_tfidf = TfidfVectorizer(max_df=0.5, min_df=2, use_idf=True)
X_hash_idf = vectorizer_hash_idf.fit_transform(corpus)
X_hash = vectorizer_hash.fit_transform(corpus)
X_tfidf = vectorizer_tfidf.fit_transform(corpus)
print(X_hash_idf.shape)
print(X_hash.shape)
print(X_tfidf.shape)
(50000, 250000)
(50000, 250000)
(50000, 207705)
对于数据维度过高,我们可以使用一些维度压缩技术进行处理,如 PCA、SVD 等,此处我们使用的是 SVD 及正则化处理,此处的输出维度我们限定到 100,如果压缩的目的是为了可视化,那么最常见的是压缩到 2 维。
from sklearn.decomposition import TruncatedSVD
from sklearn.preprocessing import Normalizer
print("Performing dimensionality reduction using LSA")
# Vectorizer results are normalized, which makes KMeans behave as
# spherical k-means for better results. Since LSA/SVD results are
# not normalized, we have to redo the normalization.
svd = TruncatedSVD(100)
normalizer = Normalizer(copy=False)
lsa = make_pipeline(svd, normalizer)
X_hash_idf_lsa = lsa.fit_transform(X_hash_idf)
X_hash_lsa = lsa.fit_transform(X_hash)
X_tfidf_lsa = lsa.fit_transform(X_tfidf)
explained_variance = svd.explained_variance_ratio_.sum()
print("Explained variance of the SVD step: {}%".format(
int(explained_variance * 100)))
print()
Performing dimensionality reduction using LSA
Explained variance of the SVD step: 11%
print(X_hash_idf_lsa.shape)
print(X_hash_lsa.shape)
print(X_tfidf_lsa.shape)
(50000, 100)
(50000, 100)
(50000, 100)
2.4 文本聚类
此处使用两种算法,普通的 KMeans 算法以及同类的更具扩展性的 MiniBatchKMeans 算法,我们将两种算法分别应用于上面 6 种不同的数据处理结果上,共 12 组测试:
minikm_X_hash_idf_lsa = MiniBatchKMeans(n_clusters=10, init='k-means++', n_init=1, init_size=1000, batch_size=1000, verbose=False)
minikm_X_hash_idf_lsa.fit(X_hash_idf_lsa)
minikm_X_hash_lsa = MiniBatchKMeans(n_clusters=10, init='k-means++', n_init=1, init_size=1000, batch_size=1000, verbose=False)
minikm_X_hash_lsa.fit(X_hash_lsa)
minikm_X_tfidf_lsa = MiniBatchKMeans(n_clusters=10, init='k-means++', n_init=1, init_size=1000, batch_size=1000, verbose=False)
minikm_X_tfidf_lsa.fit(X_tfidf_lsa)
minikm_X_hash_idf = MiniBatchKMeans(n_clusters=10, init='k-means++', n_init=1, init_size=1000, batch_size=1000, verbose=False)
minikm_X_hash_idf.fit(X_hash_idf)
minikm_X_hash = MiniBatchKMeans(n_clusters=10, init='k-means++', n_init=1, init_size=1000, batch_size=1000, verbose=False)
minikm_X_hash.fit(X_hash)
minikm_X_tfidf = MiniBatchKMeans(n_clusters=10, init='k-means++', n_init=1, init_size=1000, batch_size=1000, verbose=False)
minikm_X_tfidf.fit(X_tfidf)
km = KMeans(n_clusters=10, init='k-means++', max_iter=100, n_init=1, verbose=False)
km_X_hash_idf_lsa = km.fit(X_hash_idf_lsa)
km = KMeans(n_clusters=10, init='k-means++', max_iter=100, n_init=1, verbose=False)
km_X_hash_lsa = km.fit(X_hash_lsa)
km = KMeans(n_clusters=10, init='k-means++', max_iter=100, n_init=1, verbose=False)
km_X_tfidf_lsa = km.fit(X_tfidf_lsa)
km = KMeans(n_clusters=10, init='k-means++', max_iter=100, n_init=1, verbose=False)
km_X_hash_idf = km.fit(X_hash_idf)
km = KMeans(n_clusters=10, init='k-means++', max_iter=100, n_init=1, verbose=False)
km_X_hash = km.fit(X_hash)
km = KMeans(n_clusters=10, init='k-means++', max_iter=100, n_init=1, verbose=False)
km_X_tfidf = km.fit(X_tfidf)
下面我们结合数据集的真实标签及聚类结果标签,给出一些聚类算法中比较常见的评估指标,每种指标的含义及具体计算方法可以参考 scikit-learn 的官方用户手册。
from sklearn import metrics
labels = df.label
Xs = [X_hash_idf_lsa, X_hash_lsa, X_tfidf_lsa,
X_hash_idf, X_hash, X_tfidf,
X_hash_idf_lsa, X_hash_lsa, X_tfidf_lsa,
X_hash_idf, X_hash, X_tfidf]
for index, method in enumerate([minikm_X_hash_idf_lsa, minikm_X_hash_lsa, minikm_X_tfidf_lsa,
minikm_X_hash_idf, minikm_X_hash, minikm_X_tfidf,
km_X_hash_idf_lsa, km_X_hash_lsa, km_X_tfidf_lsa,
km_X_hash_idf, km_X_hash, km_X_tfidf]):
X = Xs[index]
print("Homogeneity: %0.3f" % metrics.homogeneity_score(labels, method.labels_))
print("Completeness: %0.3f" % metrics.completeness_score(labels, method.labels_))
print("V-measure: %0.3f" % metrics.v_measure_score(labels, method.labels_))
print("Adjusted Rand-Index: %.3f"
% metrics.adjusted_rand_score(labels, method.labels_))
print("Silhouette Coefficient: %0.3f"
% metrics.silhouette_score(X, method.labels_, metric='euclidean'))
# print("Calinski-Harabaz Index: %0.3f"
# % metrics.calinski_harabasz_score(X.toarray(), km.labels_)) # toarray 内存直接搞死
print("----------------------------------------------------------------")
Homogeneity: 0.649
Completeness: 0.704
V-measure: 0.675
Adjusted Rand-Index: 0.499
Silhouette Coefficient: 0.130
----------------------------------------------------------------
Homogeneity: 0.600
Completeness: 0.632
V-measure: 0.616
Adjusted Rand-Index: 0.508
Silhouette Coefficient: 0.115
----------------------------------------------------------------
Homogeneity: 0.629
Completeness: 0.660
V-measure: 0.644
Adjusted Rand-Index: 0.511
Silhouette Coefficient: 0.121
----------------------------------------------------------------
Homogeneity: 0.510
Completeness: 0.615
V-measure: 0.557
Adjusted Rand-Index: 0.259
Silhouette Coefficient: 0.012
----------------------------------------------------------------
Homogeneity: 0.470
Completeness: 0.557
V-measure: 0.510
Adjusted Rand-Index: 0.243
Silhouette Coefficient: 0.024
----------------------------------------------------------------
Homogeneity: 0.467
Completeness: 0.642
V-measure: 0.541
Adjusted Rand-Index: 0.223
Silhouette Coefficient: 0.009
----------------------------------------------------------------
Homogeneity: 0.689
Completeness: 0.738
V-measure: 0.713
Adjusted Rand-Index: 0.561
Silhouette Coefficient: 0.146
----------------------------------------------------------------
Homogeneity: 0.609
Completeness: 0.634
V-measure: 0.621
Adjusted Rand-Index: 0.496
Silhouette Coefficient: 0.133
----------------------------------------------------------------
Homogeneity: 0.693
Completeness: 0.729
V-measure: 0.711
Adjusted Rand-Index: 0.589
Silhouette Coefficient: 0.124
----------------------------------------------------------------
Homogeneity: 0.507
Completeness: 0.616
V-measure: 0.556
Adjusted Rand-Index: 0.248
Silhouette Coefficient: 0.013
----------------------------------------------------------------
Homogeneity: 0.497
Completeness: 0.601
V-measure: 0.544
Adjusted Rand-Index: 0.257
Silhouette Coefficient: 0.026
----------------------------------------------------------------
Homogeneity: 0.536
Completeness: 0.623
V-measure: 0.577
Adjusted Rand-Index: 0.279
Silhouette Coefficient: 0.013
----------------------------------------------------------------
我们简单分析一下上面的实验结果,一共 12 组实验,前 6 组是使用的是 MiniBatchKmeans 算法,后 6 组使用的是常规的 KMeans 算法,两个大组下前三组均为经过 lsa 降维处理的数据,后三组保持高维数据:
- 轮廓系数不太适用于高维数据,具有维度灾难现象,详见上面的 4、5、6 及 10、11、12 组实验
- V-measure 和 Adjusted Rand-Index 是基于信息论的评估分数,官方文档中提到二者不受维度影响,但实测结果也有较大差距,其中 Adjusted Rand-Index 差不多小一半
- 同等数据情况下,KMeans 结果比 MiniBatchKMeans 结果要略好一丢丢,但是计算量较高,因此大数据量下推荐使用后者。
- 除轮廓系数外,其他几个指标均需要数据的真实标签,然而这种情况实际中几乎不可能(否则做监督学习它不香么?)
更多评估指标及各个指标的优缺点可以参考 scikit-learn 的用户手册。
2.5 关键词展示
对于使用 TFIDF 向量化的文本,我们在聚类后可以展示每个聚类结果中的一些高频词汇
# minikm-tfidf-lsa
print("Top terms per cluster:")
original_space_centroids = svd.inverse_transform(minikm_X_tfidf_lsa.cluster_centers_)
order_centroids = original_space_centroids.argsort()[:, ::-1]
terms = vectorizer_tfidf.get_feature_names()
for i in range(10):
print("Cluster %d:" % i, end='')
for ind in order_centroids[i, :10]:
print(' %s' % terms[ind], end='')
print()
Top terms per cluster:
Cluster 0: 银行 陈水扁 贷款 信用卡 客户 吴淑珍 理财产品 业务 检方 台湾
Cluster 1: 游戏 玩家 中国 学生 留学 大学 美国 活动 工作 发展
Cluster 2: 电影 导演 影片 拍摄 观众 演员 角色 新浪_娱乐 上映 香港
Cluster 3: 生活 美国 中国 英国 孩子 活动 工作 设计 时间 喜欢
Cluster 4: 比赛 球队 球员 火箭 热火 篮板 奇才 防守 时间 赛季
Cluster 5: 市场 房地产 项目 企业 价格 公司 中国 亿元 投资 开发商
Cluster 6: 基金 投资 公司 市场 债券 投资者 股票 收益 经理 下跌
Cluster 7: 搭配 时尚 性感 组图 黑色 装扮 外套 设计 造型 可爱
Cluster 8: 科学家 发现 研究 地球 美国 人类 时间 英国 动物 研究_人员
Cluster 9: 空间 色彩 设计 客厅 风格 装饰 卧室 家居 家具 白色
# minikm-tfidf
print("Top terms per cluster:")
order_centroids = minikm_X_tfidf.cluster_centers_.argsort()[:, ::-1]
terms = vectorizer_tfidf.get_feature_names()
for i in range(10):
print("Cluster %d:" % i, end='')
for ind in order_centroids[i, :10]:
print(' %s' % terms[ind], end='')
print()
Top terms per cluster:
Cluster 0: 移民 投资 加拿大 申请 申请人 签证 美国 澳洲 技术移民 澳大利亚
Cluster 1: 基金 投资 公司 债券 市场 投资者 收益 股票 经理 发行
Cluster 2: 奇才 阿联 易建联 沃尔 比赛 球队 刘易斯 阿里_纳斯 麦基 莱切
Cluster 3: 市场 房地产 银行 项目 投资 房价 企业 开发商 中国 亿元
Cluster 4: 学生 大学 学校 申请 美国 专业 课程 留学 学院 学习
Cluster 5: 留学 学生 留学生 中国 学校 教育 签证 申请 美国 大学
Cluster 6: 游戏 玩家 中国 美国 活动 发现 时间 陈水扁 工作 世界
Cluster 7: 电影 导演 影片 票房 观众 拍摄 演员 上映 新浪_娱乐 角色
Cluster 8: 比赛 火箭 热火 球队 球员 篮板 科比 防守 湖人 詹姆斯
Cluster 9: 搭配 时尚 设计 风格 色彩 组图 黑色 性感 白色 装扮
# km-tfidf-lsa
print("Top terms per cluster:")
original_space_centroids = svd.inverse_transform(minikm_X_tfidf_lsa.cluster_centers_)
order_centroids = original_space_centroids.argsort()[:, ::-1]
terms = vectorizer_tfidf.get_feature_names()
for i in range(10):
print("Cluster %d:" % i, end='')
for ind in order_centroids[i, :10]:
print(' %s' % terms[ind], end='')
print()
Top terms per cluster:
Cluster 0: 银行 陈水扁 贷款 信用卡 客户 吴淑珍 理财产品 业务 检方 台湾
Cluster 1: 游戏 玩家 中国 学生 留学 大学 美国 活动 工作 发展
Cluster 2: 电影 导演 影片 拍摄 观众 演员 角色 新浪_娱乐 上映 香港
Cluster 3: 生活 美国 中国 英国 孩子 活动 工作 设计 时间 喜欢
Cluster 4: 比赛 球队 球员 火箭 热火 篮板 奇才 防守 时间 赛季
Cluster 5: 市场 房地产 项目 企业 价格 公司 中国 亿元 投资 开发商
Cluster 6: 基金 投资 公司 市场 债券 投资者 股票 收益 经理 下跌
Cluster 7: 搭配 时尚 性感 组图 黑色 装扮 外套 设计 造型 可爱
Cluster 8: 科学家 发现 研究 地球 美国 人类 时间 英国 动物 研究_人员
Cluster 9: 空间 色彩 设计 客厅 风格 装饰 卧室 家居 家具 白色
# km-tfidf
print("Top terms per cluster:")
order_centroids = km_X_tfidf.cluster_centers_.argsort()[:, ::-1]
terms = vectorizer_tfidf.get_feature_names()
for i in range(10):
print("Cluster %d:" % i, end='')
for ind in order_centroids[i, :10]:
print(' %s' % terms[ind], end='')
print()
Top terms per cluster:
Cluster 0: 中国 发展 移民 经济 企业 人民币 投资 美国 合作 国家
Cluster 1: 基金 投资 公司 债券 市场 投资者 股票 经理 收益 发行
Cluster 2: 比赛 火箭 球队 热火 球员 奇才 篮板 防守 湖人 新浪_体育讯
Cluster 3: 学生 留学 大学 学校 申请 留学生 美国 中国 专业 教育
Cluster 4: 空间 设计 色彩 家具 风格 家居 装饰 装修 客厅 卧室
Cluster 5: 房地产 市场 银行 房价 项目 开发商 楼市 土地 亿元 投资
Cluster 6: 游戏 玩家 开发 系统 活动 pc 系列 模式 体验 公布
Cluster 7: 电影 导演 影片 票房 观众 演员 拍摄 上映 新浪_娱乐 角色
Cluster 8: 美国 发现 陈水扁 时间 活动 科学家 北京 工作 公司 中国
Cluster 9: 搭配 时尚 黑色 性感 组图 装扮 外套 设计 点评 款式
最后我们计算一下每个簇的文档数,真实数据中每个类别均为 5000。可以看到下面结果中索引为 6 的簇中文档最多,而根据上面的关键词 ”Cluster 6: 游戏 玩家 中国 美国 活动 发现 时间 陈水扁 工作 世界“,我们发现这个类下至少包含”游戏“和”时政“两个类别下的高频词,聚类结果不够理想。
result = list(minikm_X_tfidf.predict(X_tfidf))
print ('Cluster distribution:')
print (dict([(i, result.count(i)) for i in result]))
print(-minikm_X_tfidf.score(X_tfidf))
Cluster distribution:
{6: 24837, 8: 3624, 2: 748, 7: 4377, 4: 1245, 3: 5499, 9: 5616, 1: 2259, 5: 1382, 0: 413}
48097.83211562563
2.6 判定最佳聚类数
一种简单的方法是绘制一系列聚类结果的 SSE。我们找图中的拐点。如下如所示,拐点因该在 11 附近。
tfidf = TfidfVectorizer(min_df=5, max_df=0.95)
tfidf.fit(corpus)
text = tfidf.transform(corpus)
def find_optimal_clusters(data, max_k):
iters = range(5, max_k+1, 2)
sse = []
for k in iters:
sse.append(MiniBatchKMeans(n_clusters=k, init="k-means++", init_size=1024, batch_size=2048, random_state=20).fit(data).inertia_)
f, ax = plt.subplots(1, 1)
ax.plot(iters, sse, marker='o')
ax.set_xlabel('Cluster Centers')
ax.set_xticks(iters)
ax.set_xticklabels(iters)
ax.set_ylabel('SSE')
ax.set_title('SSE by Cluster Center Plot')
find_optimal_clusters(text, 20)