【Python机器学习】NLP词频背后的含义——主成分分析

目录

三维向量上的PCA

回归NLP

基于PCA的短消息语义分析

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

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

LSA和SVD的增强


当SVD用于降维时,主成分分析PCA)是SVD的另一个叫法。scikit-learn中的PCA模型对SVD做了一些调整,这将提高NLP流水线的精确率。

一方面,sklearn.PCA自动通过减去平均词频来"中心化"数据。另一方面,其实现时使用了一个更微妙的技巧,即使用一个名为flip_sign的函数来确切地计算奇异向量的符号。

最后,sklearn中的PCA实现了一个可选的"白化"步骤,这类似于在将词-文档向量转换为主题-文档向量时忽略奇异值的技巧。与仅仅将S矩阵中所有的奇异值设为1不同,白化技术可以像sklearn.StandardScaler转换一样将数据除以这些方差(方差归一化处理)。这有助于分散数据,使任何优化算法都不太可能迷失于数据的U型"半管道"或"河流"(指局部集中的数据)中。而当数据集中的特征相互关联时,这些现象就会出现。

对于大多数"实际"问题,我们想使用sklearn.PCA模型来进行LSA分析,唯一例外的是,要处理的文档数多于内存中所能容纳的文档数,在这种情况下,将需要sklearn中的IncrementalPCA模型。

三维向量上的PCA

现在有一个对真实物体表面的三维扫描,对其数据做点云,我们手动将点云旋转到一个特定的方向,以最小化图中窗口轴上的方差。我们之所以这样做是为了让人们很难认出它的本来面目。如果SVD(LSA)对文档-词向量这样做的话,它将在这些向量中隐藏信息,在二维投影中,我们将这些点叠加在一起,可以防止人眼或机器学习算法将这些点分隔成有意义的簇。但是SVD通过沿着高维空间的低维阴影的维度方向最大化方差来保持向量的结果和信息内容。这就是机器学习所需要的,这样每个低维向量就能捕捉到它所代表的东西的本质。SVD最大化每个轴的方差。而方差是一个很好的信息指标,或者说就是要找的本质:

python 复制代码
import pandas as pd
pd.set_option('display.max_columns',6)
from sklearn.decomposition import PCA
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()

运行上述脚本,二维投影的方向可能会随机地从左到右旋转,但它不会扭转到新的角度。我们计算二维投影的方向,使最大的方差始终与x轴(第一个轴)对齐,第二大的方差总是与y轴(也就是阴影或投影的第二维)对齐。但是,这些轴的极性(符号)是任意的,因为优化还剩有两个自由度,于是可以自由地沿着沿x轴或y轴或者同事沿着这两个轴翻转向量(点)的极性。

回归NLP

在5000条标记为垃圾短消息(或非垃圾短消息)的短消息语料库中,我们使用SVD来寻找主成分。考虑都按这份语料的词汇量和主题数有限,我们把主题数限制在16个。我们同时使用scikit-learn PCA模型和截断的SVD模型来观察两个模型的不同。

截断的SVD模型被设计成用于稀疏矩阵。稀疏矩阵是存在很多相同值(通常为零或NaN)元素的矩阵。NLP词袋和TF-IDF矩阵几乎总是稀疏的,因为大多数文档不会包含词汇表中的大部分词。大部分的词频都为0。

稀疏矩阵就像大部分都为空的电子表格,但是一些有意义的值分散在矩阵中。与TruncatedSVD相比,sklearn PCA浪费了很多内存来保存那些0。scikit-learn中的TfidfVectorizer输出稀疏矩阵,因此在将结果与PCA进行比较值钱,需要将这些矩阵转换成密集矩阵。

首先,从nlpia包中的DataFrame加载短消息:

python 复制代码
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))

接下来可以计算每条消息的TF-IDF向量:

python 复制代码
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.tokenize 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())

现在,有4837条短信,其中包含来自分词器(casual_tokenize)的9232个不同的1-gram词条。在4837条短消息中,只有638条(13%)被标记为垃圾短消息。所以这个训练集不均衡,非垃圾短消息和垃圾短消息的比大约是8:1。

