目录
TF-IDF向量(词项频率—逆文档频率向量)可以帮助我们估算词在文本块中的重要度,我们使用TF-IDF向量和矩阵可以表明每个词对于文档集合中的一小段文本总体含义的重要度。这些TF-IDF“重要度”评分不仅适用于词,还适用于多个词构成的短序列(n-gram)。如果知道要查找的确分词或n-gram,这些n-gram的重要度评分对于搜索文本非常有用。
有一种揭示词组合的意义的算法,通过计算向量来表示上述词组合的意义,它被称为隐性语义分析(LSA)。当使用该工具时,我们不仅可以把词的意义表示为向量,还可以用向量来表示整篇文档的意义。
TF-IDF向量及词形归并
TF-IDF向量会对文档中词项的准确拼写形式进行计数,因此,如果表达相同含义的文本使用词的不同拼写形式或使用不同的词,将会得到完全不同的TF-IDF向量表示。这会使依赖词条计数的搜索引擎和文档相似性的比较变得乱七八糟。
我们可以对词尾进行归一化处理,使那些仅仅最后几个字符不同的词被归并到同一个词条。我们使用归一化方法(如词干还原和词形归并)来创建拼写相似、含义通常也相似的小型的词集合。我们用这些词集合的词元或词干来标记这些小型的词集合,然后处理这些新的词条而不是原始词。
上述方法中,词形归并的方法将拼写相似的词放在一起,但是这些词的意义不一定相似。显然它无法成功处理大多数同义词对,也无法将大多数同义词配对。同义词的区别通常不仅仅是词形归并和词干还原的词尾不同,更糟糕的是,词形归并和词干还原有时还会错误地将反义词归并在一起。
上述词形归并最终造成的结果是:在我们得到的TF-IDF向量空间模型下,如果两段文本讨论的内容相同,但是使用了不同的词,那么它们在此空间中不会“接近”。而有时,两个词形归并后的TF-IDF向量虽然相互接近,但在意义上根本不相似。即使是TF-IDF相似度评分方法,如Okapi BM25或余弦相似度,也无法连接这些同义词或分开这些反义词。具有不同拼写形式的同义词所产生的TF-IDF向量在向量空间中彼此并不接近。
主题向量
当我们对TF-IDF向量进行数学运算(如加减法)时,这些和与差告诉我们的知识参与运算的向量表示的文档中词的使用频率。上述数学运算并没有告诉我们这些词背后的含义。通过将TF-IDF矩阵与其自身相乘,可以计算词与词的TF-IDF向量(词共现或关联向量)。但是利用这些稀疏的高维向量进行“向量推理”效果并不好。这是因为当我们将这些向量相加或相减时,它们并不能很好地表示一个已有的概念、词或主题。
因此,我们需要一种方法来从词的统计数据中提取一些额外的信息,即意义信息。我们想用一个像TF-IDF一样的向量来表示意义,但是需要这个向量表示更紧凑、更有意义。
我们称这些紧凑的意义向量为“词-主题向量”,称文档的意义向量为“文档-主题向量”。上述两种向量都可以称为“主题向量”。
这些主题向量可以很紧凑,也可以像我们想要的那样高维。LSA主题向量可以少到只有一维,也可以多到有数千维。
我们可以像对其他向量一样对主题向量进行加减运算,只是这里得到的向量和与向量差与TF-IDF向量相比,意味着更多的东西。同时主题向量之间的距离对于文档搜索或语义搜素之类的任务很有用。相比于使用关键词和TF-IDF向量进行聚类和搜索,现在可以使用语义和意义来进行聚类搜索了。
处理完语料库之后,语料库中的每篇文档将会对应一个文档-主题向量。而且更重要的是,对于一个新文档或短语,可以不重新处理整个语料库就可以计算得到其对应的新主题向量。词汇表中的每个词都会有一个主题向量,我们可以使用这些词-主题向量来计算词汇表中部分词构成的任何文档的主题向量。
词库(词汇表)中的每个词都有一个词-主题向量。因此我们可以计算任何新文档的主题向量,只需将其所有词的主题向量相加即可。
对词和句子的语义(含义)进行数值化表示可能比较棘手。对于像应用这样的“模糊性”语言更是如此,因为它包含多种方言,并且对于同一个词有许多不同的解释:
- 一词多义:词和短语包含不止一种含义;
- 同音异义:词的拼写和发音相同,但含义不同;
- 轭式搭配:在同一句子中同时使用同一词的两种含义;
- 同形异义:词的拼写相同,但发音不同、含义不同;
- 同音异形:词的发音相同,但是拼写不同,含义不同(这是语音交互NLP面对的一个挑战)
类似下面的例子,如果没有LSA之类的工具,将非常难处理:
she felt...less.She felt tamped down.Dim.More faint.Feint.Feigned.fain.
我们需要找到属于同一个主题的那些词维度,然后对这些词维度的TF-IDF值求和,以创建一个新的数值来表示文档中该文档的权重。我们甚至可以对词维度进行加权以权衡它们对主题的重要度,以及我们所希望的每个词对这个组合的贡献度。我们也可以用负权重来表示这些词,从而减低文本与该主题相关的可能性。
一个思想实验
假设有一篇特定文档的TF-IDF向量,我们想将其转换为主题向量。我们可以设想一下每个词对文档的主题的贡献度有多大。
以下面这个例子:
cat dog apple lion NYC love.
我们创建3个主题:一个与宠物有关,一个与动物有关,一个则与城市有关。我们可以把主题分别称为“petness”、“animalness”、“cityness”。因此,“petness”主题会给“cat”和“dog”这样的词打高分,但很可能会忽略“NYC”这样的词。而“cityness”则会忽略“cat”和“dog”这样的词。
如果像上面这样“训练”主题模型,不用计算机,而只用常识,可能会得到下面这样的权重结果:
import numpy as np
topic={}
tfidf=dict(list(zip("cat dog apple lion NYC love".split(),np.random.rand(6))))
topic['petness']=(0.3*tfidf['cat']+
0.3*tfidf['dog']+
0*tfidf['apple']+
0*tfidf['lion']+
0.2*tfidf['NYC']+
0.2*tfidf['love'])
topic['animalness']=(0.1*tfidf['cat']+
0.1*tfidf['dog']+
0.1*tfidf['apple']+
0.5*tfidf['lion']+
0.1*tfidf['NYC']+
0.1*tfidf['love'])
topic['cityness']=(0*tfidf['cat']+
0.1*tfidf['dog']+
0.2*tfidf['apple']+
0.1*tfidf['lion']+
0.5*tfidf['NYC']+
0.1*tfidf['love'])
print(topic)
在上述思想实验中,我们把可能表示每个主题的词频加起来,并根据词与主题关联的可能性对词频(TF-IDF值)加权。同样,对于那些可能在某种意义上与主题相反的词,我们也会做类似的事,只不过这次是减而不是加。这并不是真实算法流程或示例的真正实现,而只是一个思想实验而已。我们芝士想弄明白如何教会机器像人类一样思考。这里,我们只是很随意地选择将词和文档分级为3个主题。同时,我们这里的词汇量也极其有限,只有6个词。
一旦确定了3个要建模的主题,就必须确定这些主题中每个词的权重。我们按照比例混合词,使主题也像颜色混合一样。主题建模转换(颜色混合配方)是一个3*6的比例矩阵(权重),代表3个主题与6个词之间的关联。用这个矩阵乘以一个假想的6*1 TF-IDF向量,就得到了该文档的一个3*1的主题向量。
上面我们给出了一个判断,即“cat”和“dog”两个词应该对“petness”主题有相似的贡献度(0.3)。因此,TF-IDF主题转换矩阵左上角的两个值都是0.3。计算机可以读取、切分文档并对切分词条进行计数,还有TF-IDF向量来表示任意多的文档。
我们确定“NYC”这个词项应该对“petness”主题有负向的权重。从某种意义,城市名称、缩写词等与有关宠物的词几乎没有交集。
我们还给“love”这个词赋予了一个对“petness”主题的正向的权重,这可能是因为我们经常把“love”和有关宠物的词放在同一个句子中。需要注意的是,上面我们也将少许比例的词“apple”放入“cityness”主题向量中,这可能是人工设定,因为“NYC”和“Big Apple”是同义词。理想情况下,我们的语义分析算法可以根据“apple”和“NYC”在相同文档中的出现频率来计算“apple”和“NYC”之间的同义性。
词和主题之间的关系是可以翻转的。3个主题向量组成的3*6矩阵可以转置,从而为词汇表中的每个词生成主题权重。这些权重向量就是6个词的词向量:
word_vector={}
word_vector['cat']=0.3*topic['petness']+0.1*topic['animalness']+0*topic['cityness']
word_vector['dog']=0.3*topic['petness']+0.1*topic['animalness']+0.1*topic['cityness']
word_vector['apple']=0*topic['petness']+0.1*topic['animalness']+0.2*topic['cityness']
word_vector['lion']=0*topic['petness']+0.5*topic['animalness']+0.1*topic['cityness']
word_vector['NYC']=-0.2*topic['petness']+0.1*topic['animalness']+0.5*topic['cityness']
word_vector['love']=0.2*topic['petness']+0.1*topic['animalness']+0.1*topic['cityness']
print(word_vector)
6个主题向量如下图示,每个词对应一个主题向量,以三维向量的形式表示6个词的意义:
之前,每个主题用向量来表示,而每个向量中给出了每个词的权重,我们在3个主题中使用六维向量来表示词的线性组合结果。在上述思想实验中,我们为单篇自然语言文档手工建立了一个三主题模型。如果只是计算这6个词的出现次数,并将它们乘以权重,就会得到任何文档的三维主题向量。三维向量很有意思,因为它们很容易实现可视化。三维向量(或任何低维向量空间)对于机器学习分类问题也很有用。分类算法可以用平面/超平面分割向量空间,从而将向量空间划分为类别。
上面主观的、人力耗费型的语义分析方法依赖人们的直觉和常识来将文档分解为主题。但是常识很难编码到算法中,因此上述方法是不可重现的,我们可能会得到和前面不一样的权重,这并不适合机器学习流水线。另外,上述做法也不能很好地扩展到更多的主题和词。人类无法将足够多的词分配给足够多的主题,从而精确捕获需要机器学习处理的任何不同类型语料库中的文档的含义。
每一个加权和其实都是一个点积,3个点积就是一个矩阵乘法,或者说内积。将一个3*n的权重矩阵与TF-IDF向量相乘(文档中每个词对应一个值),其中n就是词汇中的词项数。这个乘法的输出是表示该文档的一个新的3*1主题向量。我们所做的就是将一个向量从一个向量空间(TF-IDF)转换到另一个低维向量空间(主题向量)。我们的算法应该创建一个n个词项乘以m个主题的矩阵,我们可以将其乘以文档的词频向量,从而得到该文档的新主题向量。
一个主题评分算法
我们仍然需要一种算法来确定上面提到的主题向量,我们也需要将TF-IDF向量转换为主题向量。机器无法分辨哪些词应该属于同一组,或者它们当中的任何一个表示什么含义。
我们可以通过上下文来理解它们,
表达词的上下文,最直接的方法是计算词和上下文在同一文档中的共现次数。词袋(BOW)和TF-IDF向量可以满足这一需求。这种计算共现次数的方法导致了多个算法的出现,这些算法通过创建向量来表示文档或句子中词使用的统计信息。
LSA(隐性语义分析)是一种分析TF-IDF矩阵(TF-IDF向量构成的表格)的算法,它将词分组到主题中。LSA也可以对词袋向量进行处理,但是TF-IDF向量给出的结果稍好一些。
LSA还对这些主题进行了优化,以保持主题维度的多样性。当使用这些新主题而不是原始词时,我们仍然可以捕获文档的大部分含义(语义)。该模型中用于捕获文档含义所需的主题数量远远小于TF-IDF向量词汇表中的词的数量。因此,LSA通常被认为是一种降维技术。LSA减少了捕获文档含义所需的维数。
主成分分析(PCA)也是一种降维技术,它和LSA的数学计算方法是一样的。然而,当减少图像和其他数值表格而不是词袋向量或TF-IDF的维数时,我们就说这是PCA。使用PCA也可以对词进行语义分析。这时给这个特定的应用起了一个它自己的名字LSA。
还有两种算法与LSA相似,它们也有相似的NLP应用,它们分别是:
- 线性判别分析(LDA)
- 隐性狄利克雷分布(LDiA)
LDA将文档分解到单个主题中,而LDiA则更像LSA,因为它可以将文档分解到任意多个主题中。
下面是一个用于主题分析的简单LDA方法的示例:
一个LDA分类器
LDA是最直接也最快速的降维和分类模型之一。在很多应用中,它具有很高的精确率。LDA分类器是一种有监督算法,因此需要对文档的类进行标注。但是LDA所需要的训练样本可以比较少。
下面例子中,是一个LDA的一个简单的实现版本。模型训练只有3个步骤:
- 计算某个类(如垃圾短消息类)中所有TF-IDF向量的平均位置(质心);
- 计算不在该类(如非垃圾短消息类)中的所有TF-IDF的平均位置(质心);
- 计算上述两个质心之间的向量差(即链接这两个向量的直线)。
要“训练”LDA模型,只需找到两个类的质心之间的向量(直线)。LDA是一种有监督算法,因此需要为消息添加标签。要利用该模型进行推理或预测,只需要判断新的TF-IDF是否更接近类内(垃圾类)而不是类外(非垃圾类)的质心。首先,我们来训练一个LDA模型,将短消息分为垃圾类或非垃圾类:
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=pd.DataFrame(sms.values,columns=sms.columns,index=index)
sms['spam']=sms.spam.astype(int)
print(len(sms))
print(sms.spam.sum())
print(sms.head(6))
因此,上述数据集中有4387条短消息,其中638条被标注为spam(垃圾类)。
下面我们就对所有的这些短消息进行分词,并将它们转换为TF-IDF向量:
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.tokenize import casual_tokenize
tfidf_model=TfidfVectorizer(tokenizer=casual_tokenize)
tfidf_docs=tfidf_model.fit_transform(raw_documents=sms.text).toarray()
print(tfidf_docs.shape)
print(sms.spam.sum())
nltk.casual_tokenizer处理后的词汇表中包含9232个词。词的数量几乎是短消息数的两倍,是垃圾短消息数的是被。因此,模型不会有很多有关垃圾短消息指示词的信息。通常,当词汇表的规模远远大于数据集中标注的样本数量时,朴素贝叶斯分类器就不是很奏效,而这种情况下语义分析技术就可以提供帮助。
下面是最简单的语义分析技术LDA,我们可以在sklearn.discriminant_analysis.LinearDiscriminantAnalysis中使用LDA模型。但是,为了训练这个模型,只需要计算两个类(垃圾类和非垃圾类)的质心,因此我们可以直接这样做:
mask=sms.spam.astype(bool).values
spam_centroid=tfidf_docs[mask].mean(axis=0)
ham_centroid=tfidf_docs[~mask].mean(axis=0)
print(spam_centroid.round(2))
print(ham_centroid.round(2))
现在可以用一个质心向量减去另一个质心向量从而得到分类线:
spamminess_score=tfidf_docs.dot(spam_centroid-ham_centroid)
print(spamminess_score.round(2))
这个原始的spamminess_score得分是非垃圾类质心到垃圾类质心的直线距离。我们用点积将每个TF-IDF向量投影到质心之间的连线上,从而计算出这个得分。在一个“向量化”的numpy运算中,我们同时完成了4387次点积计算。与Python循环相比,这可以将处理速度提高100倍。
下图是三维TF-IDF向量的视图,同时给出了短消息数据集类的质心所在位置。
图中从非垃圾类质心到垃圾类质心的箭头就是模型训练得到的直线,该直线定义了模型。我们可以看到点的分布情况,当将它们投影到质心的连线上时,我们可能会得到一个负的垃圾信息得分。
在理想情况下,我们希望上述平分就像概率那样取值在0到1之间,sklearnMinMaxScaler可以做到这一点:
from sklearn.preprocessing import MinMaxScaler
sms['lda_score']=MinMaxScaler().fit_transform(spamminess_score.reshape(-1,1))
sms['lda_predict']=(sms.lda_score>0.5).astype(int)
print(sms['spam lda_predict lda_score'.split()].round(2).head(6))
上述结果看起来不错,当将阈值设置为50%时,前6条消息都被正确分类。我们接下来看它在训练集其他数据上的表现:
print((1.0-(sms.spam-sms.lda_predict).abs().sum()/len(sms)).round(3))
这个简单的模型对97.7%的消息进行了正确分类。对于这里得到如此高精度结果的原因在于:我们用于测试的问题,分类器实际已经在训练过程中“见过”。但是,LDA是一个非常简单的模型,参数很少,所以它应该可以很好地泛化,只要这里的短消息能够代表将要分类的消息即可。
这就是语义分析方法的威力。与朴素贝叶斯或对率回归模型不同,语义分析并不依赖独立的词。语义分析会聚合语义详细的词并将它们一起使用。但是,这个训练集只包含有限的词汇表和一些非英语词。因此,如果希望正确分类,测试消息也需要使用相似的词。
下面看训练集上的混淆矩阵的样子。它给出了标注为垃圾但是根本不是垃圾的消息(假阳),也给出了标注为非垃圾但是本应该标注为垃圾的消息(假阴):
from pugnlp.stats import Confusion
Confusion(sms['spam lda_predict'.split()])
上面的结果看起来不错。如果假阳和假阴失衡,我们可以调整0.5这个阈值。
LDiA
LDiA代表隐性狄利克雷分布,它还可以用来生成捕捉词或文档语义的向量。LDiA和LSA在数学书并不一样,它使用非线性统计算法将词分组。因此它通常会比线性方法(如LSA)的训练时间长很多。这常常使LDiA在许多实际应用中并不使用,而且它基本不会是我们要尝试的第一种方法。尽管如此,它所创建的主题的统计数据有时更接近于人类对词和主题的直觉,所以LDiA主题通常更易于向上级解释。
此外,LDiA对于一些单文档问题有用,如文档摘要。这时,语料库就是单篇文档,文档的句子就是该“语料库”中的一篇篇“文档”。这就是gensim和其他软件使用LDiA来识别文档中最核心句子的做法。然后这些句子可以穿在一起形成一篇由机器生成的摘要。
对于大多数分类或回归问题,通常最好使用LSA。