k-means实现文本分类

K-means 实现文本分类

import

from sklearn.feature_extraction import text
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
from nltk.tokenize import RegexpTokenizer
from nltk.stem import SnowballStemmer

因为自己要从头写的话实在是太麻烦了,就用了一些外部的包

我发现很多时候看不懂别人的代码是没有搞清楚import的东西是干嘛的,所以这次就先讲讲import

sklearn里面自带了很多实用的小工具,这里用到的有:

  • 文章停止词(text.ENGLISH_STOP_WORDS
  • 生成词向量TF-IDF(TfidfVectorizer)
  • KMeans聚类(KMeans)。kmeans聚类我自己也简单写了个,应该会放在文末。

nltk

  • 正则筛选单词tokenize.RegecpTokenizer(我没用明白,用了感觉跟没用一样,和自己用正则写没啥区别)
  • 将单词转换为词根stem.SnowballStemmer

这词用的数据集是abcnews-date-text.csv, 里面是日期和头条的标题,这次的目标是简单给历年的头条分类(只取了前10000条)

给大家看看数据

data = pd.read_csv('./data/abcnews-date-text.csv')
data = data.head(10000)  # 这里只获取部分数据
data.info()
print(data)

"""
Data columns (total 2 columns):
 #   Column         Non-Null Count  Dtype 
---  ------         --------------  ----- 
 0   publish_date   10000 non-null  int64 
 1   headline_text  10000 non-null  object
dtypes: int64(1), object(1)
memory usage: 156.4+ KB

      publish_date                                      headline_text
0         20030219  aba decides against community broadcasting lic...
1         20030219     act fire witnesses must be aware of defamation
2         20030219     a g calls for infrastructure protection summit
3         20030219           air nz staff in aust strike for pay rise
4         20030219      air nz strike to affect australian travellers
...            ...                                                ...
9995      20030407  us units attack palace as fighting rages in heart
9996      20030407   vasco win 10 goal thriller in brazil on edmundos
9997      20030407                       vic bushfires inquiry begins
9998      20030407             vic govt plan aims to reduce water use
9999      20030407          vic govt urged to green light marina plan

[10000 rows x 2 columns]
"""

思路

将文章聚类

要想将文本分类的话,由于我这里是用的非监督学习的方法,所以这里用的是聚类。简单来说,就是将相似的句子放在一起,将它们归为一类。

可是我们要怎么样判断句子是不是相似呢?很简单,我们将文本转换成向量,这样我们就把一个抽象的概念问题转换成了数学距离问题了。简单解释一下:假定我们有一个(多维)坐标系,每个向量指向的位置就代表着一个文本。如果这两个点的位置相近的话,我们就可以认为这两个文本相似了。

TF-IDF统计每一个单词的出现频率,并假设出现频率低的词更能表现出句子特征。那么将文本向量化的时候我们就要考虑一些问题:

  • 有些词太常用了在文本中经常出现,对于我们分类文本一点用都没有(比如a, the, this等等)
  • 有些词是一个词的不同形式,在统计的时候不应该分开来统计(比如dictionary, dictionaries等等)

那么这时候我们就要用停止词和stem了(以下为代码片段)

punc = ['.', ',', '"', "'", '?', '!', ':', ';', '(', ')', '[', ']', '{', '}',"%"]
stop_words = text.ENGLISH_STOP_WORDS.union(punc)
vectorizer = TfidfVectorizer(stop_words=stop_words)


stemmer = SnowballStemmer('english')
    tokenizer = RegexpTokenizer(r'[a-zA-Z\']+')
    new_text = [' '.join(tokenizer.tokenize(' '.join([stemmer.stem(w) for w in s.split(' ')]))) for s in text]

手肘法选择聚类簇的数量

最后,我们成功将这些文本聚类了,可是我们怎么知道到把这些文章分为几类呢

这时候我们就可以用到“手肘法”了

随着聚类数k的增大,样本划分会更加的精细,每个簇的聚合程度会逐渐提高,那么误差平方和SSE自然会逐渐变小,并且当k小于真实的簇类数时,由于k的增大会大幅增加每个簇的聚合程度,因此SSE的下降幅度会很大,而当k到达真实聚类数时,再增加k所得到的聚合程度回报会迅速变小,所以SSE的下降幅度会骤减,然后随着k值的继续增大而趋于平缓,也就是说SSE和k的关系类似于手肘的形状,而这个肘部对应的k值就是数据的真实聚类数.因此这种方法被称为手肘法.

我们按照不同的聚类簇依次给文本聚类,观察文本的聚合程度

wcss = []
for i in range(1,11):
    kmeans = KMeans(n_clusters=i)
    kmeans.fit(X)
    wcss.append(kmeans.inertia_)
plt.plot(range(1,11),wcss)
plt.title('The Elbow Method')
plt.xlabel('Number of clusters')
plt.ylabel('WCSS')
plt.savefig('elbow.png')
plt.show()
Elbow

这里只算了1-10个簇,我们可以看出3个, 5个,8个聚类簇都是“肘点”,这时候就需要人工看看结果来简单判断一下了(因为是无监督学习,学习的结果是未知的)

保存结果

output = pd.DataFrame(desc, columns=['headline_text'])
output['category'] = kmeans.labels_
output.to_csv('abcnews.csv')

完整代码可以在我的Github仓库查看ML/abcnews.py at master · RottenTangerine/ML (github.com)

最近在玩pytorch,简单做了一个CNN,还在考虑要不要手撕pytorch,自己搭一个神经网络跑CNN,看有没有空吧。。

K-Means

简单来说步骤就是

  • 随便找K个中心点
  • 到所有数据相邻的最近的中心点
  • 计算每个中心点对应的所有数据的中心
  • 将新的中心位置赋值给中心点
  • 重复2—4步直至中心点坐标不改变

考虑到篇幅,删了部分代码只留下了一些关键部分,完整代码可以到我的Github仓库RottenTangerine/ML: Machine Learning (github.com)pull一个下来

# K-means
def calculate_distance(x, y):
    return math.sqrt((x[0] - y[0]) ** 2 + (x[1] - y[1]) ** 2)


def generate_random_centers(arr, n):
    i = range(n)
    x = np.random.uniform(min(arr[:, 0]), max(arr[:, 0]), n)
    y = np.random.uniform(min(arr[:, 1]), max(arr[:, 1]), n)
    return np.stack((i, x, y), axis=1)


def scatter(a, x, y, i):
    color_map = ['green', 'orange', 'red']
    a.scatter(x, y, color=color_map[i])
    return


k = 3
centers = generate_random_centers(data, k)
step_counter = 0
while True:
    step_counter += 1
    fig, ax = plt.subplots()
    centers_backup = centers.copy()
    location_list = [[*min([(c[0], *location, calculate_distance(c[1:], location))
                            for c in centers], key=lambda a: a[-1])[:-1]]
                     for location in data]
    location_list = np.asarray(location_list)
    for index in range(k):
        dots = location_list[location_list[:, 0] == index][:, 1:]

        scatter(ax, dots[:, 0], dots[:, 1], index)
        ax.scatter(*centers[index][1:], marker='x', c='black')
        ax.set_title(f'Step: {step_counter}')
        ax.axis('equal')
        if len(dots) != 0:
            centers[index] = [index, *np.mean(dots, axis=0)]
    plt.savefig(f'{step_counter}.png')
    plt.show()
    if np.equal(centers_backup, centers).all():
        break

(如果用cache会快很多,但是ndarray不是hash able的,懒得再弄了)

可视化

1
1
1
1
1
1
1
1
1
1