针对这种正常短消息的抽样偏差,我们可以通过减少任何对正常短消息分类正确的模型的"回报"来解决。但是大型词汇表的数量|V|很难处理,词汇表中的9232个词条比要处理的4837条消息(样本)还多,也就是说,词库(或者词汇表)中包含的独立词数,比短消息还多。而这些短消息中,只有一小部分(1/8)被标记为垃圾短消息。这就是造成过拟合的原因。在大型词汇表中,只有少数独立的词会被标记为垃圾词。

过拟合的意思是指我们只会在从词汇表提取的几个关键词。因此,垃圾短消息过滤器将依赖这些垃圾词,它存在于那些过滤掉的垃圾短消息的某处。垃圾消息散布者只需使用这些垃圾词的同义词,既可以很容易地绕过过滤器。如果词汇表中没有包含这些垃圾短消息散布者使用的新同义词,那么过滤器就会将那些巧妙构造的垃圾短消息误分类为正常短消息。

上述这种过拟合是自然语言处理中的一个固有问题。很难找到一个标注好的自然语言数据集,它包含了人们可能会说的标注为语料库中的所有方式。我们无法找到这样一个大规模的短消息数据库,该数据库包含了垃圾和非垃圾的所有表达方式,所以,剩下的就需要有针对过拟合的应对措施。我们必须使用在少量样本上就可以泛化得很好的算法。

降维时针对过拟合的主要应对措施。通过将多维度(词)合并到更少维度(主题)中,我们的NLP流水线将变得更加通用。如果降维或缩小词汇表,我们的垃圾短消息过滤器将能够处理更大范围的短消息。

这正是LSA所做的事情,它减少了维度,因此有助于防止过拟合。通过假设词频之间的线性关系,LSA可以基于小数据集进行泛化。因此,如果词"half"出现在包含诸如大量"off"的垃圾短消息中,LSA帮助找到这些词之间的关联并得到关联的程度,因此它将垃圾消息中的短语"galf off"推广到诸如"80% off"这样的短语,而且,如果NLP数据中还存在"discount"和"off"之间的关联,它还可以进一步推广到短语"80% discount"。

泛化NLP流水线有助于确保它适用于更广泛的现实世界的短消息集,而不仅仅是这一组特定的短消息集。

基于PCA的短消息语义分析

下面尝试scikit-llearn中的PCA模型,之前把三维的点云变成了"马"形状的二维点图,现在,把数据集9232维的TF-IDF向量转换为16维主题向量:

python 复制代码
from sklearn.decomposition import PCA
pca=PCA(n_components=16)
pca=pca.fit(tfidf_docs)
pca_topic_vector=pca.transform(tfidf_docs)
columns=['topic{}'.format(i) for i in range(pca.n_components)]
pca_topic_vector=pd.DataFrame(pca_topic_vector,columns=columns,index=index)
print(pca_topic_vector.round(3).head(6))

通过查看权重,可以看到词half和off一起出现的频率,然后确定哪个主题是"discount"。

首先,我们将词分配给PCA转换中的所有维度。这里需要将词按正确的顺序排列,因为TFIDFVectorizer将词汇表存储为字典,并将词汇表中的每个词项映射为索引号(引号):

python 复制代码
print(tfidf.vocabulary_)
column_nums,terms=zip(*sorted(zip(tfidf.vocabulary_.values(),tfidf.vocabulary_.keys())))
print(terms)

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

python 复制代码
weights=pd.DataFrame(pca.components_,columns=terms,index=['topic{}'.format(i) for i in range(16)])
pd.options.display.max_columns=8
print(weights.head(4).round(3))

其中,有些词(词项)并不有意思,下面探索tfidf.vocabulary,查看能否找到那么"half off"词项以及它们所属的主题:

python 复制代码
pd.options.display.max_columns=12
deals=weights['! ;) :) half off off free crazy deal only $ 80 %'.split()].round(3)*100
print(deals)
print(deals.T.sum())

