[关闭]
@ArrowLLL 2017-04-18T04:28:57.000000Z 字数 5881 阅读 4266

K-Means 文本聚类

python Kmeans 机器学习


主页地址 :月光森林


引言

信息内容安全的一个作业, 要求用K-means聚类算法对一定量的新闻文本进行分类。为便于总结,同时交作业,以此记之。

参考

文档树

.
├── findK.py
├── Kmeans.py
├── pycache
│      └── findK.cpython-35.pyc
├── test
│      ├── C4-Literature11.txt
│      ├── C4-Literature14.txt
│      ├── C4-Literature23.txt
│      ├── C4-Literature32.txt
│      ├── C4-Literature4.txt
│      └── C4-Literature7.txt
├── train
│      ├── C4-Literature1.txt
│      ├── ...
│      └── C4-Literature32.txt
└── tree.txt

3 directories, 37 files

关于测试集的产生,使用的是python的自带的随机函数random.choice() ,具体产生方式如下:
getTest.png-38.6kB

模型框架

整个聚类模型主要分为5个部分,分别是 : 提取特征,找k值,K-means聚类,结果评价,对测试集分类。

提取特征

提取文本特征的主要代码是 findK.py 文件内的 getTags() 函数和 getMatrix(wordDic, wordlist) 函数。

找k值

找k值使用的 elbow point 方法。枚举聚类的 k 值,作出在每个 k 值下各个数据点到聚类中心的距离的折线图,图像中的拐点可以粗略地估计为聚类数量即k值。该方法主要参考Introduction to K-means Clustering,在《kaggle竞赛之路》中也提到过该方法。

这一部分会用到下一个会说到的K-means聚类,这里先按下不表,下一节说明。

找k值这一部分是通过 findK.py 的主函数部分完成的。枚举的聚类数量k值为 1 到 7,并且在每一个固定的k值下通过奔跑10次K-means算法防止随机取点造成只能取得局部最优解而不能得到全局最优解。

最后通过pylab下的plot函数作出图像如下:

kmeans拐点final.png-30.7kB

从图像中可以看到一个很明显的拐点是 k = 3 的点,故选取k值为3。

K-Means聚类

kmeans聚类使用的是sk-learn自带的KMeans类KMeans()。参数n_clusters表示聚类中心的个数。中使用到的属性和方法如下:

关于K-Means算法的细节及数学基础可以参考第一部分提到的Introduction to K-means Clustering

结果评价

聚类结果评价使用的是轮廓系数(Silhouette Coefficient)。具体的内涵可参考 参考部分 给出的轮廓系数的相关链接,在《kaggle竞赛之路》中也提到过这种评价方法。

该评价方法结合了内聚度与分离度两种因素,将结果定性在-1到1之间。轮廓系数越接近1说明聚类结果越好,结果小于0说明聚类并不成功。

代码当中直接使用了sklearn.metrics包中的silhouette_score类silhouette_score(X, label, metrics) , X表示聚类对象矩阵,label表示聚类结果标签,metrics 表示聚类模型中计算距离的方法,默认为 'euclidean' 即欧几里得距离计算各个点到聚类中心的距离。

对测试集分类

对测试集分类使用的是 Kmeans.py 的主函数中还有一个对未知文本的分类预测函数 KMeans.predict(X), 它可以对矩阵X中的量做出预测。X的列数与Kmeans中样本的列数相同,行数为测试的样本数。返回一个矩阵表示预测的分类结果。

代码及其相应注释

findK.py

  1. #!usr/bin/python3
  2. #coding: utf-8
  3. import os
  4. import jieba
  5. import jieba.analyse
  6. import numpy as np
  7. from sklearn.cluster import KMeans
  8. from pylab import *
  9. # 提取训练集中文本的关键字
  10. def getTags() :
  11. path = './train/'
  12. wordDic, wordslist = {}, []
  13. # os.listdir() 提取相应路径下的文件名
  14. for fileName in os.listdir(path) :
  15. # 以gbk格式打开文本
  16. with open(path + fileName, encoding = 'gbk') as f :
  17. # extract_tags() 函数提取关键字
  18. text = ' '.join(jieba.analyse.extract_tags(f.read(), topK = 10))
  19. words = text.split()
  20. # 将当前文本关键字放入总的关键字列表中
  21. wordslist.append(words)
  22. # 将当前文本关键字放入关键字字典中
  23. for word in words :
  24. wordDic[word] = 0
  25. # 遍历官架子字典,为每一个关键字取定一个字段值,即列号存入字典
  26. for (word, seqNo) in zip(wordDic, range(len(wordDic))) :
  27. wordDic[word] = seqNo
  28. return wordDic, wordslist
  29. def getMatrix(wordDic, wordslist) :
  30. # 取定第一维为文本数,第二维为关键字数量的零矩阵
  31. wordMatrix = np.zeros([len(wordslist), len(wordDic)])
  32. # 第i个文本包含第j个词,则wordMatrix[i, j] = 1,构造01矩阵
  33. for (i, words) in zip(range(len(wordslist)), wordslist) :
  34. for word in words :
  35. wordMatrix[i, wordDic[word]] = 1
  36. # 返回01矩阵
  37. return wordMatrix
  38. if __name__ == '__main__' :
  39. # 提取文本关键字列表与关键词字典
  40. wordDic, wordslist = getTags()
  41. # 构造关键词01矩阵
  42. wordMatrix = getMatrix(wordDic, wordslist)
  43. # n 表示聚类数量K的封顶值
  44. n, distance = 8, []
  45. for i in range(1, n) :
  46. # 初始化最小距离为 -1
  47. minDis = -1
  48. # 跑10次Kmeans以保证取得的是全局最优解(10可以更大)
  49. for j in range(10) :
  50. # 调用sklearn的kmeans类
  51. kmeans = KMeans(n_clusters = i).fit(wordMatrix)
  52. # centers 表示每个样本对应的聚类中心点
  53. centers = np.array([kmeans.cluster_centers_[k] for k in kmeans.labels_])
  54. # 计算当前Kmeans结果的距离,取欧式距离但没有开方
  55. dis = ((wordMatrix - centers) ** 2).sum()
  56. # 更新最小值
  57. minDis = dis if minDis < 0 else min(minDis, dis)
  58. # 将相应的k值存入distance列表
  59. distance.append(minDis)
  60. # 绘图
  61. plot(range(1, n), distance)
  62. # 绘制网格
  63. grid()
  64. show()

