Mxnet (38): 同类词和类比

北城以北 提交于 2020-10-11 18:03:45

1. 全局矢量词嵌入(GloVe)

使用常规的交叉熵损失函数有时会有一些问题:

  • 让模型预测的成本 q i j q_{ij} qij 成为合理的概率分布需要将字典中所有项目作为分母,这样会导致过多的开销。
  • 字典中经常会会有不常见的单词,这些单词很少出现在数据集中,在交叉熵损失函数中,对大量不常见单词的条件概率分布的最终预测可能不准确

GloVe为了解决上述问题而产生,相较于word2dev模型,它使用平方损失,并基于Skip-Gram模型做了如下三点改进:

  1. 使用非概率分布变量 p i j ′ = x i j p'_{ij}=x_{ij} pij=xij q i j ′ = exp ⁡ ( u j ⊤ v i ) q'_{ij}=\exp(\mathbf{u}_j^\top \mathbf{v}_i) qij=exp(ujvi) 并计算他们的log值。得到的平方损失为 ( log ⁡   p i j ′ − log ⁡   q i j ′ ) 2 = ( u j ⊤ v i − log ⁡   x i j ) 2 \left(\log\,p'_{ij} - \log\,q'_{ij}\right)^2 = \left(\mathbf{u}_j^\top \mathbf{v}_i - \log\,x_{ij}\right)^2 (logpijlogqij)2=(ujvilogxij)2.
  2. 对于每一个 w i w_i wi添加两个模型偏差标量: b i b_i bi (用于中心词)以及 c i c_i ci( 用于上下文).
  3. 用函数替换每个损失的权重 h ( x i j ) h(x_{ij}) h(xij)。权重函数 h ( x ) h(x) h(x) 是一个单调递增函数,作用域为 [ 0 , 1 ] [0, 1] [0,1]

1.1 从概率比率了解GloVe

使用 P ( w j ∣ w i ) P(w_j \mid w_i) P(wjwi) 表示生成以 w i w_i wi为中心词汇 w j w_j wj为上下文词汇的条件概率, 记录为 p i j p_{ij} pij。通过一个大型语料库中的"冰"和 "蒸汽"为例:

w k w_k wk= 固体 气体 时尚
p 1 = P ( w k ∣ 冰 ) p_1=P(w_k\mid \text{冰}) p1=P(wk) 0.00019 0.000066 0.003 0.000017
p 2 = P ( w k ∣ 蒸汽 ) p_2=P(w_k\mid\text{蒸汽}) p2=P(wk蒸汽) 0.000022 0.00078 0.0022 0.000018
p 1 / p 2 p_1/p_2 p1/p2 8.9 0.085 1.36 0.96

可以整理如下:

  • w k w_k wk 与 "冰"有关但是与 "蒸汽"无关, 比如 w k = 固体 w_k=\text{固体} wk=固体, 我们期望有更大的条件概率,如上表中为8.9。
  • w k w_k wk "蒸汽"有关但是与 "冰"没有关系,比如 w k = 气体 w_k=\text{气体} wk=气体, 我们期望有更小的条件概率, 比如上表中的0.085。
  • w k w_k wk 同时与"冰"和 "蒸汽"有关,比如 w k = 水 w_k=\text{水} wk=, 我们期望条件概率为 1, 上表中的为1.36 。
  • w k w_k wk 同时与"冰" 或者 "蒸汽"都没有关系,比如 w k = 时尚 w_k=\text{时尚} wk=时尚, 我们期望条件概率为 1, 上表中为0.96。

可见条件概率比可以更直观的表示不同单词之间的关系。

2 子词嵌入

英语单词通常具有内部结构和形成方法。如,可以通过拼写来推断“ dog”,“ dogs”和“ dogcatcher”之间的关系,所有这些词都有相同的词根“ dog”,但是它们使用不同的后缀来更改词的含义。形态学是语言学的重要分支

2.1 fastText

在word2vec中,“ dog”和“ dogs”由两个不同的向量表示,而这两个向量之间的关系并未在模型中直接表示。fastText通过在word2vec的Skip-Gram模型中引入形态信息来改进这一问题。在fastText中每个中心词都是子词的并集。与skip-gram模型相比,fastText中的字典更大,从而导致更多的模型参数。而且,一个单词的向量需要所有子单词向量的求和,这导致更高的计算复杂度。但是,通过查看结构相似的其他单词,我们可以获得更好的矢量,以用于更常见的复杂单词,甚至是词典中不存在的单词。

3. 查找同义词和类比

在实践中,可以使用已经训练好的单词向量用于自然语言处理任务,这里介绍使用这些训练好的词向量查找同义词和类比。

from d2l import mxnet as d2l
from mxnet import np, npx
import os

npx.set_np()