可以看到,主题4、8、9似乎都包含"deal"主题的正向情感。而主题0、3、5、10等似乎是反deal的主题,也就是deal反面的消息:负向deal。因此,与deal相关的词可能对一些主题产生正向影响,而对另一些主题产生负面影响。并不存在一个明显的"deal"主题编号。

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

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

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

下面将使用TruncatedSVD仅仅保留16个最有趣的主题,这些主题在TF-IDF向量中所占的方差最大:

python 复制代码
from sklearn.decomposition import TruncatedSVD
svd=TruncatedSVD(n_components=16,n_iter=100)
svd_topic_vector=svd.fit_transform(tfidf_docs.values)
svd_topic_vector=pd.DataFrame(svd_topic_vector,columns=columns,index=index)
print(svd_topic_vector.round(3).head(6))

TruncatedSVD的这些主题向量与PCA生成的主题向量完全相同。这个结果是因为我们非常谨慎的使用了很多的迭代次数(n_iter参数),并且还确保每个词项(列)的TF-IDF频率都做了基于零的中心化处理(通过减去每个词项上的平均值)。

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

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

python 复制代码
import numpy as np
svd_topic_vector=(svd_topic_vector.T/np.linalg.norm(svd_topic_vector,axis=1)).T
print(svd_topic_vector.iloc[:10].dot(svd_topic_vector.iloc[:10].T).round(1))

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

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

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

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

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

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

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

如果没有这种归一化,出现不频繁的主题会获得比它们应该获得的稍微多一点的权重。由于垃圾性是一个罕见的主题,只在13%的时间发生,通过上述归一化或者丢弃特征值的做法,有关它的主题将被赋予更大的权重。通过采用这种方法,生成的主题与细微的特性更相关。

LSA和SVD的增强

SVD在语义分析和降维方面的成功让人们对其进行了扩展和增强,主要就是针对非NLP问题。它们有时与基于NLP内容的推荐引擎一起用于基于行为的推荐引擎。它们被用于自然语言词性统计。任何矩阵分解或降维方法都可以用于自然语言的词项频率。因此,在语义分析流水线中也可以找到下列方法的用途:

  • 二次判别分析(QDA)
  • 随机投影
  • 非负矩阵分解(NMF)

QDA是LDA的一种替代方法。QDA创建的是二次多项式变换,而不是线性变换。这些变换定义了一个可以用于区分类的向量空间。QDA向量空间中的类之间的边界是二次曲面,就像碗、球、半管一样。

随机投影是一种与SVD类似的矩阵分解和变换方法,但其算法是随机的,因此每次运行得到的结果都不一样。但是这种随机性使它更容易在并行机器上运行。在某些情况下(对于某些随机运行),可以得到比从SVD和LSA得到的更好的变换。然而,随机投影很少用于NLP问题。

大多数情况下,最好坚持使用LSA,它在底层使用了经过验证的SVD算法。

相关推荐
黑色叉腰丶大魔王几秒前
《自然语言处理 Transformer 模型详解》
人工智能·自然语言处理·transformer
Ajiang28247353041 小时前
贪吃蛇项目实现(C语言)——附源码
c语言·开发语言
guicai_guojia1 小时前
面试题篇: 跨域问题如何处理(Java和Nginx处理方式)
java·开发语言·nginx
鼠鼠龙年发大财2 小时前
fly专享
开发语言·php
hunandede2 小时前
直播相关02-录制麦克风声音,QT 信号与槽,自定义信号和槽
开发语言·qt
lzb_kkk2 小时前
【Redis】redis5种数据类型(哈希)
开发语言·redis·算法·缓存·哈希算法
ersaijun3 小时前
【Obsidian】当笔记接入AI,Copilot插件推荐
人工智能·笔记·copilot
易雪寒3 小时前
Maven从入门到精通(三)
java·python·maven
FreakStudio3 小时前
全网最适合入门的面向对象编程教程:49 Python函数方法与接口-函数与方法的区别和lamda匿名函数
python·嵌入式·面向对象·电子diy
Good_tea_h3 小时前
如何实现Java中的多态性
java·开发语言·python