当前位置:   article > 正文

NLP自然语言处理实战(三):词频背后的语义--3.主成分分析_词频 语义 输出建议

词频 语义 输出建议

SVD用于降维时,主成分分析(PCA)是SVD的另一个叫法。scikit-learn中的PCA模型对SVD做了一些调整,这将提高NLP流水线的精确率。
一方面,sklearn.PCA自动通过减去平均词频来“中心化”数据。另一方面,其实现实使用了一个更微妙的技巧,即使用一个名为flip_sign的函数来确切地计算奇异向量的符号。
最后,sklearn中的PCA实现了一个可选的"白化"(whitening)步骤。这类似于在将词项-文档向量转换为主题-文档向量时忽略奇异值的技巧。白化技术可以像sklearn.StandardScaler转换一样将数据除以这些方差(方差归一化处理)。这有助于分散数据,使任何优化算法都不太可能迷失于数据的U型“半管道”或“河流”(指局部集中的数据)中。而当数据集中的特征相互关联时,这些现象就会出现。

1.主成分分析

1.1三维向量上的PCA

在二维投影中,我们将这些点叠加在一起,可以防止人眼或机器学习算法将这些点分隔成有意义的簇。但是,SVD通过沿着高维空间的低维阴影的维度方向最大化方差来保持向量的结构和信息内容。这就是机器学习所需要的,这样每个低维向量就能捕捉到它所代表的东西的本质。

import pandas as pd
pd.set_option('display.max_columns', 6)
from sklearn.decomposition import PCA  # 尽管在scikit-learn中它被成为PCA,但这确实是SVD
import seaborn
from matplotlib import pyplot as plt
from nlpia.data.loaders import get_data
df = get_data('pointcloud').sample(1000)
pca = PCA(n_components=2)  # 将三维点云缩减为二维投影,以便在二维散点图中显示
df2d = pd.DataFrame(pca.fit_transform(df), columns=list('xy'))
df2d.plot(kind='scatter', x='x', y='y')
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述


1.2回归NLP

在5000条标记为垃圾短消息(或非垃圾短消息)的短消息预料中,我们使用SVD来寻找主成分。我们把主题数限制在16个,并同时使用scikit-learn PCA模型和截断的SVD模型来观察两者是否有所不同
截断的SVD模型被设计成用于稀疏矩阵。稀疏矩阵就像大部分都为空的电子表格,但是一些有意义的值分散在矩阵中。与TruncatedSVD相比,sklearn PCA模型使用填充了所有这些0的密集矩阵,可以提供更快的解决方案。但PCA浪费了很多内存来保存所有那些0。scikit-learn中的TfidfVectorizer输出稀疏矩阵,因此在将结果与PCA进行比较之前,需要将这些矩阵转换为密集矩阵
首先,我们从nlpia包中的DataFrame加载短消息:

import pandas as pd
from nlpia.data.loaders import get_data
pd.options.display.width - 120
sms = get_data('sms-spam')
index = ['sms{}{}'.format(i, "!"*j) for (i, j)in zip(range(len(sms)), sms.spam)]  # 我们向短消息的索引号后面添加一个感叹号,以使它们更容易被发现
sms.index = index
print(sms.head(6))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述
接下来可以计算每条消息的TF-IDF向量:

from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.tokenize.casual import casual_tokenize
tfidf = TfidfVectorizer(tokenizer=casual_tokenize)
tfidf_docs = tfidf.fit_transform(raw_documents=sms.text).toarray()
print(len(tfidf.vocabulary_))
tfidf_docs = pd.DataFrame(tfidf_docs)
tfidf_docs = tfidf_docs - tfidf_docs.mean()  # 通过减去平均值对向量化的文档(词袋)进行中心化处理
print(tfidf_docs.shape)
print(sms.spam.sum())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Out[1]:9232
Out[2]:(4837, 9232)
Out[3]:638
至此,我们有4837条短消息,其中包含来自分词器(casual_tokenize)的9232个不同的1-gram词条。在4837条短消息中,只有638(13%)被标记为垃圾短消息。所以这个训练集不均衡,正常消息和垃圾消息的比大约为8:1
针对这种正常消息的抽样偏差,我们可以通过减少任何对正常消息分类正确的模型的“回报”来解决。但是大型词汇表的数量|V|很难处理,词汇表中的9232个词条比要处理的4837条消息(样本)还多。而这些短消息中,只有一小部分被标记为垃圾消息。这就是造成过拟合的原因。在大型词汇表中,只有少数独立的词会被标记为垃圾词。
过拟合的意思是指我们只会在从词汇表提取除几个关键的词。因此,垃圾短消息过滤器将依赖这些垃圾词,它们存在于那些过滤掉的垃圾短消息的某处。垃圾消息散布者只需要使用这些垃圾词的同义词,就可以很容易地绕过过滤器。
降维就是针对过拟合的主要应对措施。通过将多维度(词)合并到更少的维度(主题)中,我们的NLP流水线将变得更加通用。如果降维或缩小词汇表,我们的垃圾消息过滤器将能够处理更大范围的消息。
这正是LSA所做的事情,它减少了维度,因此有助于防止过拟合(overfitting)。通过假设词频之间的线性关系,LSA可以基于小数据集进行泛化。因此,如果词“half”出现在包含诸如大量“off”的垃圾消息中(如“Half off”),LSA帮助找到这些词之间的关联并得到关联的程度,因此它将垃圾消息中的短语“half off”推广到诸如“80% off”这样的短语,而且,如果NLP数据中还存在“discount”和“off”之间的关联,它还可以进一步推广到短语“80% discount”。
泛化NLP流水线有助于确保它适用于更广泛的现实世界的短消息集,而不仅仅是这一组特定的消息集


1.3基于PCA的短消息语义分析

下面我们先尝试一下scikit-learn中的PCA模型。我们把数据集9232维的TF-IDF向量转换为16为主题向量:

from sklearn.decomposition import PCA
pca = PCA(n_components=16)
pca = pca.fit(tfidf_docs)
pca_topic_vectors = pca.transform(tfidf_docs)
columns = ['topic{}'.format(i) for i in range(pca.n_components)]
pca_topic_vectors = pd.DataFrame(pca_topic_vectors, columns=columns, index=index)
print(pca_topic_vectors.round(3).head(6))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述
如果大家对这些主题感兴趣,可以通过检查权重来找出每个词的权重。通过查看权重(.components属性,可以获得任何已经拟合好的sklearn转换的权重),可以看到词half和off(如在half off中)一起出现的频率,然后确定哪个主题是“discount”。
首先,我们将词分配给PCA转换中的所有维度。这里需要将词按正确的顺序排列,因为TFIDFVectorizer将词汇表存储为字典,并将词汇表中的每个词项映射为索引号(列号):

column_nums, terms = zip(*sorted(zip(tfidf.vocabulary_.values(), tfidf.vocabulary_.keys())))  # 根据词项频率对词汇表进行排序,当对某个不按照最左边元素排序的序列解压并在排序后重新压缩时,这里的“zip()*sorted(zip())”模式十分有用
  • 1

现在,我们可以创建一个包含权重的不错的Pandas DataFrame,所有列和行的标签都处在正确的位置上:

weights = pd.DataFrame(pca.components_, columns=terms, index=['topic{}'.format(i) for i in range(16)])
print(weights.round(3).head())
  • 1
  • 2

在这里插入图片描述
其中,有些列(词项)并不那么有意思,所以下面我们来探索一下tfidf.vocabulary,看看是否能找到那些“half off”词项以及它们所属的主题:

deals = weights['! ;) :) half off free crazy deal only $ 80 %'.split()].round(3) * 100
print(deals)
print(deals.T.sum())
  • 1
  • 2
  • 3

在这里插入图片描述
主图4,8和9似乎都包含“deal”(交易)主题的正向情感。而主题0,3和5似乎是反deal的主题,即负向deal。因此,与deal相关的词可能对一些主题产生正向影响,而对另一些主题产生负向影响。并不存在一个明显的“deal”主题。
重要说明: casual_tokenize分词器将“80%”切分成[“80”,“%”],将“ $ 80 million”切分成[“$”,“80”,“million”]。因此,除非使用LSA或2-gram分词器,否则NLP流水线不会注意到“80%”和“$80 million”之间的差别,它们共享词条“80”。

