【Python机器学习】NLP词频背后的含义——隐性狄利克雷分布(LDiA)

目录

LDiA思想

基于LDiA主题模型的短消息语义分析

LDiA+LDA=垃圾消息过滤器

更公平的对比:32个LDiA主题


对于大多数主题建模、语义搜索或基于内容的推荐引擎来说,LSA应该是首选方法。它的数学机理直观、有效,它会产生一个线性变换,可以应用于新来的自然语言文本而不需要训练过程,并几乎不会损失精确率。但是,在某些情况下,LDiA可以给出稍好的结果。

LDiA和LSA(已经底层的SVD)一样做了很多创建主题建模的工作,但是与LSA不同的是,LDiA假设词频满足狄利克雷分布。相对于LSA的线性数学,LDiA则更精确的给出了将词赋给主题的统计信息。

LDiA创建了一个语义向量空间模型,根据词在同一文档中的共现频率将它们分配给主题,然后,文档的主题混合可以由每个主题中的词的混合结果来确定,而这些词被分配给每个主题中。这使得LDiA主题模型更容易理解,因为分配给主题的词以及分配给文档的主题往往比LSA更有意义。

LDiA假设每篇文档都由某个任意数量的主题混合(线性组合)而成,该数量是在开始训练LDiA模型时选择的。LDiA还假设每个主题都可以用词的分布(词项频率)来表示。文档中每个主题的概率或权重,以及某个词被分配到一个主题的概率,都假定一开始满足狄利克雷概率分布(先验)。这就是该算法得名的来历。

LDiA思想

Blei和Ng通过类似思想实验提出了LDiA思想。他们设想,一台只能掷骰子(生成随机数字)的机器如何能写出语料库中的文档。由于我们仅基于词袋进行统计,因此在编写一篇真正的文档时,他们去掉了词序对文档语义的影响,他们只对词的混合统计数据进行建模,而这些词混合构成了每篇文档的词袋。

他们设想有一台机器,该机器只有两个选择项来开始生成特定文档的词混合结果。他们设想文档生成器会以某种概率分布来随机选择这些词,就像选择骰子的边数然后将骰子的组合情况加在一起创建一个D&D人物卡。我们的“人物卡”只需要骰子的两轮投掷过程。但是骰子本身很大,而且有好几个,关于如何组合它们来为不同的值生成所需的概率,有十分复杂的规则。我们希望词的数量和主题的数量有特定的概率分布,这样它们就可以匹配待分析的真实文档中的词和主题的分布。

骰子的两轮投掷过程分别代表:

  1. 生成文档的词的数量(泊松分布)
  2. 文档中混合的主题的数量(狄利克雷分布)

有了上面两个数值之后,就会遇到较难的部分,也就是要为文档选择词。设想的词袋生成机会在这些主题上迭代,并随机选择适合该主题的词,直到达到文档应该包含的词数量为止。确定这些词对应主题的概率(每个主题的词的适宜度)是比较困难的。但是一旦确定,“机器人”就会从一个词项-主题概率矩阵中查找每个主题的词的概率。

因此,这台机器只需要一个泊松分布的参数来告诉它文档的平均长度有多长,以及另外两个参数来定义设置主题数的狄利克雷分布。然后,文档生成算法需要文档喜欢使用的所有词和主题组成的词项-主题矩阵,并且,它还需要喜欢谈论的一个主题混合。

下面将文档生成问题转回最初的问题,即从现有文档中估算主题和词。我们需要为前两个步骤测算或计算关于词和主题的那些参数。然后需要从一组文档中计算出词项-主题矩阵,这就是LDiA所做的事情。

Blei和Ng意识到,他们可以通过分析语料库中文档的统计数据来确定步骤1和步骤2的参数。例如步骤1,可以计算出语料库中文档的所有词袋中的平均词(或n-gram)数量:

import pandas as pd
from nlpia.data.loaders import get_data
from nltk.tokenize import casual_tokenize
pd.options.display.width=120

sms=get_data('sms-spam')
total_corpus_len=0
for document_text in sms.text:
    total_corpus_len=total_corpus_len+len(casual_tokenize(document_text))
mean_document_len=total_corpus_len/len(sms)
print(round(mean_document_len,2))

还可以通过下面这行代码求解:

print(sum([len(casual_tokenize(t)) for t in sms.text])*1.0/len(sms.text))

大家应该直接从词袋来计算这个统计数据,我们需要确保正在对文档中的已分词和已向量化的词计数,并确保在对独立词项进行计数之前,已应用任一停用词过滤器或其他归一化方法。这样的话,我们的计数就不仅包括词袋向量词汇表中的所有词(正在计数的所有n-gram),而且包括词袋使用的那些词(如非停用词)。LDiA算法也依赖一个词袋向量空间模型。

设定LDiA模型所需要的第二个参数即主题的数量更加棘手。在一组特定的文档中,只有在为这些主题分配了词之后,才能直接得到主题的数量。就像KNN、k均值以及其他的聚类算法一样,我们必须提取设定k的值。我们可以猜测主题的数量(类似于k均值中的k,即簇的数量),然后检查这是否适用于这组文档。一旦设定好LDiA要寻找的主题数量,它就会找到要放入每个主题中的词的混合结果,从而优化其目标函数。

我们可以通过调整这个“超参数”(k,即主题的数量)来对其进行优化,直到它适合我们的应用为止。如果能够度量表示文档含义的LDiA语言模型的质量,就可以自动化上述优化过程。可以用于词优化的一个代价函数是,LDI模型在某些分类或回归问题(如情绪分析、文档关键词标注或主题分析)中的表现如何。我们只需要一些带标签的文档来测试主题模型或分类器。

基于LDiA主题模型的短消息语义分析

LDiA生成的主题对人类来说更容易理解和解释。这是因为经常一起出现的词被分配给相同的主题,而人类的期望也是如此。

这听起来好像是一回事,但事实并非如此。其背后的数学子啊优化不同的东西,优化器有不同的目标函数,因此它将达到一个不同的目标。为了让接近的高维空间向量在低维空间中继续保持接近,LDiA必须以非线性的方式变换(扭转和扭曲)空间(向量)。这种过程很难实现可视化,除非在某个三维空间上执行上述操作并将结果向量投影到二维空间。

以nlpia中的horse为例,我们可以为horse中的数千个点创建词-文档向量,方法是将它们转换为词x、y、z(即三维空间向量的维数)上的整数计数结果。然后,可以从这些计数生成人造文档,并将它们传递给LDiA和LSA示例。然后,我们就可以直接实现上述每一种方法产生马的不同的二维影子(投影)的过程的可视化。

下面来看对于一个包含数千条短消息的数据集(按照是否垃圾来标记)上述方法的应用过程。首先计算TF-IDF向量,然后为每个短消息计算一些主题向量。我们假设只使用16个主题来对垃圾短消息进行分类。保持主题(维度)的数量较低有助于减少过拟合的可能性。

LDiA使用原始词袋词频向量,而不是归一化的TF-IDF向量。这里有一个简单的方法在scikit-learn中计算词袋向量:

from sklearn.feature_extraction.text import CountVectorizer
from nltk.tokenize import casual_tokenize
import numpy as np
import pandas as pd
from nlpia.data.loaders import get_data

sms=get_data('sms-spam')
index=['sms{}{}'.format(i,'!'*j) for (i,j) in zip(range(len(sms)),sms.spam)]
sms.index=index

np.random.seed(42)
counter=CountVectorizer(tokenizer=casual_tokenize)
bow_docs=pd.DataFrame(counter.fit_transform(raw_documents=sms.text).toarray(),index=index)
column_nums,terms=zip(*sorted(zip(counter.vocabulary_.values(),counter.vocabulary_.keys())))
bow_docs.columns=terms

#检查一下,看这里的词频是否对标记为"sms0"的第一条短消息有意义
print(sms.loc['sms0'].text)
print(bow_docs.loc['sms0'][bow_docs.loc['sms0']>0].head())

下面给出了如何使用LDiA为短消息语料库创建主题向量的过程:

from sklearn.decomposition import LatentDirichletAllocation as LDiA
ldia=LDiA(n_components=16,learning_method='batch')
ldia=ldia.fit(bow_docs)
print(ldia.components_.shape)

