过去的NLP实验人员发现了一种揭示词组合的意义的算法,该算法通过计算向量来表示上述词组合的意义。它被称为隐语义模型(latent semantic analysis,LSA)。当使用该工具时,我们不仅可以把词的意义表示为向量,还可以用向量来表示整篇文档的意义。
在本章中,我们将学习这些语义或主题向量。我们将使用TF
TF-IDF向量会对文档中词项的准确拼写形式进行计数。因此,如果表达相同含义的文本使用词的不同拼写形式或使用不同的词,将会得到完全不同的TF-IDF向量表示。
在之前的学习中,我们对词尾进行了归一化处理,使那些仅仅最后几个字符不同的词被归并到同一个词条。我们使用了归一化方法(如词干还原和词形归并)来创建拼写相似、含义通常也相似的小型的词集合。我们用这些词集合的词元或词干来标记这些小型的词集合,然后处理这些新的词条而不是原始词。
上述分析中,词形归并的方法将拼写相似的词放在一起,但是这些词的意义不一定相似。显然,它无法成功处理大多数同义词对,也无法将大多数同义词配对。同义词的区别通常不仅仅是词形归并和词干还原处理的词尾不同。更糟糕的是,词形归并和词干还原有时会错误地将反义词归并在一起。
上述词形归并造成的最终结果是,在我们得到的TF-IDF向量空间模型下,如果两段文本讨论的内容相同,但是使用了不同的词,那么它们在此空间不会”接近“。而有时,两个词形归并后的TF-IDF向量虽然相互接近,但在意义上根本不相似。
当我们对TF-IDF向量进行数学运算(加法、减法)时,这些和与差告诉我们的只是参与运算的向量表示的文档中词的使用频率。上述数学运算并没有告诉我们这些词背后的含义。通过将TF-IDF矩阵与自身相乘,可以计算词与词的TF-IDF向量(词共现或关联向量)。但是利用这些稀疏的高维向量进行”向量推理“效果并不好。这是因为当我们将这些向量相加或相减时,它们并不能很好地表示一个已有的概念、词或主题。
因此,我们需要一种方法来从词的统计数据中提取一些额外的信息,即意义信息。我们想用一个像TF-IDF一样的向量来表示意义,但是需要这个向量表示更紧凑、更有意义。
我们称这些紧凑的意义向量为”词-主题向量“(word-topic vector),称文档的意义向量为”文档-主题向量“(document-topic vector)。
处理完语料库后,语料库中的每篇文档将会对应一个文档-主题向量。而且,更重要的是,对于一个新文档或短语,我们不必重新处理整个语料库就可以计算得到其对应的新主题向量。词汇表中的每个词都会有一个主题向量,我们可以使用这些词-主题向量来计算词汇表中部分词构成的任何文档的主题向量。
我们需要找到属于用一个主题的那些词维度,然后对这些词维度的TF-IDF值求和,以创建一个新的数值来表示文档中该主题的权重。我们甚至可以对词维度进行加权以衡量它们对主题的重要度,以及我们所希望的每个词对这个组合(混合)的贡献度。我们也可以用负权重来表示词,从而降低文本与该主题相关的可能性。
假设我们在处理一些关于纽约(NYC)中央公园中宠物的句子。我们创建了3个主题:一个与宠物有关,称为”petness“;一个与动物有关,称为”animalness“;一个与城市有关,称为”cityness“。因此,”prtness“会给”cat“和”dog“这样的词打高分,但很可能忽略”NYC“和”apple“这样的词。接下来我们简单的对数据赋予一些权重:
import numpy as np
topic = {}
tfidf = dict(list(zip('cat dog apple lion NYC love'.split(), np.random.rand(6))))
topic['petness'] = (.3 * tfidf['cat'] + .3 * tfidf['dog'] + 0 * tfidf['apple'] + 0 * tfidf['lion'] - .2 * tfidf['NYC'] + .2 * tfidf['love'])
topic['animalness'] = (.1 * tfidf['cat'] + .1 * tfidf['dog'] - .1 * tfidf['apple'] + .5 * tfidf['lion'] + .1 * tfidf['NYC'] - .1 * tfidf['love'])
topic['cityness'] = (0 * tfidf['cat'] - .1 * tfidf['dog'] + .2 * tfidf['apple'] - .1 * tfidf['lion'] + .5 * tfidf['NYC'] + .1 * tfidf['love'])
在上述思想实验中,我们把可能表示每个主题的词频加起来,并根据词与主题关联的可能性对词频(TF-IDF值)加权,同样,对于那些可能在某种意义上与主题相反的词,我们也会做类似的事,只不过这次是减而不是加。
这里,我们只是很随意地选择将词和文档分解为3个主题。同时,我们这里的词汇量也极其有限,只有6各词。
一旦确定了3个要建模的主题,就必须确定这些主题中每个词的权重。主题建模转换是一个36的比例矩阵(权重),代表3个主题与6个词之间的关联。用这个矩阵乘以一个假想的61TF-IDF向量,就得到了该文档的一个31的主题向量。
在阅读上述向量时,大家可能已经意识到词和主题之间的关系可以翻转。3个主题向量组成的36矩阵可以转置,从而为词汇表中的每个词生成主题权重。
我们语料库中的文档可能会使用更多的词,但是这个特定的主题向量模型只会受到这6个词的用法的影响。我们可以将这种办法扩展到尽可能多的词,只要我们有足够的耐心(或算法)。只要模型还需要根据3个不同的维度或主题来区分文档,词汇表就可以像我们希望的那样不断增长。在上述思想实验中,我们将6维(TF-IDF归一化频率)压缩为3维(主题)。
20世纪的英国语言学家J.R.Firth研究了如何估计一个词或语素的含义。1957年,他给出了一条如何计算词主题的线索,他写道:可以通过词的上下文来理解它。
最直接的方法是计算词和上下文在同一文档中的共现次数。LSA是一种分析TF-IDF矩阵的算法,它将词分组到主题中。LSA也可以对词袋向量进行处理,但是TF-IDF向量给出的结果稍好。
LSA还对这些主题进行了优化,以保持主题维度的多样性。当使用这些新主题而不是原始词时,我们仍然可以捕获文档的大部分含义(语义)。该模型中用于捕获文档含义所需的主题数量远远少于TF-IDF向量词汇表中的词的数量。因此,LSA通常被认为是一种降维技术。LSA减少了捕获文档含义所需的维数。
有两种算法与LSA相似,它们也有相似的NLP应用:
LDA是最直接也是最快速的降维和分类模型之一,LDA分类器是一种有监督算法,因此需要对文档的类进行标注。
在本例中,我们给出了LDA的一个简单的实现版本,该实现无法在scikit-learn中找到。模型训练只有3个步骤,我们可以直接使用python来实现
import pandas as pd
from nlpia.data.loaders import get_data
pd.options.display.width = 120
sms = get_data('sms-spam')
print(sms)
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())
上述数据集中有4837条短消息,其中638条被标注为二类标签“spam”(垃圾类)
下面我们就对所有这些短消息进行分词,并将它们转换为TF-IDF向量:
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.tokenize.casual import casual_tokenize
tfidf_model = TfidfVectorizer(tokenizer=casual_tokenize)
tfidf_docs = tfidf_model.fit_transform(raw_documents=sms.text).toarray()
print(tfidf_docs.shape)
Out[1]:(4837, 9232)
经过casual_tokenize处理后的词汇表包含9232个词。词的数量几乎是短消息数的两倍,是垃圾短消息数的十倍。因此,模型不会有很多有关垃圾短消息指示词的信息。通常,当词汇表的规模远远大于数据集中标注的样本数量时,朴素贝叶斯分类器就不是很奏效,而这种情况下语义分析技术就可以提供帮助
下面先从最简单的语义分析技术LDA开始,我们可以在sklearn.discriminant_analysis.LinearDiscriminantAnalysis中使用LDA模型。但是,为了训练这个模型,只需要计算两个类(垃圾类和非垃圾类)的质心,因此我们可以直接这样做:
mask = sms.spam.astype(bool).values
# 因为TF-IDF向量是行向量,所以需要确保numpy使用axis=0独立计算每一列的平均值
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))
Out[1]:[0.06 0. 0. … 0. 0. 0. ]
Out[2]:[0.02 0.01 0. … 0. 0. 0. ]
现在可以用一个质心向量减去另一个质心向量从而得到分类线:
# 该点积计算的是每个向量在质心连线上的“阴影”投影
spamminess_score = tfidf_docs.dot(spam_centroid - ham_centroid)
print(spamminess_score.round(2))
Out[1]:[-0.01 -0.02 0.04 … -0.01 -0. 0. ]
这个原始的spamminess_score得分是非垃圾类质心到垃圾类质心的直线距离。我们用点积将每个TF-IDF向量投影到质心之间的连线上,从而计算出这个得分。
注意:点乘的几何意义是:是一条边向另一条边的投影乘以另一条边的长度。
LDA线性判别原理解析<数学推导>
在理想情况下,我们希望上述评分就像概率那样取值在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 > .5).astype(int)
print(sms['spam lda_predict lda_score'.split()].round(2).head())
Out[1]:
上面的结果看起来不错,当将阈值设置为50%时,前5条消息都被正确分类。我们接下来看看它在训练集其余部分的表现:
print((1 - (sms.spam - sms.lda_predict).abs().sum() / len(sms)).round(3))
Out[1]:0.977
这个简单的模型对97.7%的消息进行了正确分类。
这就是语义分析方法的威力,与朴素贝叶斯或对率回归(logistic regression)模型不同,语义分析并不依赖独立的词。语义分析会聚合语义相似的词(如spamminess)并将它们一起使用。
现在,我们已经准备好学习可以计算多维语义向量而不仅仅时一维语义得分的模型,到目前为止,一维向量“理解”的唯一事情就是词和文档的垃圾行,我们希望它能够学习更多的词上的细微差别,并提供一个多维向量来捕捉词的含义