下面列出了尺寸为50、100和300的预先训练的GloVe嵌入,可以从GloVe网站下载。预训练的fastText嵌入可用多种语言提供。在这里,我们考虑一种可以从fastText网站下载的英文版本(300维“ wiki.en”)。

d2l.DATA_HUB['glove.6b.50d'] = (d2l.DATA_URL + 'glove.6B.50d.zip', '0b8703943ccdb6eb788e6f091b8946e82231bc4d')

d2l.DATA_HUB['glove.6b.100d'] = (d2l.DATA_URL + 'glove.6B.100d.zip',  'cd43bfb07e44e6f27cbcc7bc9ae3d80284fdaf5a')

d2l.DATA_HUB['glove.42b.300d'] = (d2l.DATA_URL + 'glove.42B.300d.zip',  'b5116e234e9eb9076672cfeabf5469f3eec904fa')

d2l.DATA_HUB['wiki.en'] = (d2l.DATA_URL + 'wiki.en.zip',  'c1816da3821ae9f43899be655002f6c723e91b88')

定义一个TokenEmbedding类用来加载上述预训练的Glove和fastText词向量集。

class TokenEmbedding:
    """加载词向量"""
    def __init__(self, embedding_name):
        self.idx_to_token, self.idx_to_vec = self._load_embedding(
            embedding_name)
        self.unknown_idx = 0
        self.token_to_idx = {
   
   token: idx for idx, token in enumerate(self.idx_to_token)}

    def _load_embedding(self, embedding_name):
        idx_to_token, idx_to_vec = ['<unk>'], []
        data_dir = d2l.download_extract(embedding_name)
        with open(os.path.join(data_dir, 'vec.txt'), 'r') as f:
            for line in f:
                elems = line.rstrip().split(' ')
                token, elems = elems[0], [float(elem) for elem in elems[1:]]
                # 跳过头部信息
                if len(elems) > 1:
                    idx_to_token.append(token)
                    idx_to_vec.append(elems)
        idx_to_vec = [[0] * len(idx_to_vec[0])] + idx_to_vec
        return idx_to_token, np.array(idx_to_vec)

    def __getitem__(self, tokens):
        indices = [self.token_to_idx.get(token, self.unknown_idx) for token in tokens]
        vecs = self.idx_to_vec[np.array(indices)]
        return vecs

    def __len__(self):
        return len(self.idx_to_token)

使用预先训练在Wikipedia子集上的50维GloVe嵌入。词典包含400000个单词以及未知token

glove_6b50d = TokenEmbedding('glove.6b.50d')
len(glove_6b50d)

# 400001

3.2 应用预训练的单词向量

3.2.1 查找同义词

为了找到k个最近实例,这里将编写一个函数实现部分knn(k-nearest Neighbor)的功能。

def knn(W, x, k):
    cos = np.dot(W, x.reshape(-1,)) / (np.sqrt(np.sum(W*W, axis=1) + 1e-9) * np.sqrt((x*x).sum()))
    topk = npx.topk(cos, k=k, ret_typ='indices')
    return topk, [cos[int(i)] for i in topk]

编写通过词向量搜索同义词方法。

def get_similar_tokens(query_token, k, embed):
    topk, cos = knn(embed.idx_to_vec, embed[[query_token]], k+1)
    for i, c in zip(topk[1:], cos[1:]):  # 将输入单词移除
        print(f'cosine sim={float(c):.3f}:  {embed.idx_to_token[int(i)]}')

glove_6b50d已经创建的预训练词向量实例字典包含40万个词和一个特殊的未知标记。除了输入单词和未知单词,我们搜索与“chip”含义相同的词汇:

get_similar_tokens('chip', 3, glove_6b50d)

在这里插入图片描述

3.2.2 寻找类比

除了寻找同义词,还可以用来寻找词向量之间的类比:对于类比关系中的四个词 a : b : : c : d a:b::c:d a:b::c:d ,已知, a , b 和 c ,我们想找到 d 。转化为向量为找到与 vec ( c ) + vec ( b ) − vec ( a ) \text{vec}(c)+\text{vec}(b)-\text{vec}(a) vec(c)+vec(b)vec(a)相近的词。

def get_analogy(token_a, token_b, token_c, embed):
    vecs = embed[[token_a, token_b, token_c]]
    x = vecs[1] - vecs[0] + vecs[2]
    topk, cos = knn(embed.idx_to_vec, x, 1)
    return embed.idx_to_token[int(topk[0])]  

北京对于中国来说是首都,等同于东京对于日本的关系。

get_analogy('beijing', 'china', 'tokyo', glove_6b50d)

# 'japan'

4. 参考

https://d2l.ai/chapter_natural-language-processing-pretraining/similarity-analogy.html

5. 代码

github

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!