手把手教你搭建NLP经典模型

上一篇我们讲到了最简单的词向量表示方法——共现矩阵(没有看的朋友可以点击这里小白跟学系列之手把手搭建NLP经典模型(含代码)回顾一下!)
共现矩阵简单是简单,但是有很严重的问题 。
作者强调,自己动手的经验、花时间思考的经验,都是无法复制的 。(所以,听话,要自己尝试敲1?敲代码噢!
手把手教你搭建NLP经典模型
文章插图
目录
共现矩阵存在的问题:无意义词干扰
余弦相似度:表示两个词向量的相似度
基于计数统计的方法改进
PMI矩阵(排除无意义词干扰)
PPMI矩阵(排除负数)
SVD降维(解决维度爆炸和矩阵稀疏)
总结(用计数统计的方法表示词向量的步骤)
共现矩阵存在的问题
!很多常用的无意义词(比如“thecar”)在文中出现次数太多的话,共现矩阵会认为“the”和“car”强相关,这是不合理的!
那怎么表示两个向量之间的相似度呢?
余弦相似度
设有x=(x1,x2,x3,。..,xn)和y=(y1,y2,y3,。..,yn)两个向量,它们之间的余弦相似度如下式所示 。
手把手教你搭建NLP经典模型
文章插图
分子为内积,分母为L2范数 。(范数表示向量的大小,L2范数即向量各个元素的平方和的平方根 。)
式(2.1)的要点是先对向量进行正规化,再求它们的内积 。
余弦相似度:两个向量在多大程度上指向同一方向 。也就是说,余弦相似度越靠近1,两个词越相似;余弦相似度越靠近0,两个词越没什么关系;
现在,我们来代码实现余弦相似度 。
defcos_similarity(x,y):#x和y是NumPy数组nx=x/np.sqrt(np.sum(x**2))#x的正规化ny=y/np.sqrt(np.sum(y**2))#y的正规化returnnp.dot(nx,ny)
为了防止除数为0(比如0向量),所以要给分母加个微小值eps=10^-8
修改后的余弦相似度的实现如下所示(common/util.py) 。
defcos_similarity(x,y,eps=1e-8):nx=x/(np.sqrt(np.sum(x**2))+eps)ny=y/(np.sqrt(np.sum(y**2))+eps)returnnp.dot(nx,ny)
在绝大多数情况下,加上eps不会对最终的计算结果造成影响,因为根据浮点数的舍入误差,这个微小值会被向量的范数“吸收”掉 。而当向量的范数为0时,这个微小值可以防止“除数为0”的错误 。
利用这个cos_similarity函数,可以求得单词向量间的相似度 。我们尝试求you和I的相似度(ch02/similarity.py) 。
importsyssys.path.append(‘ 。.’)fromcommon.utilimportpreprocess,create_co_matrix,cos_similarity#引入文本预处理,创建共现矩阵和计算余弦相似度的函数text=‘YousaygoodbyeandIsayhello.’corpus,word_to_id,id_to_word=preprocess(text)#文本预处理vocab_size=len(word_to_id)C=create_co_matrix(corpus,vocab_size)#创建共现矩阵c0=C[word_to_id[‘you’]]#you的词向量c1=C[word_to_id[‘i’]]#i的词向量print(cos_similarity(c0,c1))#计算余弦相似度#0.7071067691154799
从上面的结果可知,you和i的余弦相似度是0.70 。. 。,接近1,即存在相似性 。
【手把手教你搭建NLP经典模型】说完单词向量之间的相似度可以余弦相似度表示,用共现矩阵的元素表示两个单词同时出现的次数 。而很多常用的无意义词(比如“thecar”)在文中出现次数太多的话,共现矩阵会认为“the”和“car”强相关,这是不合理的!
所以共现矩阵中无意义词的干扰怎么解决呢?
基于计数的方法改进
接下来将对共现矩阵进行改进,并使用更实用的语料库,获得单词“真实的”分布式表示 。
点互信息
引入点互信息(PointwiseMutualInformation,PMI)这一指标 。即考虑单词单独出现的次数 。无意义词(“the”)单独出现次数肯定多,这点要考虑进去 。
对于随机变量x和y,它们的PMI定义如下:
手把手教你搭建NLP经典模型
文章插图
P(x)表示x发生的概率,
P(y)表示y发生的概率,
P(x,y)表示x和y同时发生的概率 。
PMI的值越高,表明相关性越强 。
例如设X=“the”,Y=“car”
P(“the”)=“the”出现的次数
P(“car”)=“car”出现的次数
P(“thecar”)=“thecar”共同出现的次数
“the”单独出现的次数多,所以P(“the”)分母也就大,也就抵消掉了the的作用啦 。
怎么表示概率呢?简单的方式就是用共现矩阵来表示概率,因此也能表示出PMI 。也就是用单词出现的次数表示概率 。
手把手教你搭建NLP经典模型
文章插图
N:语料单词总数;
C(X):X出现的次数;
C(X,Y):X,Y共现的次数;
举个栗子:
这里假设有一个文本语料库 。单词总数量(N)为10000,the出现100次,car出现20次,drive出现10次,the和car共现10次,car和drive共现5次 。这时,如果从共现次数的角度来看,the和car的相关性更强 。而如果从PMI的角度来看,结果是怎样的呢?
手把手教你搭建NLP经典模型
文章插图
结果表明,在使用PMI的情况下,drive和car具有更强的相关性 。这是我们想要的结果 。之所以出现这个结果,是因为我们考虑了单词单独出现的次数 。因为the本身出现得多,所以PMI的得分被拉低了 。
但也存在一个问题 。
当两个单词的共现次数为0时,log20=?∞ 。为了解决这个问题,实际上我们会使用正的点互信息(PositivePMI,PPMI) 。
解决办法
用PPMI(PositivePMI,正的点互信息)来表示词之间的相关性
正的点互信息
PPMI(x,y)=max(0,PMI(x,y)),即当PMI为负数的时候,视作0 。
所以,PPMI构建的矩阵要优于共现矩阵(因为排除了像the等无意义词的干扰呀) 。所以PPMI是更好的词向量 。
实现将共现矩阵转化为PPMI矩阵的函数为ppmi(C,verbose=False,eps=1e-8) 。这里不再赘述,我们只需要调用,需要看源代码的去(common/util.py)
如何调用使用它呢?可以像下面这样进行实现(ch02/ppmi.py) 。
importsyssys.path.append(‘ 。.’)importnumpyasnpfromcommon.utilimportpreprocess,create_co_matrix,cos_similarity,ppmitext=‘YousaygoodbyeandIsayhello.’corpus,word_to_id,id_to_word=preprocess(text)#文本预处理vocab_size=len(word_to_id)C=create_co_matrix(corpus,vocab_size)#创建共现矩阵W=ppmi(C)#将共现矩阵——》PPMI矩阵np.set_printoptions(precision=3)#设置有效位数为3位print(‘covariancematrix’)print(C)print(‘-’*50)print(‘PPMI’)print(W)
运行该文件,可以得到:
covariancematrix[[0100000][1010110][0101000][0010100][0101000][0100001][0000010]]--------------------------------------------------PPMI[[0.1.8070.0.0.0.0.][1.8070.0.8070.0.8070.8070.][0.0.8070.1.8070.0.0.][0.0.1.8070.1.8070.0.][0.0.8070.1.8070.0.0.][0.0.8070.0.0.0.2.807][0.0.0.0.0.2.8070.]]
这样一来,我们就成功的将共现矩阵转化为了PPMI矩阵啦,也获取了一个更好的单词向量!
但是PPMI矩阵也存在很明显的问题
维度爆炸
矩阵稀疏
如果语料库的词汇量达到10万,则词向量的维数也同样达到10万 。处理10万维向量是不现实的 。
另外,我们可以看得出该矩阵很多元素都是0 。这表明向量中的绝大多数元素并不重要,也就是说,每个元素拥有的“重要性”很低 。这样的向量也容易受到噪声影响,稳健性差 。
对于这些问题,一个常见的方法是向量降维 。
解决办法:降维
降维:减少向量维度(尽量保留重要信息) 。发现重要的轴/分布广的轴,将二维数据变一维数据 。
目的:从稀疏矩阵中找到重要的轴,用更少的维度去表示词向量 。
手把手教你搭建NLP经典模型
文章插图
降维的方法有很多,这里我们使用奇异值分解(SingularValueDecomposition,SVD),如下式所示:
手把手教你搭建NLP经典模型
文章插图
SVD将任意的矩阵X分解为U、S、V这3个矩阵的乘积,其中U和V是列向量彼此正交的正交矩阵,S是除了对角线元素以外其余元素均为0的对角矩阵 。
手把手教你搭建NLP经典模型
文章插图
在式(2.7)中,U是正交矩阵 。这个正交矩阵构成了一些空间中的基轴(基向量),我们可以将矩阵U作为“单词空间” 。
S是对角矩阵,奇异值在对角线上降序排列 。
简单地说,我们可以将奇异值视为“对应的基轴”的重要性 。这样一来,如图2-10所示,减少非重要元素就成为可能 。
手把手教你搭建NLP经典模型
文章插图
如图2-10所示,矩阵S的奇异值小,对应的基轴的重要性低,因此,可以通过去除矩阵U中的多余的列向量来近似原始矩阵 。用我们正在处理的“单词的PPMI矩阵”来说明的话,矩阵X的各行包含对应的单词ID的单词向量,这些单词向量使用降维后的矩阵U‘表示 。
想从数学角度仔细理解SVD的读者,请参考文献[20]等 。
接下来将用代码实现SVD,这里可以使用NumPy的linalg模块中的svd方法 。linalg是linearalgebra(线性代数)的简称 。下面,我们创建一个共现矩阵,将其转化为PPMI矩阵,然后对其进行SVD降维(h02/count_method_small.py) 。
importsyssys.path.append(’ 。.‘)importnumpyasnpimportmatplotlib.pyplotaspltfromcommon.utilimportpreprocess,create_co_matrix,ppmitext=’YousaygoodbyeandIsayhello.‘corpus,word_to_id,id_to_word=preprocess(text)vocab_size=len(id_to_word)C=create_co_matrix(corpus,vocab_size,window_size=1)W=ppmi(C)#SVDU,S,V=np.linalg.svd(W)#变量U已成为密集向量
SVD执行完毕 。上面的变量U包含经过SVD转化的密集向量表示(稀疏的反义词,就是没那么多0啦) 。现在,我们来看一下它的内容 。单词ID为0的单词向量you如下 。
print(C[0])#共现矩阵(简单的用次数来表示)#[0100000]print(W[0])#PPMI矩阵(用PPMI指标(概率)表示)#[0.1.8070.0.0.0.0.]print(U[0])#做了SVD降维#[3.409e-01-1.110e-16-1.205e-01-4.441e-160.000e+00-9.323e-01#2.226e-16]
如上所示,原先的稀疏向量W[0]经过SVD被转化成了密集向量U[0] 。
但SVD同样也有缺点:速度太慢
加快方法:采用TruncatedSVD(截去奇异值较小的部分,实现高速化)
如果矩阵大小是N,SVD的计算的复杂度将达到O(N3) 。这意味着SVD需要与N的立方成比例的计算量 。现实中这样的计算量是做不到的,所以往往会使用TruncatedSVD等更快的方法 。
TruncatedSVD通过截去(truncated)奇异值较小的部分,从而实现高速化 。下面,我们将使用sklearn库的TruncatedSVD 。
以上都是用的一句话语料来举的例子,接下来要来“真的”了!
使用真的更大的语料库:PennTreebank(PTB数据集)
PTB数据集
这个PTB语料库是以文本文件的形式提供的,与原始的PTB的文章相比,多了若干预处理,包括将稀有单词替换成特殊字符《unk》(unknown的简称),将具体的数字替换成“N”等 。
作为参考,图2-12给出了PTB语料库的部分内容 。一行保存一个句子 。
手把手教你搭建NLP经典模型
文章插图
这里,我们还要将所有句子连接起来,在每个句子的结尾处插入一个特殊字符《eos》(endofsentence的简称) 。
接下来我们将代码实现如何使用PTB数据集 。
importsyssys.path.append(’ 。.‘)fromdatasetimportptbcorpus,word_to_id,id_to_word=ptb.load_data(’train‘)#训练用数据print(’corpussize:‘,len(corpus))print(’corpus[:30]:‘,corpus[:30])print()print(’id_to_word[0]:‘,id_to_word[0])print(’id_to_word[1]:‘,id_to_word[1])print(’id_to_word[2]:‘,id_to_word[2])print()print(“word_to_id[’car‘]:”,word_to_id[’car‘])print(“word_to_id[’happy‘]:”,word_to_id[’happy‘])print(“word_to_id[’lexus‘]:”,word_to_id[’lexus‘])
结果如下所示:
corpussize:929589#数据集中词总数corpus[:30]:[01234567891011121314151617181920212223242526272829]id_to_word[0]:aerid_to_word[1]:banknoteid_to_word[2]:berlitzword_to_id[’car‘]:3856word_to_id[’happy‘]:4428word_to_id[’lexus‘]:7426
和之前一样,corpus中保存了单词ID列表,id_to_word是将单词ID转化为单词的字典,word_to_id是将单词转化为单词ID的字典 。
如上面的代码所示,使用ptb.load_data()加载数据 。此时,指定参数’train‘、’test‘和’valid‘中的一个,它们分别对应训练用数据、测试用数据和验证用数据中的一个 。以上就是ptb.py文件的使用方法 。
加载PTB数据集的代码:dataset/ptb.py
使用PTB数据集的例子:ch2/show_ptb.py
基于计数(统计)的方法利用PTB数据集的代码:ch02/count_method_big.py
总结
词向量表示总共介绍了:
基于同义词词典的方法
基于计数统计的方法
同义词词典需要人工定义词之间的相关性,很费力;
使用计数统计的方法可以自动的获取词向量表示 。
用计数统计的方法表示词向量的步骤:
使用语料库(使用语料库对单词进行向量化是主流方法)
计算上下文单词共同出现的次数(共现矩阵)
转化为PPMI矩阵(为了减少无意义词的干扰)
基于SVD降维(解决维度爆炸和矩阵稀疏问题,以提高稳健性)
从而获得了每个单词的分布式表示,也就是词向量表示,每个单词表示为固定长度的密集向量 。(单词的分布式表示=词向量表示)
在单词的向量空间中,含义上接近的单词距离上也更接近 。
使用语料库对单词进行向量化是主流方法 。
其实在海量数据的今天,基于计数统计的方法难以处理大规模的数据集,统计方法是需要一次性统计整个语料库,需要一次性处理全部的数据,而SVD降维的复杂度又太大,于是将推出——基于推理的方法,也就是基于神经网络的方法 。
神经网络一次只需要处理一个mini-batch的数据进行学习,并且反复更新网络权重 。
基于推理(神经网络)的方法,最著名的就是Word2Vec 。下一次我们会详细的介绍它的优点缺点以及使用方法噢!
原文标题:小白跟学系列之手把手搭建NLP经典模型-2(含代码)
文章出处:【微信公众号:深度学习自然语言处理】欢迎添加关注!文章转载请注明出处 。
责任编辑:haq
.dfma {position: relative;width: 1000px;margin: 0 auto;}.dfma a::after {position: absolute;left: 0;bottom: 0;width: 30px;line-height: 1.4;text-align: center;background-color: rgba(0, 0, 0, .5);color: #fff;font-size: 12px;content: "广告";}.dfma img {display: block;}
手把手教你搭建NLP经典模型
文章插图

    推荐阅读