天天看点

自然语言处理(NLP)之求近义词和类比词<MXNet中GloVe和FastText的模型使用>

这节主要就是熟悉MXNet框架中的两种模型:GloVe和FastText的模型(词嵌入名称),每个模型下面有很多不同的词向量,这些基本都来自wiki维基百科和twitter推特这些子集预训练得到的。

我们只需要导入mxnet.contrib中的text模块即可,这里面提供了很多关于自然语言处理相关的函数和类。

from mxnet import nd
from mxnet.contrib import text
print(text.embedding.get_pretrained_file_names().keys())
#dict_keys(['glove', 'fasttext'])
print(text.embedding.get_pretrained_file_names('glove'))
'''
['glove.42B.300d.txt', 'glove.6B.50d.txt', 'glove.6B.100d.txt', 'glove.6B.200d.txt', 
'glove.6B.300d.txt', 'glove.840B.300d.txt', 'glove.twitter.27B.25d.txt', 'glove.twitter.27B.50d.txt', 
'glove.twitter.27B.100d.txt', 'glove.twitter.27B.200d.txt']
'''
           

这些模型的命名大概是"模型名.(数据集.)数据集词数.词向量维度.txt",比如最后这个'glove.twitter.27B.200d.txt'表示glove模型,基于twitter的数据,200个维度的词向量,这个有1.4G,我们选择一个小点的'glove.6B.50d.txt'来测试,822M,可以自己手动也可以让程序自动下载。这里下载下来之后里面包括了50维、100维、200维、300维的文档,下载的目录,本人的是C:\Users\Tony\AppData\Roaming\mxnet\embeddings\glove\glove.6B.zip 解压之后分别是glove.6B.50d.txt、glove.6B.100d.txt、glove.6B.200d.txt、glove.6B.300d.txt这四个文本文档,对于不愿意下载这么大文件的伙伴可以只下载glove.6B.50d.txt,只有163M,下载地址:https://download.csdn.net/download/weixin_41896770/87464231

我们用Notepad++打开看下这个50维的词向量的内容。如下图:

自然语言处理(NLP)之求近义词和类比词<MXNet中GloVe和FastText的模型使用>

可以看到每行第一个词对应后面的数字有50个,也就是说是50维的意思,这个50个可以使用一个统计函数来快速验证下:

>>> import collections
>>> collections.Counter('of 0.70853 0.57088 -0.4716 0.18048 0.54449 0.72603 0.18157 -0.52393 0.10381 -0.17566 0.078852 -0.36216 -0.11829 -0.83336 0.11917 -0.16605 0.061555 -0.012719 -0.56623 0.013616 0.22851 -0.14396 -0.067549 -0.38157 -0.23698 -1.7037 -0.86692 -0.26704 -0.2589 0.1767 3.8676 -0.1613 -0.13273 -0.68881 0.18444 0.0052464 -0.33874 -0.078956 0.24185 0.36576 -0.34727 0.28483 0.075693 -0.062178 -0.38988 0.22902 -0.21617 -0.22562 -0.093918 -0.80375')
Counter({'0': 69, ' ': 50, '.': 50, '1': 34, '6': 33, '8': 32, '-': 29, '3': 28, '7': 27, '2': 26, '5': 23, '4': 17, '9': 16, 'o': 1, 'f': 1})
           

其中点'.'是50,说明有50个数字,就是说每个词都映射成50维的数字来表示。

GloVe模型

创建词向量实例:

glove_6b50d=text.embedding.create('glove',pretrained_file_name='glove.6B.50d.txt')
print(len(glove_6b50d))#400001
           

这个词典有40万个词和一个特殊的未知词符号。我们可以通过词来获取它在词典中的索引或者通过索引获取词。

print(glove_6b50d.token_to_idx['of'],glove_6b50d.idx_to_token[4])#4 of
           

上面图片中我们看到的of是第四个,索引应该是3,为什么是4呢?我们打印索引为0的词就明白了,是未知词<unk>

我们通过上面的操作应该了解了这个预训练的词向量。接下来我们就以GloVe模型为例,展示这个预训练词向量的应用。