上面对主题的理解,就是LSA的挑战之一。LSA只允许词之间的线性关系,另外,我们通常处理的只有小规模语料库。所以LSA主题倾向于以人们认为没有意义的方式将词组合起来。来自不同主题的几个词将被塞进单一维度(主成分)中,以确保模型在使用9232个词时能够捕捉到尽可能多的差异。


1.4基于截断的SVD的短消息语义分析

现在我们可以在scikit-learn中试用一下TruncatedSVD模型。这是一种更直接的LSA方式,它绕过了scikit-learn PCA模型,因此我们可以看到在PCA包装器内部到底发生了什么。它可以处理稀疏矩阵,所以如果我们正在处理大规模数据集,那么无论如何都要使用TruncatedSVD而非PCA。TruncatedSVD的SVD部分将TF-IDF矩阵分解为3个矩阵,其截断部分将丢弃包含TF-IDF矩阵最少信息的维度。这些被丢弃的维度表示文档集中变化最小的主题(词的线性组合),它们可能对语料库的总体语义没有意义。它们可能包含许多停用词和其他词,这些词在所有文档中均匀分布。
下面将使用TruncatedSVD仅仅保留16个最有趣的主题,这些主题在TF-IDF向量中所占的方差最大:

from sklearn.decomposition import TruncatedSVD
svd = TruncatedSVD(n_components=16, n_iter=100)  # 就像在PCA中一样,这里将计算16个主题,但是将在数据上迭代100次(默认为5次),以确保这里的结果几乎与在PCA中一样精确
svd_topic_vectors = svd.fit_transform(tfidf_docs.values)  # fit_transform在一部当中分解TF-IDF向量并将它们转换为主题向量
svd_topic_vectors = pd.DataFrame(svd_topic_vectors, columns=columns, index=index)
print(svd_topic_vectors.round(3).head(6))
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述
TruncatedSVD的这些主题向量与PCA生成的主题向量完全相同!这个结果是因为我们非常谨慎地使用了很多次的迭代次数(n_iter),并且还确保每个词项(列)的TF-IDF频率都做了基于零的中心化处理(通过减去每个词项上的平均值)
下面花点时间看看每个主题的权重,并试着能够理解它们。在既不知道这些主题的相关对象也不知道高权重词的情况下,大家认为自己能把这6条短消息判定为垃圾或非垃圾短消息吗?也许看看垃圾短消息标签后面的“!”标签有助于上述判定。这虽然很困难,但还是有可能的,特别是对机器来说,它可以查看所以5000个训练样本,计算出每个主题的阈值来分离垃圾和非垃圾短消息的主题空间。


1.5基于LSA的垃圾短消息分类的结果

要了解向量空间模型在分类方面的效果如何,一种方法是查看类别内部向量之间的余弦相似度与它们的类别归属之间的关系。下面,我们看看对应文档对之间的余弦相似度是否对这里的特定二分类问题有用。我们计算前6条消息对应的前6个主题向量间的点积,我们应该会看到,任何垃圾短消息之间的正的余弦相似度更大。

import numpy as np
svd_topic_vectors = (svd_topic_vectors.T / np.linalg.norm(svd_topic_vectors, axis=1)).T  # 对每个主题向量按照其长度(2范数)进行归一化,然后用点积计算余弦距离
print(svd_topic_vectors.iloc[:10].dot(svd_topic_vectors.iloc[:10].T).round(1))
  • 1
  • 2
  • 3

在这里插入图片描述
从上到下读取sms0对应的列(或从左到右读取sms0对应的行),我们会发现,sms0和垃圾短消息(sms5!、sms6!、sms8!、sms9!)之间的余弦相似度是显著的负值。sms0的主题向量与垃圾短消息的主题向量有显著不同,非垃圾短消息所谈论的内容与垃圾短消息是不同的。

对sms2!对应的列或行进行相同的处理,我们会看到它与其他垃圾短消息正相关。垃圾短消息具有相似的语义,它们谈论相似的“主题”。

