使用预训练的词向量

本节介绍如何通过mxnet.contrib.text使用预训练的词向量。需要注意的是,mxnet.contrib.text正在测试中并可能在未来有改动。如有改动,本节内容会作相应更新。

本节使用的预训练的GloVe和fastText词向量分别来自:

我们先载入需要的包。

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

由数据集建立词典和载入词向量——以fastText为例

看一下fastText前五个预训练的词向量。它们分别从不同语言的Wikipedia数据集训练得到。

In [2]:
text.embedding.get_pretrained_file_names('fasttext')[:5]
Out[2]:
['wiki.ab.vec', 'wiki.ace.vec', 'wiki.ady.vec', 'wiki.aa.vec', 'wiki.af.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)>

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

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]

使用预训练词向量初始化gluon.nn.Embedding

我们可以使用预训练的词向量初始化gluon.nn.Embedding

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

使用词典中“hello”和“world”两个词在词典中的索引,我们可以通过gluon.nn.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前五个预训练的词向量。

In [12]:
text.embedding.get_pretrained_file_names('glove')[:5]
Out[12]:
['glove.42B.300d.txt',
 'glove.6B.50d.txt',
 'glove.6B.100d.txt',
 'glove.6B.200d.txt',
 'glove.6B.300d.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]:
# 词到索引。
print(glove_6b50d.token_to_idx['beautiful'])
# 索引到词。
print(glove_6b50d.idx_to_token[3367])
# 词向量长度。
print(glove_6b50d.vec_len)
3367
beautiful
50

预训练词向量的应用——以GloVe为例

为了应用预训练词向量,我们需要定义余弦相似度。它可以比较两个向量之间的相似度。

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

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

In [17]:
x = nd.array([1, 2])
y = nd.array([10, 20])
z = nd.array([-1, -2])

print(cos_sim(x, y))
print(cos_sim(x, z))

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

[-1.]
<NDArray 1 @cpu(0)>

求近似词

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

In [18]:
def norm_vecs_by_row(x):
    return x / nd.sqrt(nd.sum(x * x, axis=1)).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+2,
                      ret_typ='indices')
    indices = [int(i.asscalar()) for i in indices]
    # 除去未知词符号和输入词。
    return token_embedding.to_tokens(indices[2:])

查找词典中与“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。解类比词的思路是,找到和c+(b-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+1,
                      ret_typ='indices')
    indices = [int(i.asscalar()) for i in indices]

    # 不考虑未知词为可能的类比词。
    if token_embedding.to_tokens(indices[0]) == token_embedding.unknown_token:
        return token_embedding.to_tokens(indices[1:])
    else:
        return token_embedding.to_tokens(indices[:-1])

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

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

验证一下vec(“son”)+vec(“woman”)-vec(“man”)与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”之于什么?

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

“形容词-形容词最高级”类比:“bad”之于“worst”相当于“big”之于什么?

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

“动词一般时-动词过去时”类比:“do”之于“did”相当于“go”之于什么?

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

结论

  • 使用mxnet.contrib.text可以轻松载入预训练的词向量。
  • 我们可以应用预训练的词向量求相似词和类比词。

练习

  • 将近似词和类比词应用中的\(k\)调大一些,观察结果。
  • 测试一下fastText的中文词向量(pretrained_file_name=’wiki.zh.vec’)。
  • 如果在使用循环神经网络的语言模型中将Embedding层初始化为预训练的词向量,效果如何?

吐槽和讨论欢迎点这里