近义词

我们通过余弦相似度来搜索近义词,在后面求类比词也将用到k近邻(k-nearest neighbor,knn)的逻辑,所以这里写一个knn的方法

def knn(W, x, k):
    # 1e-9为了数值稳定性
    cos = nd.dot(W, x.reshape((-1,))) / (
        (nd.sum(W**2, axis=1) + 1e-9).sqrt() * nd.sum(x**2).sqrt()
    )
    #topk选取最大值的索引,k表示取前几个最大
    topk = nd.topk(cos, k=k, ret_typ="indices").asnumpy().astype("int32")
    return topk, [cos[i].asscalar() for i in topk]
           

然后通过词向量实例来搜索近义词

我们再次来了解下这个词向量,在上面我们知道第一个是未知词,打开文档没有看到,所以我们这里通过输出来看下

print(glove_6b50d.idx_to_vec)
'''
[[ 0.        0.        0.       ...  0.        0.        0.      ]
 [ 0.418     0.24968  -0.41242  ... -0.18411  -0.11514  -0.78581 ]
 [ 0.013441  0.23682  -0.16899  ... -0.56657   0.044691  0.30392 ]
 ...
 [-0.51181   0.058706  1.0913   ... -0.25003  -1.125     1.5863  ]
 [-0.75898  -0.47426   0.4737   ...  0.78954  -0.014116  0.6448  ]
 [ 0.072617 -0.51393   0.4728   ... -0.18907  -0.59021   0.55559 ]]
<NDArray 400001x50 @cpu(0)>
'''
           

可以看到第一行都是0,这个是<unk>的词向量,其余400000个词都对应着50维的数值。

还可以指定词来查询对应的向量:

print(glove_6b50d.get_vecs_by_tokens(["the", "of"]))
'''
[[ 4.1800e-01  2.4968e-01 -4.1242e-01  1.2170e-01  3.4527e-01 -4.4457e-02
  -4.9688e-01 -1.7862e-01 -6.6023e-04 -6.5660e-01  2.7843e-01 -1.4767e-01
  -5.5677e-01  1.4658e-01 -9.5095e-03  1.1658e-02  1.0204e-01 -1.2792e-01
  -8.4430e-01 -1.2181e-01 -1.6801e-02 -3.3279e-01 -1.5520e-01 -2.3131e-01
  -1.9181e-01 -1.8823e+00 -7.6746e-01  9.9051e-02 -4.2125e-01 -1.9526e-01
   4.0071e+00 -1.8594e-01 -5.2287e-01 -3.1681e-01  5.9213e-04  7.4449e-03
   1.7778e-01 -1.5897e-01  1.2041e-02 -5.4223e-02 -2.9871e-01 -1.5749e-01
  -3.4758e-01 -4.5637e-02 -4.4251e-01  1.8785e-01  2.7849e-03 -1.8411e-01
  -1.1514e-01 -7.8581e-01]
 [ 7.0853e-01  5.7088e-01 -4.7160e-01  1.8048e-01  5.4449e-01  7.2603e-01
   1.8157e-01 -5.2393e-01  1.0381e-01 -1.7566e-01  7.8852e-02 -3.6216e-01
  -1.1829e-01 -8.3336e-01  1.1917e-01 -1.6605e-01  6.1555e-02 -1.2719e-02
  -5.6623e-01  1.3616e-02  2.2851e-01 -1.4396e-01 -6.7549e-02 -3.8157e-01
  -2.3698e-01 -1.7037e+00 -8.6692e-01 -2.6704e-01 -2.5890e-01  1.7670e-01
   3.8676e+00 -1.6130e-01 -1.3273e-01 -6.8881e-01  1.8444e-01  5.2464e-03
  -3.3874e-01 -7.8956e-02  2.4185e-01  3.6576e-01 -3.4727e-01  2.8483e-01
   7.5693e-02 -6.2178e-02 -3.8988e-01  2.2902e-01 -2.1617e-01 -2.2562e-01
  -9.3918e-02 -8.0375e-01]]
<NDArray 2x50 @cpu(0)>
'''
           