因此,上述模型已经将9232个词(词项)分配给16个主题(成分)。下面看开头的几个词,了解一下它们是如何分配到16个主题的。LDiA是一种随机算法,它依赖随机数生成器做出一些统计决策来为主题分配词,所以每次运行sklearn.LatentDirichletAllocation,如果随机种子没有设定为固定值,都将获得不一样的结果:

pd.set_option('display.width',75)
components=pd.DataFrame(ldia.components_.T,index=terms,columns=columns)
print(components.round(2).head(3))

因此,感叹号(!)被分配到大多数主题中,但它其实是topic3的特别重要的部分,在该主题中,引号几乎不起作用。或许topic3关注情感的强度或强调,并不太在意数值或引用。

print(components.topic3.sort_calues(ascending=False)[:10])

可以看到,该主题的前10个词似乎是在要求某人做某事或支付某事的强调指令中可能使用的词类型。如果该主题更多使用在垃圾信息而不是非垃圾信息的话,那么这个发现很特别哦我们可以看到,即使这样粗略浏览一下,也可以对主题的词分配进行合理化解释或推理。

在拟合LDA分类器之前,需要为所有文档(短消息)计算出LDiA主题向量。下面看一下这些向量与SVD及PCA为相同文档生成的主题向量的不同:

ldia16_topic_vectors=ldia.transform(bow_docs)
ldia16_topic_vectors=pd.DataFrame(ldia16_topic_vectors,index=index,columns=columns)
print(ldia16_topic_vectors.round(2).head())

可以看到,上述主题之间分隔得更清晰,在为消息分配主题时,会出现很多0。在基于NLP流水线结果做出业务决策时,这是使LDiA主题更容易向同伴解释的做法之一。

LDiA+LDA=垃圾消息过滤器

下面观察这些LDiA主题在预测时的有效性,再次使用LDiA主题向量来训练LDA模型:

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.model_selection import train_test_split
X_train,X_text,y_train,y_test=train_test_split(ldia16_topic_vectors,sms.spam,test_size=0.5,random_state=271828)
lda=LDA(n_components=1)
lda=lda.fit(X_train,y_train)
sms['ldia16_spam']=lda.predict(ldia16_topic_vectors)
print(round(float(lda.score(X_text,y_test)),2))

train_test_split和LDiA的算法是随机的,所以如果不设置特定种子,每次运行会得到不同的结果和不同的精确率。

共线警告可能发生的一种情况是:如果文本包含一些2-gram或3-gram,其中组成他们的词只出现在这些2-gram或3-gram中。因此,最终的LDiA模型必须在这些相等的词项频率之间任意分配权重。当在短消息中导致共线性的词出现时,另一个词(它的配对)总是在相同的消息中。

我们可以使用Python而不是手工进行搜索。首先,我们可能只想在语料库中寻找任何相同的词袋向量。这些向量可能出现在不完全相同的短消息中,因为它们有相同的出现频率。我们可以遍历所有词袋对,以寻找相同的向量。这些向量肯定会在LDiA或LSA中引发共线警告。

如果没有找到任何词袋向量的精确副本,那么可能遍历词汇表中所有的词对。然后遍历所有的词袋,以寻找包含完全相同的词对的短消息。如果这些词在短消息中没有单独出现过,那么已经在数据集中找到了一个“共线”。一些常见的2-gram(比如英文人名)可能会导致这种情况,而且从来没有分开使用过,例如“Bill Gates”。

我们在测试集上获得的精确率超过90%,而且只需要在一半的可用数据上进行训练。但是,由于数据集有限,我们确实得到了关于特征共线的警告,这给LDA带来了一个待确定问题。一旦使用train_test_split丢弃了一半的文档,那么主题-文档矩阵的行列式就接近于零。如果需要的话,可以关闭LDiA n_components来解决这个问题,但是它往往会将这些主题组合在一起,而这些主题是彼此的线性组合(共线)。

但是,我们看一下这里的LDiA模型与基于TF-IDF向量的高维模型相比如何。TF-IDF向量有更多的特征(超过3000个独立的词项),所以很可能会遇到过拟合和弱泛化问题,这就是LDiA和PCA泛化的用武之地:

