文本是大数据时代结构数据类型的典型代表(文本大数据时代,每个开发人员都需要了解如何分析文本)
此前,我们通过分析文本来更好地理解文本或语料库是由什么组成的,比如POS标注或NER标注会告诉我们文档中出现了什么样的单词;主题模型会告诉我们隐藏在文本中的潜在主题是什么。当然,开发人员也可以使用主题模型来对文档进行聚类,但这并不是主题模型的强项,贸然尝试并期望取得比较好的效果,是不切实际的。注意,由于主题建模的目的是在语料库中查找隐藏的主题,而不是将文档分组,因此目前没有特别好的办法对其进行聚类方面的优化。例如,在执行主题建模之后,文档可以由主题1、2、3组成,占比分别为30%、30%和40%,这些信息还不足以用来进行聚类。
下面我们开始介绍两种更偏向定量分析的机器学习算法:聚类和分类。聚类是【资 ;源 之.家.】一种流行的机器学习任务,经典的聚类任务中所使用的技术也可以用于文本。顾名思义,聚类是将同一组中的数据点分组或聚类的任务,其中同一组中的点比其他组中的点更相似。扩展开来讲,数据点可以是一篇文档,或者是一个单词。聚类是一个无监督的学习问题。在开始将数据点分配给集群或组之前,我们并不知道它们的类别(尽管我们可能知道会找到什么)。
分类任务与聚类任务有些类似,是通过包含已知样本类别(或实例)的训练数据集,来确定未知样本属于一组类别中的哪一个类别。例如,将收到的电子邮件分配到垃圾邮件或非垃圾邮件类,或者将报纸文章分配到指定的类或组。
一个著名的聚类或分类任务的数据集叫作Iris,该数据集包含花的花瓣长度和类【资 ;源 之.家.】别信息。另一个非常流行的数据集叫作MNIST,它包含手写数字,这些数字应该按照它所代表的数字进行分类。
文本聚类遵循标准聚类问题所遵循的大多数原则,但是文本分析领域的维数实在太多了。例如,在Iris数据集中,只有4个特征可以用来标识类或集群。而对于文本,在对问题进行建模时,我们必须处理整个词汇表。当然,我们将尽力使用一些技术,如SVD、LDA和LSI来减少维度。
前几章中大量使用Gensim以及计算语言学的spaCy来执行计算语言学的量化任务。从现在开始,我们将开始使用一个更传统的机器学习库scikit-learn。本书的前面几章已经介绍过部分scikit-learn的内容。
研究聚类和分类算法,我【资 ;源 之.家.】们就不得不提到Word2Vec和Doc2Vec这两种将单词和文档表示为向量方法。这是一种新的关于单词和文档的向量表示方法,比此前涉及的几种算法更为复杂。我们将在第12章中再次探讨Word2Vec和Doc2Vec,并使用它们进行聚类和分类。
1 聚类前的准备工作
最重要的准备工作仍旧是预处理步骤,即删除停用词和词干化。然后就是将文档转换为向量表示。
本节使用scikit-learn来完成聚类、分类和预处理这三个任务。首先需要确定使用哪个数据集。选择有很多,在这里我们选择目前最流行的20个新闻组数据集。由于数据集本身内置于scikit-learn之中,所以加载和使用也很方便。
读者可以参考课后Jupyte【资 ;源 之.家.】r Notebook的聚类分类示例,我们从中摘取了代码片段来解释该过程。
加载数据集的代码如下:
from sklearn.datasets import fetch_20newsgroups categories = [ alt.atheism, talk.religion.misc, comp.graphics, sci.space, ] dataset = fetch_20newsgroups(subset=all, categories=categories, shuffle=True, random_state=42) labels = dataset.tar【资 ;源 之.家.】get true_k = np.unique(labels).shape[0] data = dataset.data上述代码使用import语句访问20 NG数据集,在本例中,我们只选取了4个类别。通过选择所有子集来创建数据集,同时也对数据集进行梳理,保证其状态随机。然后将文本数据转换成机器学习算法可以理解的形式向量。
这里使用的是scikit-learn内置的TfidfVectorizer类来简化工作:
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer(max_df=0.【资 ;源 之.家.】5, min_df=2, stop_words=english, use_idf=True) X = vectorizer.fit_transform(data)X对象是输入向量,包含数据集的TF-IDF表示。在TF-IDF转换时,我们处理的仍是高维数据。为了更好地理解数据的性质,我们将其进行可视化处理。我们可以使用PCA(主成分分析)将数据集中的数据映射到二维空间。PCA可以查找出数据集中的无关成分(数学术语叫作线性不相关)。通过识别高维数据集中的不相关成分,可以有效地把数据降维。当然本例的主要目的还是可视化,在聚类场景中我们会采用其他降维技术:
from sklearn.decomposition im【资 ;源 之.家.】port PCA from sklearn.pipeline import Pipeline newsgroups_train = fetch_20newsgroups(subset=train, categories=[alt.atheism, sci.space]) pipeline = Pipeline([ (vect, CountVectorizer()), (tfidf, TfidfTransformer()), ]) X_visualise = pipeline.fit_transform(newsgroups_train.data).t【资 ;源 之.家.】odense() pca = PCA(n_components=2).fit(X_visualise) data2D = pca.transform(X_visualise) plt.scatter(data2D[:,0], data2D[:,1], c=newsgroups_train.target)简单浏览上述代码。首先还是加载数据集,但只加载了两个类别(我们希望可视化的类别)。在此基础上运行计数矢量化和TF-IDF变换,并拟合了只需要两个关键分量的PCA模型。绘制出效果图后可以清晰地看到数据集中聚类的分离情况如图1所示。
图1 数据集可视化结果
图1中的两个坐标轴分别【资 ;源 之.家.】代表PCA转换后的两个关键分量。
再来回顾原始向量X,用它来实现聚类。在讨论主题模型的章节,我们探讨过一些降维技术,比如SVD和LSA/LSI(读者可以回顾第8章),本例将使用这些技术进行降维。
对数据集执行SVD操作之后还需要进行归一化处理。
from sklearn.decomposition import TruncatedSVD from sklearn.preprocessing import Normalizer n_components = 5 svd = TruncatedSVD(n_components) normalizer = Normalizer(copy=False) lsa = make_pipeli【资 ;源 之.家.】ne(svd, normalizer) X = lsa.fit_transform(X)经过清洗、TF-IDF转换、降维这3个操作之后,最后生成的X向量才是我们需要的输入,下一步就可以开始进行聚类处理了。
2 K-means
K-means是一种经典的聚类学习算法,其原理很容易理解。它会根据用户设置的聚类数量,通过减少聚类中心点和类中其他各点的距离来达到聚类效果。作为一种迭代算法,它会一直执行这个过程,直到聚类中心点稳定不变。我们需要简单地了解这个算法背后的原理。
用scikit-learn实现K-means非常简单,scikit-learn库提供了两种实现方式,一种是标准K-means,另一种是小批量K【资 ;源 之.家.】-means。下面的代码中包括两种实现,用户可自由切换:
minibatch = True if minibatch: km = MiniBatchKMeans(n_clusters=true_k, init=k-means++, n_init=1, init_size=1000, batch_size=1000) else: km = KMeans(n_clusters=true_k, init=k-means++, max_iter=100, n_init=1) km.fit(X)通过执行fit函数,我们训练出了4个不同的聚类。之前我们可视化了聚类结果,这里只把每个类别的主题词打印出来:
original_space_c【资 ;源 之.家.】entroids = svd.inverse_transform(km.cluster_centers_) order_centroids = original_space_centroids.argsort()[:, ::-1]代码中最前面的代码位必须保留,因为LSI转换需要用到它。
terms = vectorizer.get_feature_names() for i in range(true_k): print(“Cluster %d:” % i) for ind in order_centroids[i, :10]: print( %s % terms[ind]) Cluster 0: graphics space i【资 ;源 之.家.】mage com university nasa images ac programposting Cluster 1: god people com jesus don say believe think bible just Cluster 2: space henry toronto nasa access com digex pat gov alaska Cluster 3: sgi livesey keith solntze wpd jon com caltech morality moral每次训练的结果可能会有差异,因为机器学习算法不会每次生成完全相同的结果。
我们可以看到每个聚类都代表了最初选择的4个类别,聚类结果不错。我们可以进一步使用训练好的模型来预测新文档【资 ;源 之.家.】属于哪个聚类,只需保证预测前对新文档进行了相同的预处理步骤。
km.predict(X_test)重新回顾聚类的步骤:先加载数据集,然后选择4个类别,运行预处理步骤,可视化数据,训练一个K-means模型,并为每个聚类打印出最重要的单词以查看它们是否有意义,得到了不错的效果。因为我们制定了分类数量,所以K-means中的K=4。
接下来读者可以尝试不同的预处理步骤,得到不同的聚类效果。下面探讨另一种聚类形式。
3 层次聚类
在介绍层次聚类之前,建议先学习scikit-learn中的聚类教程。在scikit-learn中切换使用不同的模型是很简单的事情,而且聚类过程中的其他步骤始终保持不变。
我们将使用W【资 ;源 之.家.】ard算法尝试分层聚类。该算法基于减少每个聚类内部方差的思想,并采用距离度量来实现。Ward方法是各种层次聚类算法中最早使用的方法之一,它的核心思想是构建聚类并将其按层次排列。本例将使用树形图来表示层次聚类。
使用数据集之前,先通过scikit-learn创建一个由成对距离组成的矩阵:
from sklearn.metrics.pairwise import cosine_similarity dist = 1 – cosine_similarity(X)建立好距离矩阵之后,我们将开始调用SciPy库里的ward和dendrogram函数:
from scipy.cluster.hierarchy imp【资 ;源 之.家.】ort ward, dendrogram linkage_matrix = ward(dist) fig, ax = plt.subplots(figsize=(10, 15)) # set size ax = dendrogram(linkage_matrix, orientation=“right”)SciPy封装了所有复杂步骤,并向我们展示了一个漂亮的图表(见图2)。树形图提出了一个概念,即文档是可以排列的。图中x轴代表文档的名称或索引,因为文档太多,现在无法通过图表看到这些名称或索引。y轴指的是聚类的每个层次结构之间的距离。
图2 一个通过SciPy的Ward算法生成的文本聚类树【资 ;源 之.家.】形图的例子
因为文档数量的关系,我们很难判定图中的聚类结果是否最优,也无法了解文档和聚类之间的关系,因此可以使用较小的语料库进一步确认效果。
这里再次强调,在将语料库输入到聚类算法之前,读者可以尝试使用不同的降维和向量表示方法。Word2Vec和Doc2Vec都提供了非常有趣的方法来实现这一点,Gensim也能够为此提供支持。
下面介绍文本分类算法,这是文本机器学习算法中的另一重要领域。
4 文本分类
上一节讨论了聚类,它是一种无监督的学习算法。而分类则是一种有监督的学习算法。有监督和无监督分别是什么意思?在前面的示例中,有些样本带有标签数据,用于表示当前文档实际属于哪个类的信息。但你也许会注意到我们从【资 ;源 之.家.】未使用过这些信息。当我们训练聚类模型时,从不使用标签。这种学习称为无监督学习,聚类是无监督学习任务的一个常见例子。
在分类问题中,我们知道要将文档或数据点分配给哪些类,并使用这些信息来训练我们的模型。事实上,我们的聚类和分类方法几乎没有任何区别,除了注意标签外,我们只是使用不同的机器或模型进行训练。
在开始将文本输入任何机器学习流程之前,我们需要确保文本清洗和向量化步骤已经完成。虽然没有引入新的步骤,但开发人员可以进行适当的调整来提高模型准确性或性能。
我们将使用NaiveBayes分类器和支持向量机分类器来辅助完成分类任务。这些模型的数学性质超出了本书的描述范围,有兴趣的读者可以参考阅读sciki【资 ;源 之.家.】t-learn的学习文档。
SVM通过核函数把输入空间映射到另一个空间上,以便我们在该空间画出一条线(或者是一个超平面)用于分类,如图3所示。核函数由数学函数组成,用于完成向量转换。
图3 SVM如何通过核函数进行向量转换
NaiveBayes分类器是通过应用贝叶斯定理来工作的,它假定每个特征之间都是独立的,我们可以预测文档可能属于哪个类别。必须注意的是这种独立性通常是假定的。如果这种情况不成立,就称为naive。使用标签计算文档是否属于某个类的先验概率。从本质上讲,我们试图找出哪些单词可以用于预测类别。代码本身非常简单,唯一的区别是我们使用标签来训练模型。这里只列出部分代码片段,如果想获得完整可运【资 ;源 之.家.】行的代码请参考Jupyter Notebook。在训练模型之前不要忘记转换数据,如果是稀疏数组,请先运行X=X.to_array():
from sklearn.naive_bayes import GaussianNB gnb = GaussianNB() gnb.fit(X, labels) from sklearn.svm import SVC svm = SVC() svm.fit(X, labels)训练好的gnb和svm模型通过调用predict()方法来预测未知文档的类别。
NaiveBayes的预测代码如下所示:
gnb.predict(X_test)输出结果是类别【资 ;源 之.家.】数据,如果数据集包含4个类别,则输出结果如下:
array([0, 3, 3, …, 3, 3, 3])与SVM类似,运行以下代码:
svm.predict(X_test)结果如下:
array([0, 3, 3, …, 3, 3, 3])虽然聚类是一个更具解释性的过程,但在分类过程中,我们往往希望提高预测正确类的准确性或成功率。GridSearchCV是一个scikit-learn函数,它允许我们为分类器对象选择最佳参数,并且可以使用classificaiton_report对象检查分类器的性能。
scikit-learn官方文档给出如下示例:
from sklearn import svm, datasets fromsk【资 ;源 之.家.】learn.model_selectionimport GridSearchCV iris = datasets.load_iris() parameters = {kernel:(linear, rbf), C:[1, 10]} svc = svm.SVC() clf = GridSearchCV(svc, parameters) clf.fit(iris.data, iris.target)在本例中,我们分别对核函数是linear和rbf的SVM进行了参数寻优,C值分别是1和10。
另一段官网的代码则可以用多个分类器运行同一个数据集,并比较分类器结果的差异。图【资 ;源 之.家.】4是这些分类器训练和预测时间的比较。
图4 20NG数据集上不同分类器的性能表现
对于想使用更强大的机器学习工具的读者来说,可以参考相关资料来了解如何使用Word2Vec对文档进行分类。第12章将详细介绍这部分内容。
5 总结
总而言之,读者现在可以创建自己的分类程序,比如将电子邮件分类为垃圾邮件和正常邮件。我们已经学习了各种聚类算法,如K-means和层次聚类算法,也讨论了什么是有监督和无监督的学习算法,并学习了如何使用scikit-learn运行这两种算法的示例。
此外,我们还可以使用书中提供的聚类和主题建模工具,以各种方式探索文本数据。下一章将尝试构建一个简单的信息检索系统来查找相似文档。