这个可以跟上面的图片比对下,是不是一样。

然后通过上面的knn,我们来做个示例:

def get_similar_tokens(query_token, k, embed):
    topk, cos = knn(embed.idx_to_vec, embed.get_vecs_by_tokens([query_token]), k + 1)
    for i, c in zip(topk[1:], cos[1:]):
        print("余弦相似度:%.3f:%s" % (c, embed.idx_to_token[i]))

get_similar_tokens("chip", 3, glove_6b50d)
'''
余弦相似度:0.856:chips
余弦相似度:0.749:intel
余弦相似度:0.749:electronics
'''

get_similar_tokens("beautiful", 3, glove_6b50d)
'''
余弦相似度:0.921:lovely
余弦相似度:0.893:gorgeous
余弦相似度:0.830:wonderful
'''
           

当然这里的近义词,跟我们传统上的近义词还是有点区别,比如上面的芯片和英特尔不是近义词,这里相似度很高,是因为英特尔主营芯片。

类比词

类比词跟近义词不一样,它类似于一种对照出来的词,例如:"man"类比"woman"那么"son"类比是"daughter",那么如何求类比词,这里就是需要三个词求第四个词,对于a:b::c:d,给定a、b、c求出d。假设词w的词向量为vec(w),求类比词的思路是,搜索与vec(b)+vec(c)-vec(a)的结果向量最相似的词向量。

def get_analogy(a, b, c, embed):
    vecs = embed.get_vecs_by_tokens([a, b, c])
    x = vecs[1] + vecs[2] - vecs[0]
    topk, cos = knn(embed.idx_to_vec, x, 1)
    return embed.idx_to_token[topk[0]], cos

print(get_analogy("man", "woman", "son", glove_6b50d))
'''
('daughter', [0.9658343])
'''
print(get_analogy("beijing", "china", "tokyo", glove_6b50d))
'''
('japan', [0.9054066])
'''
print(get_analogy("bad", "worst", "big", glove_6b50d))
'''
('biggest', [0.8059626])
'''
print(get_analogy("do", "did", "go", glove_6b50d))
'''
('went', [0.9242296])
'''
           

FastText模型

上面介绍了全局词向量模型GloVe,接下来看下FastText模型,我们回到最开始,先来看下它下面有哪些词向量模型:

print(text.embedding.get_pretrained_file_names('fasttext'))
'''
['crawl-300d-2M.vec', 'wiki.aa.vec', 'wiki.ab.vec', 'wiki.ace.vec', 'wiki.ady.vec', 'wiki.af.vec', 'wiki.ak.vec', 'wiki.als.vec', 'wiki.am.vec', 'wiki.ang.vec', 'wiki.an.vec', 'wiki.arc.vec', 'wiki.ar.vec', 'wiki.arz.vec', 'wiki.ast.vec', 'wiki.as.vec', 'wiki.av.vec', 'wiki.ay.vec', 'wiki.azb.vec', 'wiki.az.vec', 'wiki.bar.vec', 'wiki.bat_smg.vec', 'wiki.ba.vec', 'wiki.bcl.vec', 'wiki.be.vec', 'wiki.bg.vec', 'wiki.bh.vec', 'wiki.bi.vec', 'wiki.bjn.vec', 'wiki.bm.vec', 'wiki.bn.vec', 'wiki.bo.vec', 'wiki.bpy.vec', 'wiki.br.vec', 'wiki.bs.vec', 'wiki.bug.vec', 'wiki.bxr.vec', 'wiki.ca.vec', 'wiki.cbk_zam.vec', 'wiki.cdo.vec', 'wiki.ceb.vec', 'wiki.ce.vec', 'wiki.cho.vec', 'wiki.chr.vec', 'wiki.ch.vec', 'wiki.chy.vec', 'wiki.ckb.vec', 'wiki.co.vec', 'wiki.crh.vec', 'wiki.cr.vec', 'wiki.csb.vec', 'wiki.cs.vec', 'wiki.cu.vec', 'wiki.cv.vec', 'wiki.cy.vec', 'wiki.da.vec', 'wiki.de.vec', 'wiki.diq.vec', 'wiki.dsb.vec', 'wiki.dv.vec', 'wiki.dz.vec', 'wiki.ee.vec', 'wiki.el.vec', 'wiki.eml.vec', 'wiki.en.vec', 'wiki.eo.vec', 'wiki.es.vec', 'wiki.et.vec', 'wiki.eu.vec', 'wiki.ext.vec', 'wiki.fa.vec', 'wiki.ff.vec', 'wiki.fiu_vro.vec', 'wiki.fi.vec', 'wiki.fj.vec', 'wiki.fo.vec', 'wiki.frp.vec', 'wiki.frr.vec', 'wiki.fr.vec', 'wiki.fur.vec', 'wiki.fy.vec', 'wiki.gag.vec', 'wiki.gan.vec', 'wiki.ga.vec', 'wiki.gd.vec', 'wiki.glk.vec', 'wiki.gl.vec', 'wiki.gn.vec', 'wiki.gom.vec', 'wiki.got.vec', 'wiki.gu.vec', 'wiki.gv.vec', 'wiki.hak.vec', 'wiki.ha.vec', 'wiki.haw.vec', 'wiki.he.vec', 'wiki.hif.vec', 'wiki.hi.vec', 'wiki.ho.vec', 'wiki.hr.vec', 'wiki.hsb.vec', 'wiki.ht.vec', 'wiki.hu.vec', 'wiki.hy.vec', 'wiki.hz.vec', 'wiki.ia.vec', 'wiki.id.vec', 'wiki.ie.vec', 'wiki.ig.vec', 'wiki.ii.vec', 'wiki.ik.vec', 'wiki.ilo.vec', 'wiki.io.vec', 'wiki.is.vec', 'wiki.it.vec', 'wiki.iu.vec', 'wiki.jam.vec', 'wiki.ja.vec', 'wiki.jbo.vec', 'wiki.jv.vec', 'wiki.kaa.vec', 'wiki.kab.vec', 'wiki.ka.vec', 'wiki.kbd.vec', 'wiki.kg.vec', 'wiki.ki.vec', 'wiki.kj.vec', 'wiki.kk.vec', 'wiki.kl.vec', 'wiki.km.vec', 'wiki.kn.vec', 'wiki.koi.vec', 'wiki.ko.vec', 'wiki.krc.vec', 'wiki.kr.vec', 'wiki.ksh.vec', 'wiki.ks.vec', 'wiki.ku.vec', 'wiki.kv.vec', 'wiki.kw.vec', 'wiki.ky.vec', 'wiki.lad.vec', 'wiki.la.vec', 'wiki.lbe.vec', 'wiki.lb.vec', 'wiki.lez.vec', 'wiki.lg.vec', 'wiki.lij.vec', 'wiki.li.vec', 'wiki.lmo.vec', 'wiki.ln.vec', 'wiki.lo.vec', 'wiki.lrc.vec', 'wiki.ltg.vec', 'wiki.lt.vec', 'wiki.lv.vec', 'wiki.mai.vec', 'wiki.map_bms.vec', 'wiki.mdf.vec', 'wiki.mg.vec', 'wiki.mhr.vec', 'wiki.mh.vec', 'wiki.min.vec', 'wiki.mi.vec', 'wiki.mk.vec', 'wiki.ml.vec', 'wiki.mn.vec', 'wiki.mo.vec', 'wiki.mrj.vec', 'wiki.mr.vec', 'wiki.ms.vec', 'wiki.mt.vec', 'wiki.multi.ar.vec', 'wiki.multi.bg.vec', 'wiki.multi.ca.vec', 'wiki.multi.cs.vec', 'wiki.multi.da.vec', 'wiki.multi.de.vec', 'wiki.multi.el.vec', 'wiki.multi.en.vec', 'wiki.multi.es.vec', 'wiki.multi.et.vec', 'wiki.multi.fi.vec', 'wiki.multi.fr.vec', 'wiki.multi.he.vec', 'wiki.multi.hr.vec', 'wiki.multi.hu.vec', 'wiki.multi.id.vec', 'wiki.multi.it.vec', 'wiki.multi.mk.vec', 'wiki.multi.nl.vec', 'wiki.multi.no.vec', 'wiki.multi.pl.vec', 'wiki.multi.pt.vec', 'wiki.multi.ro.vec', 'wiki.multi.ru.vec', 'wiki.multi.sk.vec', 'wiki.multi.sl.vec', 'wiki.multi.sv.vec', 'wiki.multi.tr.vec', 'wiki.multi.uk.vec', 'wiki.multi.vi.vec', 'wiki.mus.vec', 'wiki.mwl.vec', 'wiki.my.vec', 'wiki.myv.vec', 'wiki.mzn.vec', 'wiki.nah.vec', 'wiki.nap.vec', 'wiki.na.vec', 'wiki.nds_nl.vec', 'wiki.nds.vec', 'wiki.ne.vec', 'wiki-news-300d-1M-subword.vec', 'wiki-news-300d-1M.vec', 'wiki.new.vec', 'wiki.ng.vec', 'wiki.nl.vec', 'wiki.nn.vec', 'wiki.no.vec', 'wiki.nov.vec', 'wiki.nrm.vec', 'wiki.nso.vec', 'wiki.nv.vec', 'wiki.ny.vec', 'wiki.oc.vec', 'wiki.olo.vec', 'wiki.om.vec', 'wiki.or.vec', 'wiki.os.vec', 'wiki.pag.vec', 'wiki.pam.vec', 'wiki.pap.vec', 'wiki.pa.vec', 'wiki.pcd.vec', 'wiki.pdc.vec', 'wiki.pfl.vec', 'wiki.pih.vec', 'wiki.pi.vec', 'wiki.pl.vec', 'wiki.pms.vec', 'wiki.pnb.vec', 'wiki.pnt.vec', 'wiki.ps.vec', 'wiki.pt.vec', 'wiki.qu.vec', 'wiki.rm.vec', 'wiki.rmy.vec', 'wiki.rn.vec', 'wiki.roa_rup.vec', 'wiki.roa_tara.vec', 'wiki.ro.vec', 'wiki.rue.vec', 'wiki.ru.vec', 'wiki.rw.vec', 'wiki.sah.vec', 'wiki.sa.vec', 'wiki.scn.vec', 'wiki.sco.vec', 'wiki.sc.vec', 'wiki.sd.vec', 'wiki.se.vec', 'wiki.sg.vec', 'wiki.sh.vec', 'wiki.simple.vec', 'wiki.si.vec', 'wiki.sk.vec', 'wiki.sl.vec', 'wiki.sm.vec', 'wiki.sn.vec', 'wiki.so.vec', 'wiki.sq.vec', 'wiki.srn.vec', 'wiki.sr.vec', 'wiki.ss.vec', 'wiki.stq.vec', 'wiki.st.vec', 'wiki.su.vec', 'wiki.sv.vec', 'wiki.sw.vec', 'wiki.szl.vec', 'wiki.ta.vec', 'wiki.tcy.vec', 'wiki.tet.vec', 'wiki.te.vec', 'wiki.tg.vec', 'wiki.th.vec', 'wiki.ti.vec', 'wiki.tk.vec', 'wiki.tl.vec', 'wiki.tn.vec', 'wiki.to.vec', 'wiki.tpi.vec', 'wiki.tr.vec', 'wiki.ts.vec', 'wiki.tt.vec', 'wiki.tum.vec', 'wiki.tw.vec', 'wiki.ty.vec', 'wiki.tyv.vec', 'wiki.udm.vec', 'wiki.ug.vec', 'wiki.uk.vec', 'wiki.ur.vec', 'wiki.uz.vec', 'wiki.vec.vec', 'wiki.vep.vec', 'wiki.ve.vec', 'wiki.vi.vec', 'wiki.vls.vec', 'wiki.vo.vec', 'wiki.war.vec', 'wiki.wa.vec', 'wiki.wo.vec', 'wiki.wuu.vec', 'wiki.xal.vec', 'wiki.xh.vec', 'wiki.xmf.vec', 'wiki.yi.vec', 'wiki.yo.vec', 'wiki.za.vec', 'wiki.zea.vec', 'wiki.zh_classical.vec', 'wiki.zh_min_nan.vec', 'wiki.zh.vec', 'wiki.zh_yue.vec', 'wiki.zu.vec']
'''
           