from nlpia.data.loaders import get_data

sms=get_data('sms-spam')
index=['sms{}{}'.format(i,'!'*j) for (i,j) in zip(range(len(sms)),sms.spam)]
sms.index=index


from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.tokenize.casual import casual_tokenize
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.model_selection import train_test_split
tfidf=TfidfVectorizer(tokenizer=casual_tokenize)
tfidf_docs=tfidf.fit_transform(raw_documents=sms.text).toarray()
tfidf_docs=tfidf_docs-tfidf_docs.mean(axis=0)
X_train,X_text,y_train,y_test=train_test_split(tfidf_docs,sms.spam.values,test_size=0.5,random_state=271828)
lda=LDA(n_components=1)
lda=lda.fit(X_train,y_train)
print(round(float(lda.score(X_train,y_train)),3))
print(round(float(lda.score(X_text,y_test)),3))

在训练集上基于TF-IDF的模型的精确率是完美的,但是,当使用低维主题向量而不是TF-IDF向量训练时,测试集上的精确率要低很多。

测试集的精确率是唯一重要的精确率。这正是主题建模(LSA)应该做的,它可以帮助我们从一个小型训练集中泛化出模型,因此它仍然可以很好地处理使用不同词组合的消息。

更公平的对比:32个LDiA主题

下面使用更多的维度和更多的主题。也许LDiA不如LSA(PCA)高效,所以它需要更多的主题来分配词,比如32个主题(成分):

from nlpia.data.loaders import get_data
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.tokenize.casual import casual_tokenize
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.model_selection import train_test_split
from sklearn.decomposition import LatentDirichletAllocation as LDiA
from sklearn.feature_extraction.text import CountVectorizer
from nltk.tokenize import casual_tokenize
import numpy as np
import pandas as pd

sms=get_data('sms-spam')
index=['sms{}{}'.format(i,'!'*j) for (i,j) in zip(range(len(sms)),sms.spam)]
sms.index=index

np.random.seed(42)
counter=CountVectorizer(tokenizer=casual_tokenize)
bow_docs=pd.DataFrame(counter.fit_transform(raw_documents=sms.text).toarray(),index=index)
column_nums,terms=zip(*sorted(zip(counter.vocabulary_.values(),counter.vocabulary_.keys())))
bow_docs.columns=terms

ldia32=LDiA(n_components=32,learning_method='batch')
ldia32=ldia32.fit(bow_docs)
print(ldia32.components_.shape)

下面计算所有文档(短消息)的新的32维主题向量:

ldia32_topic_vectors=ldia32.transform(bow_docs)
columns32=['topic{}'.format(i) for i in range(ldia32.n_components)]
ldia32_topic_vectors=pd.DataFrame(ldia32_topic_vectors,index=index,columns=columns32)
print(ldia32_topic_vectors.round(2).head())

我们可以看到在,这些主题更加稀疏,而且能够更加清晰的分开。

下面是LDA模型(分类器)的训练过程,这次使用32维的LDiA主题向量:

X_train,X_text,y_train,y_test=train_test_split(ldia32_topic_vectors,sms.spam.values,test_size=0.5,random_state=271828)
lda=LDA(n_components=1)
lda=lda.fit(X_train,y_train)
sms['ldia32_spam']=lda.predict(ldia32_topic_vectors)
print(X_train.shape)
print(round(float(lda.score(X_train,y_train)),3))
print(round(float(lda.score(X_text,y_test)),3))

不要将这里“主题”或成分数量的优化与前面的共线性问题混淆。增加或减少主体的数量并不能解决或造成共线问题。这是底层数据造成的问题。如果想要摆脱这个警告,那么需要将“噪声”或元数据以人造词的方式添加到短消息中,或者需要删除那些重复的词向量。如果文档中有重复出现多次的词向量或词对,那么主题的数量优化也无法解决这个问题。

主题的数量越多,那么主题的精确率就可以越高,至少对这个数据集来货,产品这一主题线性分隔得更好。但是这里的效果仍然不如PCA+LDA的96%的精确率。因此,PCA能使这里的短消息主题向量更有效的展开,这样就允许时候用超平面以更大的消息间隔来分隔类。

  • 16
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值