求近似词和类比词

本节介绍如何应用在大规模语料上预训练的词向量,例如求近似词和类比词。这里使用的预训练的 GloVe 和 fastText 词向量分别来自它们的项目网站 [1,2]。

首先导入实验所需的包或模块。

In [1]:
from mxnet import nd
from mxnet.contrib import text
from mxnet.gluon import nn

由数据集建立词典和载入词向量

下面,我们以 fastText 为例,由数据集建立词典并载入词向量。fastText 提供了基于不同语言的多套预训练的词向量。这些词向量是在大规模语料上训练得到的,例如维基百科语料。以下打印了其中的 10 种。

In [2]:
print(text.embedding.get_pretrained_file_names('fasttext')[:10])
['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']

访问词向量

为了演示方便,我们创建一个很小的文本数据集,并计算词频。

In [3]:
text_data = ' hello world \n hello nice world \n hi world \n'
counter = text.utils.count_tokens_from_str(text_data)

我们先根据数据集建立词典,并为该词典中的词载入 fastText 词向量。这里使用 Simple English 的预训练词向量。

In [4]:
my_vocab = text.vocab.Vocabulary(counter)
my_embedding = text.embedding.create(
    'fasttext', pretrained_file_name='wiki.simple.vec', vocabulary=my_vocab)
/var/lib/jenkins/miniconda3/envs/gluon_zh_docs/lib/python3.6/site-packages/mxnet/contrib/text/embedding.py:278: UserWarning: At line 1 of the pre-trained text embedding file: token 111051 with 1-dimensional vector [300.0] is likely a header and is skipped.
  'skipped.' % (line_num, token, elems))

词典除了包括数据集中四个不同的词语,还包括一个特殊的未知词符号。打印词典大小。

In [5]:
len(my_embedding)
Out[5]:
5

默认情况下,任意一个词典以外词的词向量为零向量。

In [6]:
my_embedding.get_vecs_by_tokens('beautiful')[:10]
Out[6]:

[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
<NDArray 10 @cpu(0)>

fastText 中每个词均使用 300 维的词向量。打印数据集中两个词“hello”和“world”词向量的形状。

In [7]:
my_embedding.get_vecs_by_tokens(['hello', 'world']).shape
Out[7]:
(2, 300)

打印“hello”和“world”词向量前五个元素。

In [8]:
my_embedding.get_vecs_by_tokens(['hello', 'world'])[:, :5]
Out[8]:

[[ 0.39567     0.21454    -0.035389   -0.24299    -0.095645  ]
 [ 0.10444    -0.10858     0.27212     0.13299    -0.33164999]]
<NDArray 2x5 @cpu(0)>

打印“hello”和“world”在词典中的索引。

In [9]:
my_embedding.to_indices(['hello', 'world'])
Out[9]:
[2, 1]

使用预训练词向量初始化 Embedding 实例

我们在 “循环神经网络——使用 Gluon” 一节中介绍了 Gluon 中的 Embedding 实例,并对其中每个词的向量做了随机初始化。实际上,我们还可以使用预训练的词向量初始化 Embedding 实例。

In [10]:
layer = nn.Embedding(len(my_embedding), my_embedding.vec_len)
layer.initialize()
layer.weight.set_data(my_embedding.idx_to_vec)

使用词典中“hello”和“world”两个词在词典中的索引,我们可以通过 Embedding 实例得到它们的预训练词向量,并向神经网络的下一层传递。

In [11]:
layer(nd.array([2, 1]))[:, :5]
Out[11]:

[[ 0.39567     0.21454    -0.035389   -0.24299    -0.095645  ]
 [ 0.10444    -0.10858     0.27212     0.13299    -0.33164999]]
<NDArray 2x5 @cpu(0)>

由预训练词向量建立词典——以 GloVe 为例

除了使用数据集建立词典外,我们还可以直接由预训练词向量建立词典。

这一次我们使用 GloVe 的预训练词向量。以下打印了 GloVe 提供的各套预训练词向量。这些词向量是在大规模语料上训练得到的,例如维基百科语料和推特语料。

In [12]:
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']

我们使用 50 维的词向量。和之前不同,这里不再传入根据数据集建立的词典,而是直接使用预训练词向量中的词建立词典。

In [13]:
glove_6b50d = text.embedding.create('glove',
                                    pretrained_file_name='glove.6B.50d.txt')

打印词典大小。注意其中包含一个特殊的未知词符号。

In [14]:
print(len(glove_6b50d))
400001

我们可以访问词向量的属性。

In [15]:
# 词到索引,索引到词。
glove_6b50d.token_to_idx['beautiful'], glove_6b50d.idx_to_token[3367]
Out[15]:
(3367, 'beautiful')

应用预训练词向量

下面我们以 GloVe 为例,展示预训练词向量的应用。

首先,我们定义余弦相似度,并用它表示两个向量之间的相似度。

In [16]:
def cos_sim(x, y):
    return nd.dot(x, y) / (x.norm() * y.norm())

余弦相似度的值域在 -1 到 1 之间。两个余弦相似度越大的向量越相似。

In [17]:
x = nd.array([1, 2])
y = nd.array([10, 20])
z = nd.array([-1, -2])
cos_sim(x, y), cos_sim(x, z)
Out[17]:
(
 [ 1.]
 <NDArray 1 @cpu(0)>,
 [-1.]
 <NDArray 1 @cpu(0)>)

求近似词

给定任意词,我们可以从 GloVe 的整个词典(大小 40 万,不含未知词符号)中找出与它最接近的 \(k\) 个词。之前已经提到,词与词之间的相似度可以用两个词向量的余弦相似度表示。

In [18]:
def norm_vecs_by_row(x):
    # 分母中添加的 1e-10 是为了数值稳定性。
    return x / (nd.sum(x * x, axis=1) + 1e-10).sqrt().reshape((-1, 1))

def get_knn(token_embedding, k, word):
    word_vec = token_embedding.get_vecs_by_tokens([word]).reshape((-1, 1))
    vocab_vecs = norm_vecs_by_row(token_embedding.idx_to_vec)
    dot_prod = nd.dot(vocab_vecs, word_vec)
    indices = nd.topk(dot_prod.reshape((len(token_embedding), )), k=k+1,
                      ret_typ='indices')
    indices = [int(i.asscalar()) for i in indices]
    # 除去输入词。
    return token_embedding.to_tokens(indices[1:])

查找词典中与“baby”最近似的 5 个词。

In [19]:
get_knn(glove_6b50d, 5, 'baby')
Out[19]:
['babies', 'boy', 'girl', 'newborn', 'pregnant']

验证一下“baby”和“babies”两个词向量之间的余弦相似度。

In [20]:
cos_sim(glove_6b50d.get_vecs_by_tokens('baby'),
        glove_6b50d.get_vecs_by_tokens('babies'))
Out[20]:

[ 0.83871299]
<NDArray 1 @cpu(0)>

查找词典中与“computers”最近似的 5 个词。

In [21]:
get_knn(glove_6b50d, 5, 'computers')
Out[21]:
['computer', 'phones', 'pcs', 'machines', 'devices']

查找词典中与“run”最近似的 5 个词。

In [22]:
get_knn(glove_6b50d, 5, 'run')
Out[22]:
['running', 'runs', 'went', 'start', 'ran']

查找词典中与“beautiful”最近似的 5 个词。

In [23]:
get_knn(glove_6b50d, 5, 'beautiful')
Out[23]:
['lovely', 'gorgeous', 'wonderful', 'charming', 'beauty']

求类比词

除近似词以外,我们还可以使用预训练词向量求词与词之间的类比关系。例如,man(男人): woman(女人):: son(儿子) : daughter(女儿)是一个类比例子:“man”之于“woman”相当于“son”之于“daughter”。求类比词问题可以定义为:对于类比关系中的四个词 \(a : b :: c : d\),给定前三个词 \(a\)\(b\)\(c\),求 \(d\)。设词 \(w\) 的词向量为 \(\text{vec}(w)\)。而解类比词的思路是,找到和 \(\text{vec}(c)+\text{vec}(b)-\text{vec}(a)\) 的结果向量最相似的词向量。

本例中,我们将从整个词典(大小 40 万,不含未知词符号)中搜索类比词。

In [24]:
def get_top_k_by_analogy(token_embedding, k, word1, word2, word3):
    word_vecs = token_embedding.get_vecs_by_tokens([word1, word2, word3])
    word_diff = (word_vecs[1] - word_vecs[0] + word_vecs[2]).reshape((-1, 1))
    vocab_vecs = norm_vecs_by_row(token_embedding.idx_to_vec)
    dot_prod = nd.dot(vocab_vecs, word_diff)
    indices = nd.topk(dot_prod.reshape((len(token_embedding), )), k=k,
                      ret_typ='indices')
    indices = [int(i.asscalar()) for i in indices]
    return token_embedding.to_tokens(indices)

“男 - 女”类比:“man”之于“woman”相当于“son”之于什么?

In [25]:
get_top_k_by_analogy(glove_6b50d, 1, 'man', 'woman', 'son')
Out[25]:
['daughter']

验证一下 \(\text{vec(son)+vec(woman)-vec(man)}\)\(\text{vec(daughter)}\) 两个向量之间的余弦相似度。

In [26]:
def cos_sim_word_analogy(token_embedding, word1, word2, word3, word4):
    words = [word1, word2, word3, word4]
    vecs = token_embedding.get_vecs_by_tokens(words)
    return cos_sim(vecs[1] - vecs[0] + vecs[2], vecs[3])

cos_sim_word_analogy(glove_6b50d, 'man', 'woman', 'son', 'daughter')
Out[26]:

[ 0.96583432]
<NDArray 1 @cpu(0)>

“首都 - 国家”类比:“beijing”(北京)之于“china”(中国)相当于“tokyo”(东京)之于什么?答案应该是“japan”(日本)。

In [27]:
get_top_k_by_analogy(glove_6b50d, 1, 'beijing', 'china', 'tokyo')
Out[27]:
['japan']

“形容词 - 形容词最高级”类比:“bad”(坏的)之于“worst”(最坏的)相当于“big”(大的)之于什么?答案应该是“biggest”(最大的)。

In [28]:
get_top_k_by_analogy(glove_6b50d, 1, 'bad', 'worst', 'big')
Out[28]:
['biggest']

“动词一般时 - 动词过去时”类比:“do”(做)之于“did”(做过)相当于“go”(去)之于什么?答案应该是“went”(去过)。

In [29]:
get_top_k_by_analogy(glove_6b50d, 1, 'do', 'did', 'go')
Out[29]:
['went']

小结

  • 我们可以应用预训练的词向量求近似词和类比词。

练习

  • 将近似词和类比词应用中的 \(k\) 调大一些,观察结果。
  • 测试一下 fastText 的中文词向量(pretrained_file_name=’wiki.zh.vec’)。
  • 如果在 “循环神经网络的 Gluon 实现” 一节中将 Embedding 实例里的参数初始化为预训练的词向量,效果如何?

扫码直达 讨论区

参考文献

[1] GloVe 项目网站 . https://nlp.stanford.edu/projects/glove/

[2] fastText 项目网站 . https://fasttext.cc/