这也是语义搜索的工作原理。我们可以使用查询向量和文档库中所有主题向量之间的余弦相似性来查找其中语义最相似的消息。离该查询向量最近的文档(最短距离)对应的是含义最接近的文档。垃圾性只是混入的短消息主题中的一种“意义”。

遗憾的是,每个类(垃圾短消息和非垃圾短消息)中主题向量之间的相似性并没有针对所有消息进行维护。对这组主题向量来说,在垃圾短消息和非垃圾短消息之间画一条直线把它们区分开十分困难。我们很难设定某个与单个垃圾短消息之间的相似度阈值,以确保该阈值始终能够正确地区分垃圾和非垃圾短消息。但是,一般来说,短消息的垃圾程度越低,它与数据集中另外的垃圾短消息之间的距离就越远(不太相似)。如果想使用这些主题向量构建垃圾短消息和过滤器的话,那么这就是你所需要的。机器学习算法可以单独查看所有垃圾和非垃圾短消息标签的主题,并可能在垃圾和非垃圾短消息之间绘制超平面或其他分界面。

在使用截断的SVD时,计算主题向量之前应该丢弃特征值。scikit-learn在实现TruncatedSVD时采用了一些技巧,使其忽略了特征值(S矩阵)中的尺度信息,其方法使:

  • 将TF-IDF向量按其长度(2范数)对TF-IDF词频进行归一化
  • 通过减去每个词项的平均频率进行中心化处理

归一化过程消除了特征值中的任何缩放或偏离,并将SVD集中于TF-IDF向量变换的旋转部分。通过忽略特征值(向量尺度或长度),可以摆好对主题向量空间进行限定的超立方体,这允许我们对模型中的所有主题一视同仁。如果想在自己的SVD实现中使用该技巧,那么可以在计算SVD或截断的SVD之前,按2范数对所有TF-IDF向量进行归一化,在PCA的scikit-learn实现中,可以通过对数据进行中心化和白化处理来实现这一点。

无论使用那种算法或具体实现来进行语义分析(LSA、PCA、SVD、截断的SVD或LDiA),都应该首先对词袋向量或TF-IDF向量进行归一化。否则,可能会在主题之间产生巨大的尺度差异。主题之间的尺度差异会降低模型区分细微的、出现不频繁的主题的能力。


1.6一点补充

  • PCA是一种机器学习方法,它通过求特征的协方差,对其协方差矩阵进行特征分解。所以PCA使用了矩阵分解(特征分解)的手段,实现一种特征降维和主成分分析的效果。
  • SVD本身是一种矩阵分解方法,可以认为是一种线性代数领域的数学手段,它其实是和PCA中用到的特征分解是对等的概念,都是一种矩阵分解的方法。
  1. 对于方阵,可以进行特征分解
  2. 对于非方阵,不能进行特征分解,可做SVD分解

因为PCA是对特征的协方差矩阵做分解,而协方差矩阵必为方阵,所以肯定是可做特征分解的。这也是PCA的直接解法。
而SVD分解,常是用在直接对特征矩阵本身进行分解。

特征矩阵,一般行数表示样本数量,列数表示特征数量,对N个样本,特征维度为M的矩阵A,其SVD分解可表示为:

在这里插入图片描述
U的维度为(N,N),其实就是AA^T的特征分解矩阵,V的维度为(M,M),其实就是A ^TA的特征分解矩阵。

这就巧了,PCA里面需要求特征的协方差矩阵,那不就是我们要的A ^TA吗?(注意这个A需要先减均值去中心化),所以SVD就可以恰巧解决PCA的问题了。SVD分解后的右奇异矩阵,对应着PCA所需的主成分特征矩阵。

同样,特征的协方差矩阵是 A^TA ,而AA^T其实就是样本的协方差矩阵。
(这里的前提是,矩阵表示数据集时,每列代表一个特征,每行代表一个样本。如果表示上关系调换了,也就是说矩阵做了转置,那么左右奇异矩阵代表的含义也会对掉)。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/377556
推荐阅读
相关标签
  

闽ICP备14008679号