当前位置:   article > 正文

自然语言处理中的负采样_相似性负采样策略

相似性负采样策略

word2vec出现的背景

  • 我们都知道,自然语言处理需要对文本进行编码,将语言中的词转化为向量的形式,计算机的世界里只有数字,所以这是一项必须要做的工作。
  • 有人可能会想,最常见的编码如one-hot编码,能不能用于自然语言处理对于文本的编码呢?答案是肯定的,当然能,但是效果不好,也许没人会这么做。
  • 为什么呢,因为文本几乎都具有相关性,构成文本的单词更是有相关性,它们之间应该能通过各自的词向量来表征它们之间的关系。我们知道余弦相似度可以用来表征两个向量的相似性,我们也可以用余弦相似度来表征词向量的相似性,而如果采用one-hot编码,则不管取哪两个词,结果都是0,也就是说,单身男子这个词和人类的相似度,等同于单身男子的相似度。
  • 所以独热编码是不适用于该领域的。所以人们需要构造出从单词到向量的转换。word2vec应运而生。

跳字模型(skip-gram)

  • 关于这些基础知识我不手写了,这里引用Dive-into-DL-PyTorch中的介绍:
    在这里插入图片描述
    在这里插入图片描述

连续词袋模型(CBOW)

  • 下面
    在这里插入图片描述
    在这里插入图片描述

小结

  • 可以看出,这些训练词向量的方法都是使用了内积相似度(即向量w*向量u) 来衡量两个词向量之间的相似程度,这是为什么呢?
  • 因为衡量两个向量的相似程度不能仅靠其夹角,还要考虑其长度,而内积运算正是如此:
    在这里插入图片描述
    所以,word2vec使用内积相似度是合理的。

负采样

  • 我们可以看到其实跳字模型和连续词袋模型是差不多的,它们有一个共同点,每次计算条件概率都要在分母统计所有词与中心词(或背景词)的向量内积,我们都知道词典是很大的, 而每次迭代计算条件概率都要进行这么庞大的内积计算,成本很大。当然计算之后根据loss来修改词向量的时候是对全部词向量进行修改。
  • 负采样就是来解决这个问题的。由于跳字模型和连续词袋模型类似,此处仅以跳字模型为例介绍这两种方法。负采样采用了一个背景窗口,原先不是更新所有词与中心词的向量内积嘛,现在不这样了,我们选取一个窗口大小的背景词填满窗口,然后每次就用这些背景词和中心词进行向量内积,然后呢根据loss更改参数也是只修改这些词和中心词的词向量,并不是全部修改。
  • 负采样修改了原来的目标函数。给定中心词 W c W_{c} Wc的一个背景窗口,我们把背景词 W o W_{o} Wo出现在该背景窗口看作一个事件,并将该事件的概率计算为
    在这里插入图片描述
    其中的σσ函数与sigmoid激活函数的定义相同:
    在这里插入图片描述
    我们先考虑最大化文本序列中所有该事件的联合概率来训练词向量。具体来说,给定一个长度为T的文本序列,设时间步t的词为 W t W^{t} Wt,且背景窗口大小为m,考虑最大化联合概率
    在这里插入图片描述
  • 使用sigmoid函数来根据内积结果生成对应的概率值是合理的,因为内积结果越大,概率值越高,即内积相似度越高,概率越高,这是合理的。
  • 然而上面的背景窗口只包含了真实的背景词,这导致当所有词向量相等且值为无穷大时,以上的联合概率才被最大化为1。很明显,这样的词向量毫无意义。负采样通过采样并添加负类样本使目标函数更有意义。
  • 其实我始终不太明白这里为什么一定要加入负样本,虽然确实联合概率很难达到1,但是这也是正常的,损失肯定是有的,如果有哪位小伙伴明白,可以评论告诉我。
  • 那现在看来不能只考虑背景词了,还要加入一些噪声词,很明显,背景词是正儿八经出现在中心词附近的,而噪声词是我们人为添加的,所以背景词是正类,噪声词是负类。每次更新词向量,就更新正类和负类以及中心词的词向量。这儿有点像跳词模型中的分母考虑所有词,而这里只考虑背景词和少量噪声词,我们设置噪声词的个数为超参数K。所谓的负采样,其实就是采样K个负类,即噪声词。
  • 那么条件概率就变为了:
    在这里插入图片描述

具体训练过程

  • 下面我具体来说,我们训练词向量的时候,一个中心词对应多个背景词,我们设置背景窗口的大小s,然后中心词在一句话中的前s个词和后s个词作为背景词,然后随机选取K个噪声词。
  • 然后我们需要统一尺寸,而由于中心词可能位于句子的前端或者后端,导致背景词数量并不一致,我们以所有中心词对应的最多的背景词+噪声词作为尺寸max_len,然后不满的补0,最终得到了[batch_size,max_len]的训练样本。
  • 然后我们要设置label,背景词的标签是1,噪声词是负类,标签是0。对于长度补充的也设置为0,意思是只让背景词和噪声词向量参与运算。
  • 还得设置是否是填充的,用mask来表示,对于填充的元素设置为0,背景词和噪声词都是1,之所以设置mask是因为在调用nn.functional.binary_cross_entropy_with_logits时需要设置填充的不参与运算。
  • 举个栗子:
class SigmoidBinaryCrossEntropyLoss(nn.Module):
    def __init__(self): # none mean sum
        super(SigmoidBinaryCrossEntropyLoss, self).__init__()
    def forward(self, inputs, targets, mask=None):
        """
        input – Tensor shape: (batch_size, len)
        target – Tensor of the same shape as input
        """
        inputs, targets, mask = inputs.float(), targets.float(), mask.float()
        res = nn.functional.binary_cross_entropy_with_logits(inputs, targets, reduction="none", weight=mask)
        return res.mean(dim=1)

loss = SigmoidBinaryCrossEntropyLoss()

pred = torch.tensor([[1.5, 0.3, -1, 2], [1.1, -0.6, 2.2, 0.4]])
# 标签变量label中的1和0分别代表背景词和噪声词
label = torch.tensor([[1, 0, 0, 0], [1, 1, 0, 0]])
mask = torch.tensor([[1, 1, 1, 1], [1, 1, 1, 0]])  # 掩码变量
loss(pred, label, mask) * mask.shape[1] / mask.float().sum(dim=1)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
输出tensor([0.8740, 1.2100])
  • 1

上面的运算过程就相当于:

def sigmd(x):
    return - math.log(1 / (1 + math.exp(-x)))

print('%.4f' % ((sigmd(1.5) + sigmd(-0.3) + sigmd(1) + sigmd(-2)) / 4)) # 注意1-sigmoid(x) = sigmoid(-x)
print('%.4f' % ((sigmd(1.1) + sigmd(-0.6) + sigmd(-2.2)) / 3))

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
输出
0.8740
1.2100
  • 1
  • 2
  • 3
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/1005768
推荐阅读
相关标签
  

闽ICP备14008679号