可以看到基本都是维基百科的子集,还是挺多的(327个),有各种语言,还有粤语,我们来使用其中一个包含中文的['wiki.zh.vec']来看看效果是怎么样的。

同样可以自动下载或手动下载,https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/embeddings/fasttext/wiki.zh.zip

本人下载之后的地址为C:\Users\Tony\AppData\Roaming\mxnet\embeddings\fasttext

然后自动解压之后的wiki.zh.vec有821M,这么大的文件使用Notepad++打不开。

同样的创建词向量,这里使用预处理文件是:'wiki.zh.vec'

fasttext_zh = text.embedding.create("fasttext", pretrained_file_name="wiki.zh.vec")
           

这里我这里出现了一个警告:

UserWarning: At line 1 of the pre-trained text embedding file: token 332647 with 1-dimensional vector [300.0] is likely a header and is skipped.

'skipped.' % (line_num, token, elems))

也就是说这里的第一行应该是头,然后就忽略掉了,这里我们不管它。

另外从警告中的信息我们侧面知道了这个词典大小有332648个词包括一个未知词<unk>,词向量是300维度。当然这里我们也可以验证下是不是这样的:

print(fasttext_zh.idx_to_vec)
'''
[[ 0.         0.         0.        ...  0.         0.         0.       ]
 [ 2.6678     5.4073    -0.057711  ... -1.8354     0.36975    2.8985   ]
 [-0.11755    1.1515    -0.63657   ...  0.1388     1.0023     0.31335  ]
 ...
 [-0.36881   -0.21542   -0.20998   ... -0.12527    0.36239    0.56859  ]
 [-0.1583    -0.47584   -0.22553   ...  0.27256    0.033567   0.34423  ]
 [-0.17572    0.31794   -0.40152   ...  0.22954    0.48944   -0.0054855]]
<NDArray 332648x300 @cpu(0)>
'''
           

没有问题,跟警告信息是相符合的。

接下来我们看下在中文中的近义词和类比词:

get_similar_tokens("县长", 3, fasttext_zh)
'''
余弦相似度:0.950:县政
余弦相似度:0.950:省长
余弦相似度:0.949:县外
'''
           

恩,基本上还是比较接近

但是在类比词的时候,就不对了

print(get_analogy("爸爸", "妈妈", "儿子", fasttext_zh))
'''
('儿子', [0.95196044])
'''
           

英文能够很好的识别出来,为什么这里的中文就不可以了?正常来说中英文是没有区别的,都使用了词向量来代替原本的中英文。

然后我将那个get_analogy函数修改如下,多返回几个值看下什么情况:

def get_analogy(a, b, c, embed):
    vecs = embed.get_vecs_by_tokens([a, b, c])
    x = vecs[1] + vecs[2] - vecs[0]
    topk, cos = knn(embed.idx_to_vec, x, 4)
    return (
        embed.idx_to_token[topk[0]],
        embed.idx_to_token[topk[1]],
        embed.idx_to_token[topk[2]],
        embed.idx_to_token[topk[3]],
        cos,
    )


print(get_analogy("湖南", "长沙", "贵州", fasttext_zh))
print(get_analogy("爸爸", "妈妈", "儿子", fasttext_zh))
'''
('贵州', '长沙', '贵阳', '遵义', [0.9504416, 0.9466409, 0.9465305, 0.94506186])
('儿子', '女儿', '母亲', '妈妈', [0.95196044, 0.940535, 0.9378925, 0.9308049])
'''
           

候选的答案有正确的,只不过不是第一个,也不固定是第二个。这是什么情况,从占位来看,中文的处理跟英文还是存在区别,这里的具体原因是什么不是很清楚,有大神知道的,请指正!

继续阅读