word2vec是早期NLP的必要预处理过程,其用于生成词的向量表示(embeding)。
其将单词映射为固定长度的向量(embeding向量),而通过向量表示可以更好地表达不同词之间的相关性,因此会使得后续的分类、生成等NLP任务更好地学习训练。word2vec描述不同词之间的相关性,主要是指词同其上下文的其他词的共现性,主要有两种范式:
- 跳元模型Skip-gram:其是假设通过中心词生成其上下文,因此其目标是在中心词下,其上下文的条件概率最大,即如下优化式子,C表示中心词的数量,k表示上下文窗口数。
- 连续词袋CBOW:其是假设通过上下文生成中心词,因此其目标在上下文下,其中心词生成条件概率最大,即如下优化式子:
本文重点介绍跳元模型Skip-gram,为了求解上述式子,将上式求log转换为最小化下式:
其中上式中的词与词间的联合分布可以由词向量相似度衡量,word2vec为了方便计算,通过exp形式进行度量:
上式中的表示词向量,word2vec就是通过Embeding模块实现由单词到词向量的转换,从而上面的Loss最小化。Embeding模块实际上类似由于一个全连接网络层,其输入是N维的one-hot向量(N是指全量词的个数),输出是L维的向量(L是词向量的长度),其参数量总共为N*L。
word2vec主要是为求解上述Embeding模块的权重参数,其组成了中心词c的词向量,可以求其偏导数如下:
以下我们通过paddle代码实现word2vec网络结构的定义:
class Word2Vec(nn.Layer):
def __init__(self, num_embeddings, embedding_dim):
super(Word2Vec, self).__init__()
self.embed = nn.Embedding(num_embeddings, embedding_dim,
weight_attr=paddle.ParamAttr(
name="center_embed",
initializer=paddle.nn.initializer.XavierUniform()))
# 执行前向计算
def forward(self, center, contexts_and_negatives=None):
"""Skip-Gram"""
v = self.embed(center)
if contexts_and_negatives is None:
return v
u = self.embed(contexts_and_negatives)
pred = paddle.squeeze(paddle.bmm(v, u.transpose(perm=[0, 2, 1])), axis=1)
return pred
上述定义中的pred用于表示来描述两个词向量相乘项。
在训练时,我们将原来Loss转换为批量进行训练,另外的求解由于涉及到softmax计算,计算相对困难,因此一种我们将简化的方式进行训练(负采样)。
其首先定义共同出现时,定义词u在中心词窗口k内的概率为:
同理不在中心词窗口k内的概率为:
此时条件概率可以表示为:
此时batch内的loss可以表示为:
其中k表示正例的窗口大小,h表示负例数(即不在上下文窗口的词),上述loss函数可以用binary_cross_entropy_with_logits损失函数表示:
其中中label表示词是正或负例,logits即为,因此我们可以设计如下的损失函数代码
class SigmoidBCELoss(nn.Layer):
# 带掩码的二元交叉熵损失
def __init__(self):
super().__init__()
def forward(self, inputs, label, mask):
out = nn.functional.binary_cross_entropy_with_logits(
logit=inputs, label=label, weight=mask, reduction="none")
return out.mean(axis=1)
整体的paddle训练代码如下:
# 中心词
center_spec = paddle.static.InputSpec([None, 1], 'int64', 'center')
# 上下文正例词及负例词
context_spec = paddle.static.InputSpec([None, max_context_len], 'int64', 'contexts_and_negatives')
# 正例及负例的标识
label_spec = paddle.static.InputSpec([None, max_context_len], 'float32', 'label')
# mask,正例及负例以外的填充为0不参与训练
mask_spec = paddle.static.InputSpec([None, max_context_len], 'float32', 'mask')
model = paddle.Model(Word2Vec(num_embeddings, embedding_dim), [center_spec, context_spec], [label_spec, mask_spec])
model.prepare(
optimizer=paddle.optimizer.Adam(learning_rate=learning_rate, parameters=model.parameters()),
loss=SigmoidBCELoss()
)
model.fit(
train_dataset,
valid_dataset,
batch_size=batch_size,
epochs=num_epochs,
eval_freq=1,
shuffle=True,
save_dir=save_model_dir,
callbacks=[loss_print, vdl_record]
)
全局向量的词嵌入(GloVe)
GloVe主要在原来loss函数中引入了两点特性:
- 引入全局共现权重,表示词u和词c共现的次数,此时全局的损失函数可以表示为:
- 重新定义条件概率的计算,条件概率实际表示为,假设,此时学习目标为:
- 此时GloVe的损失函数定义为: