[译] Pytorch 官方教程 词嵌入:编码词语与语义

原文传送门:WORD EMBEDDINGS: ENCODING LEXICAL SEMANTICS

[译] Pytorch 官方教程 词嵌入:编码词语与语义

WORD EMBEDDINGS: ENCODING LEXICAL SEMANTICS

词嵌入指的是:将用户词表中每一个单词与一个实数对应,并嵌入到一个密度向量中。在自然语言处理中,绝大多数情况研究的特征都是单词。然而,应该怎样在计算机中表示一个单词呢?一种方案是存储单词每个字母对应的ascii码,但这只能让我们知道我们存储的这个单词由哪些字母组成,并不能表达这个单词具体的含义(或许能从词缀推断出词性、从大小写推断出属性,但这远远不够)。而且,凭什么可以将这些表示组合在一起?

我们设计的神经网络往往是V|V|(词汇表的长度)维的,但是却需要从神经网络中得到低维、密集的输出(譬如需要预测的标签只有几个),我们要怎样从将数据从高维空间转移到低维空间呢?

不妨试试用one-hot编码来代替ascii码?即,对某个单词w,按以下方式表示:
[0,0,,1,,0,0]|V| elements \overbrace{\left[ 0, 0, \dots, 1, \dots, 0, 0 \right]}^\text{|V| elements}
其中,每个单词都只含有1个’1’(剩余为0),而且每个单词的’1’的位置都不同。

上述方法不仅占用了巨量的空间,而且还有一个致命的弊端:这种方法将每个单词标记为独立的实体,认为他们之间没有关联。

我们真正想要的,是使用一些标记方法,来标记词与词之间的相似关系。难以理解?且看以下示例。

假设我们正在建立一个语言模型,假设有以下语句:

  • The mathematician ran to the store.
  • The physicist ran to the store.
  • The mathematician solved the open problem.

在我们的训练数据中。

现在,我们读取到了一个在训练数据中从未见过的语句:

  • The physicist solved the open problem.

我们的语言模型在这一语句上也许能表现不错,但如果能够将以下两个事实也考虑进去,相信将会更好:

  • 我们在训练语句中,发现mathematician以及physicist在句中是相同的成分,换句话说,他们有着一定的语义关系。
  • 在新语句中,虽然看到的是physicist,但是我们认为mathematician在句中的成分是和physicist一致的。

由此,推断出physicist在新语句中的拟合度很好。

这正是上述标记词与词之间的相似关系的真实含义:相似关系指的是语义上的相似,而不是指字符拼写上相似。

这一技术在训练数据和未知的数据之间建立了联系,能有效克服语言学(文本)数据的稀疏性。当然,这基于一个基本假设:出现在相似上下文之中的单词在语义上相关。这也被称为分布式假设


稠密词嵌入

Getting Dense Word Embeddings

如何获得词嵌入呢?换句话说,我们具体要如何将语义的相似性进行编码?

或许我们可以考虑一些语义属性。例如,is able to run属性:mathematicians 和 physicists 都能 “run”,因此二者在该属性上都标记为高分。再考虑一些属性,并且为这些属性对不同的单词进行打分。

如果将每个属性视为一个维度,那么我们能将每个单词表示为一个向量(词嵌入向量):
qmathematician=[2.3can run,9.4likes coffee,5.5majored in Physics,] q_\text{mathematician} = \left[ \overbrace{2.3}^\text{can run}, \overbrace{9.4}^\text{likes coffee}, \overbrace{-5.5}^\text{majored in Physics}, \dots \right]

qphysicist=[2.5can run,9.1likes coffee,6.4majored in Physics,] q_\text{physicist} = \left[ \overbrace{2.5}^\text{can run}, \overbrace{9.1}^\text{likes coffee}, \overbrace{6.4}^\text{majored in Physics}, \dots \right]

由此,我们得到了度量单词相似性的一个方法:
Similarity(physicist,mathematician)=qphysicistqmathematician \text{Similarity}(\text{physicist}, \text{mathematician}) = q_\text{physicist} \cdot q_\text{mathematician}
按照向量长度标准化后:
Similarity(physicist,mathematician)=qphysicistqmathematicianqphysicistqmathematician=cos(ϕ) \text{Similarity}(\text{physicist}, \text{mathematician}) = \frac{q_\text{physicist} \cdot q_\text{mathematician}} {\| q_\text{physicist} \| \| q_\text{mathematician} \|} = \cos (\phi)
其中,ϕ\phi 标记两个向量的夹角。这样一来,极度相似的单词(他们的词嵌入向量指向几乎相同的方向)的similarity值为1,极度不相似的单词(他们的词嵌入向量指向几乎相反的方向)的similarity值为-1,

现在回顾one-hot编码,可以将其看成词嵌入向量的一种特殊情况,每个单词的相似度为0,且为每个单词单独分配了一个唯一的语义属性。

同时,我们称新定义的向量是稠密的,也就是说这些向量的元素绝大多数不为0。

然而, 这些新定义的向量有一个大问题:我们能为相似性的度量,定义成千上万个不同的语义属性,那么到底如何为不同的属性打分呢?

深度学习的核心思想是使计算机利用神经网络自动学习特征的含义,而不是需要程序员来手动设计。那么,为何不将词嵌入作为模型参数,让词嵌入在训练过程中自动更新迭代呢?

这正是我们要做的:我们将设计一些神经网络在原则上能够进行学习的潜在语义属性。注意,词嵌入可能是难以解释甚至不可解释的。也就是说,虽然在上述例子中,我们能通过手工标记的方法知道mathematicians以及physicists都喜欢咖啡,但是如果我们用一个神经网络去学习这些词嵌入,并且发现mathematicians以及physicists的词嵌入在某个维度上都有着很高的分数,我们也不清楚这个维度到底意味着什么(即不明该维度的语义)。他们在潜在语义属性上相似,但这对我们来说不可解释。

总而言之,词嵌入表示的是单词语义,能够高效地编码与手头任务相关的语义信息。除了语义信息,还能够嵌入其他的信息:词性标签、语义分析树、甚至任意信息!特征嵌入的思想是这一领域的核心。


Pytorch中的词嵌入

Word Embeddings in Pytorch

(部分内容已省略)

在定义词嵌入时,我们需要为每一个单词定义一个索引,这些索引将会成为一个查询表中的键值。

换句话说,词嵌入将被存储为一个$|V|×D $的矩阵,D为词嵌入的维度。我们为某一单词定义的索引i即表示该单词在词嵌入矩阵中的第i行。下文中,从单词到其索引的映射将被命名为 word_to_ix

Pytorch中为词嵌入提供服务的模块是 torch.nn.Embeddingtorch.nn.Embedding接受两个参数:词汇表长度、词嵌入维度

# Author: Robert Guthrie
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)
word_to_ix = {"hello": 0, "world": 1}
embeds = nn.Embedding(2, 5)  # 2 words in vocab, 5 dimensional embeddings(词汇表长度为2,嵌入维度为5)
lookup_tensor = torch.tensor([word_to_ix["hello"]], dtype=torch.long)
hello_embed = embeds(lookup_tensor)
print(hello_embed)

Out:

tensor([[ 0.6614, 0.2669, 0.0617, 0.6213, -0.4519]],
grad_fn=)


示例:N-Gram 语言模型

An Example: N-Gram Language Modeling

回顾n-gram模型,给定一个单词序列 w,我们需要计算:
P(wiwi1,wi2,,win+1) P(w_i | w_{i-1}, w_{i-2}, \dots, w_{i-n+1} )
其中 wiw_i 是w中的第i个单词。

在本例中,我们将计算一些训练样本的损失函数然后使用反向传播来更新模型参数

CONTEXT_SIZE = 2
EMBEDDING_DIM = 10

# We will use Shakespeare Sonnet 2
# 使用莎士比亚的《十四行诗》2
test_sentence = """When forty winters shall besiege thy brow,
And dig deep trenches in thy beauty's field,
Thy youth's proud livery so gazed on now,
Will be a totter'd weed of small worth held:
Then being asked, where all thy beauty lies,
Where all the treasure of thy lusty days;
To say, within thine own deep sunken eyes,
Were an all-eating shame, and thriftless praise.
How much more praise deserv'd thy beauty's use,
If thou couldst answer 'This fair child of mine
Shall sum my count, and make my old excuse,'
Proving his beauty by succession thine!
This were to be new made when thou art old,
And see thy blood warm when thou feel'st it cold.""".split()
# we should tokenize the input, but we will ignore that for now
# 我们本应该需要标记输入文本,但此次我们忽略这一工作

# build a list of tuples.  Each tuple is ([ word_i-2, word_i-1 ], target word)
# 建立一个元组列表,每个元组是一个([ word_i-2, word_i-1 ], target word)
trigrams = [([test_sentence[i], test_sentence[i + 1]], test_sentence[i + 2])
            for i in range(len(test_sentence) - 2)]

# print the first 3, just so you can see what they look like
print(trigrams[:3])

Out:

[([‘When’, ‘forty’], ‘winters’), ([‘forty’, ‘winters’], ‘shall’), ([‘winters’, ‘shall’], ‘besiege’)]

vocab = set(test_sentence)
word_to_ix = {word: i for i, word in enumerate(vocab)}

class NGramLanguageModeler(nn.Module):
    def __init__(self, vocab_size, embedding_dim, context_size):
    super(NGramLanguageModeler, self).__init__()
    self.embeddings = nn.Embedding(vocab_size, embedding_dim)
    self.linear1 = nn.Linear(context_size * embedding_dim, 128)
    self.linear2 = nn.Linear(128, vocab_size)

def forward(self, inputs):
    embeds = self.embeddings(inputs).view((1, -1))
    out = F.relu(self.linear1(embeds))
    out = self.linear2(out)
    log_probs = F.log_softmax(out, dim=1)
    return log_probs


losses = []
loss_function = nn.NLLLoss()
model = NGramLanguageModeler(len(vocab), EMBEDDING_DIM, CONTEXT_SIZE)
optimizer = optim.SGD(model.parameters(), lr=0.001)

for epoch in range(10):
    total_loss = 0
    for context, target in trigrams:
        # Step 1. Prepare the inputs to be passed to the model (i.e, turn the words
        # into integer indices and wrap them in tensors)
        # 1. 送入模型前的准备工作(如:将单词转化为对应的索引,并张量化)
        context_idxs = torch.tensor([word_to_ix[w] for w in context], dtype=torch.long)

        # Step 2. Recall that torch *accumulates* gradients. Before passing in a
        # new instance, you need to zero out the gradients from the old instance
        # 2. Pytorch 能积累梯度,因此在传入新实例之前需要将旧梯度清零
        model.zero_grad()

        # Step 3. Run the forward pass, getting log probabilities over next words
        # 3. 前向传播,求下一个单词的对数概率
        log_probs = model(context_idxs)

        # Step 4. Compute your loss function. (Again, Torch wants the target
        # word wrapped in a tensor)
        # 4. 计算损失函数(Pytorch要求将目标单词转换为张量)
        loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))

        # Step 5. Do the backward pass and update the gradient
        # 5. 反向传播,更新梯度
        loss.backward()
        optimizer.step()

        # Get the Python number from a 1-element Tensor by calling tensor.item()
        # 使用tensor.item()方法来从单一元素张量中取得数据
		total_loss += loss.item()
	losses.append(total_loss)

print(losses)  # The loss decreased every iteration over the training data!(误差每次迭代都下降)

Out:

[518.5035681724548, 516.1092879772186, 513.7281634807587, 511.3589131832123, 509.00272035598755, 506.6583275794983, 504.32336044311523, 501.9988875389099, 499.68528985977173, 497.3804793357849]


练习:计算词嵌入:连续词袋模型

Exercise: Computing Word Embeddings: Continuous Bag-of-Words

连续词袋模型(CBOW)是自然语言处理中非常常用的一种语言模型。CBOW通过参考目标单词上下文几个单词来对目标单词进行预测。与语言建模不同的是,CBOW并非顺序模型,也不必一定是概率化的。CBOW通常被用来快速训练词嵌入,并且用这些训练得到的词嵌入来初始化那些更为复杂的词嵌入模型,通常这一过程也被称为预训练词嵌入。CBOW绝大多数情况下能够使模型表现提升数个百分点。

以下定义CBOW模型:

给定目标单词 wiw_i 以及左右两边大小为 N 的上下文窗口:wi1,,wiNw_{i-1},\dots, w_{i-N}wi+1,,wi+Nw_{i+1},\dots, w_{i+N},将上述这些上下文单词记做 cc ,CBOW模型尝试最小化以下内容:
logp(wiC)=logSoftmax(A(wCqw)+b) -\log p(w_i | C) = -\log \text{Softmax}(A(\sum_{w \in C} q_w) + b)
其中,qwq_w是单词w的词嵌入。

使用Pytorch,完善下面的类,实现这一模型。

提示:

  • 考虑需要定义哪些参数/变量
  • 明确每步操作要求的形状,需要变换张量形状时,使用.view()方法
CONTEXT_SIZE = 2  # 2 words to the left, 2 to the right(考虑目标单词左右2个单词)
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules
called a program. People create programs to direct processes. In effect,
we conjure the spirits of the computer with our spells.""".split()

# By deriving a set from `raw_text`, we deduplicate the array
#通过集合化,将元素唯一化
vocab = set(raw_text)
vocab_size = len(vocab)

word_to_ix = {word: i for i, word in enumerate(vocab)}
data = []
for i in range(2, len(raw_text) - 2):
    context = [raw_text[i - 2], raw_text[i - 1], raw_text[i + 1], raw_text[i + 2]]
    target = raw_text[i]
    data.append((context, target))
print(data[:5])

Out:

[([‘We’, ‘are’, ‘to’, ‘study’], ‘about’), ([‘are’, ‘about’, ‘study’, ‘the’], ‘to’), ([‘about’, ‘to’, ‘the’, ‘idea’], ‘study’), ([‘to’, ‘study’, ‘idea’, ‘of’], ‘the’), ([‘study’, ‘the’, ‘of’, ‘a’], ‘idea’)]

class CBOW(nn.Module):

    def __init__(self):
        pass
    
    def forward(self, inputs):
        pass
    
# create your model and train.  here are some functions to help you make
# the data ready for use by your module

def make_context_vector(context, word_to_ix):
    idxs = [word_to_ix[w] for w in context]
    return torch.tensor(idxs, dtype=torch.long)

make_context_vector(data[0][0], word_to_ix)  # example

该练习答案后续更新。


[Translation] 2020/4 Karl

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章