Kmeans.py

  1. #!usr/bin/python3
  2. #coding: utf-8
  3. import os
  4. import numpy as np
  5. import jieba.analyse as jbays
  6. from sklearn.cluster import KMeans
  7. from sklearn.metrics import silhouette_score
  8. import findK
  9. # 获取测试集的关键词01矩阵,与findK.py 中getMatrix()函数基本相同,不再做过多解释
  10. def getTestMatrix(wordDic, matrixshape1) :
  11. path = './test/'
  12. keyWordslist = []
  13. for fileName in os.listdir(path) :
  14. with open(path + fileName, encoding = 'gbk') as f :
  15. text = ' '.join(jbays.extract_tags(f.read(), topK = 10))
  16. words = text.split()
  17. keyWordslist.append(words)
  18. matrixshape0 = len(os.listdir(path))
  19. testMatrix = np.zeros((matrixshape0, matrixshape1))
  20. for (i, keywords) in zip(range(matrixshape0), keyWordslist) :
  21. for keyword in keywords :
  22. if keyword in wordDic :
  23. testMatrix[i, wordDic[keyword]] = 1
  24. return os.listdir(path), testMatrix
  25. if __name__ == '__main__' :
  26. # 获得训练集的关键词字典与列表
  27. worDic, wordslist = findK.getTags()
  28. # 获得训练集关键词的01矩阵
  29. minDis, wordMatrix = -1, findK.getMatrix(worDic, wordslist)
  30. # 获得测试集的文本标题与测试集关键词01矩阵
  31. fileName, testMatrix = getTestMatrix(worDic, wordMatrix.shape[1])
  32. # 奔跑10次KMeans防止取到局部最优解,与findK.py中类似,不过多解释
  33. for j in range(10) :
  34. # 取k值为3
  35. kmeans = KMeans(n_clusters = 3).fit(wordMatrix)
  36. centers = np.array([kmeans.cluster_centers_[k] for k in kmeans.labels_])
  37. dis = ((wordMatrix - centers) ** 2).sum()
  38. if minDis < 0 or dis < minDis :
  39. minDis,testKmeans = dis, kmeans
  40. # 使用轮廓系数评价聚类模型
  41. siCoScore = silhouette_score(wordMatrix, testKmeans.labels_, metric='euclidean')
  42. print ('轮廓系数为:', siCoScore)
  43. # 预测测试集的分类结果
  44. result = testKmeans.predict(testMatrix)
  45. for i in range(result.shape[0]) :
  46. print (fileName[i] + ' 类别为 :\t', result[i])

最终结果

KmeansResult.png-60.2kB

最终结果显示轮廓系数为0.043左右,聚类成功但是结果并不是很理想。推断应该有两个原因:

一是提取的关键词并不理想, 并且会有近义词的存在将原本意思相同的两个关键词作为完全不同的两个关键词,而近义词需要很大的语料库支撑。

二是01矩阵这个模型并不是很适合。做完以后想到可以用tf-idf值代替0和1作为矩阵中的值出现。但是后来写完tfidf的模型以后发现,使用tf-idf值会因为测试集中每个测试文本都会影响整体的idf值,每一次都要重新计算一遍,如果文本量巨大,计算时间成本相应地也会变得很大,就类似于实时的KNN算法一样,每一个测试样本又会作为训练样本加入原本的训练集。这种奔跑学习的方式虽然也算一种暴力美学,但是实在是有失优雅。所以放弃了使用tfidf的想法,改为使用jieba自带的extract_tags()函数提取关键词构建01矩阵作为特征。只是没想到结果会变得这么差。

最后只好以 结果至少没有小于0 来安慰自己了。

以上です~

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注