多分类问题处理为二分类问题,需要能够正确地对正例和负例进行分类。

如果以所有的负例为对象,词汇量将增加许多,无法处理。作为一种近似方法,将只使用少数负例。

负采样方法:求正例作为目标词时的损失,同时采样(选出)若干个负例,对这些负例求损失。然后,将正例和采样出来的负例的损失加起来,作为最终的损失。例子如下图所示。

二分类负采样方法-编程之家

负采样的采样方法:

抽取负例:让语料库中常出现的单词易被抽到,不常出现的单词难被抽到。

基于频率的采样方法:计算语料库中各个单词的出现次数,并将其表示为概率分布,然后使用这个概率分布对单词进行采样。

通过给np.random.choice函数参数p,指定表示概率分布的列表,将进行基于概率分布的采样。

import numpy as np
words = ['you', 'say', 'goodbye', 'I', 'hello', '.']
a = np.random.choice(words)
b = np.random.choice(words, size=5)
c = np.random.choice(words, size=5, replace=False)
p = [0.5, 0.1, 0.05, 0.2, 0.05, 0.1]
d = np.random.choice(words, p=p)
print(a)
print(b)
print(c)
print(d)

输出:

hello
['.' 'hello' 'hello' 'goodbye' 'goodbye']
['hello' '.' 'goodbye' 'you' 'I']
you

word2vec中的负采样:对原来的概率分布取0.75次方。分子表示第i个单词的概率。分母是变换后的概率分布的总和。(使变换后的概率总和仍为1),通过取0.75次方,低频单词的概率将稍微变高。作为一种补救措施,使得变换后低频单词比变换前抽到的几率增加。

二分类负采样方法-编程之家

以1个连续单词(unigram)为对象创建概率分布,进行负例抽取的函数如下。corpus是是单词ID列表;power是概率分布取的次方值;sample_size是负例的采样个数。

如果是bigram,则以(you,say)、(you,goodbye)这样的2个单词的组合为对象创建概率分布。

class UnigramSampler:def __init__(self, corpus, power, sample_size):self.sample_size = sample_sizeself.vocab_size = Noneself.word_p = Nonecounts = collections.Counter()for word_id in corpus:counts[word_id] += 1vocab_size = len(counts)self.vocab_size = vocab_sizeself.word_p = np.zeros(vocab_size)for i in range(vocab_size):self.word_p[i] = counts[i]self.word_p = np.power(self.word_p, power)self.word_p /= np.sum(self.word_p)def get_negative_sample(self, target):batch_size = target.shape[0]if not GPU:negative_sample = np.zeros((batch_size, self.sample_size), dtype=np.int32)for i in range(batch_size):p = self.word_p.copy()target_idx = target[i]p[target_idx] = 0p /= p.sum()negative_sample[i, :] = np.random.choice(self.vocab_size, size=self.sample_size, replace=False, p=p)else:# 在用GPU(cupy)计算时,优先速度# 有时目标词存在于负例中negative_sample = np.random.choice(self.vocab_size, size=(batch_size, self.sample_size),replace=True, p=self.word_p)return negative_sample

使用这个类:[1, 3, 0]这3个数据的mini-batch作为正例,对各个数据采样2个负例。

import numpy as np
from negative_sampling_layer import UnigramSampler
corpus = np.array([0, 1, 2, 3, 4, 1, 2, 3])
power = 0.75
sample_size = 2
sampler = UnigramSampler(corpus, power, sample_size)
target = np.array([1, 3, 0])
negative_sample = sampler.get_negative_sample(target)
print(negative_sample)

输出:可以看到每个数据的负例。

[[2 4][2 0][2 1]]

实现负采样层:

参数:输出侧权重W,单词ID列表corpus,概率分布的次方值power,负例的采样数sample_size, sampler保存UnigramSampler生成的采样负例。

loss_layers 和 embed_dot_layers 中以列表格式保存了必要的层,生成sample_size + 1 个层,意味着生成一个正例用的层和 sample_size 个负例用的层。loss_layers[0] 和 embed_dot_layers[0] 是处理正例的层。

正向传播:通过 Embedding Dot 层的 forward 输出得分,再将得分和标签一起输入 Sigmoid with Loss 层来计算损失和。

反向传播:以与正向传播相反的顺序调用各层的 backward() 函数,将多份梯度累加起来。

class NegativeSamplingLoss:def __init__(self, W, corpus, power=0.75, sample_size=5):self.sample_size = sample_sizeself.sampler = UnigramSampler(corpus, power, sample_size)self.loss_layers = [SigmoidWithLoss() for _ in range(sample_size + 1)]self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size + 1)]self.params, self.grads = [], []for layer in self.embed_dot_layers:self.params += layer.paramsself.grads += layer.gradsdef forward(self, h, target):batch_size = target.shape[0]negative_sample = self.sampler.get_negative_sample(target)# 正例的正向传播score = self.embed_dot_layers[0].forward(h, target)correct_label = np.ones(batch_size, dtype=np.int32)loss = self.loss_layers[0].forward(score, correct_label)# 负例的正向传播negative_label = np.zeros(batch_size, dtype=np.int32)for i in range(self.sample_size):negative_target = negative_sample[:, i]score = self.embed_dot_layers[1 + i].forward(h, negative_target)loss += self.loss_layers[1 + i].forward(score, negative_label)return lossdef backward(self, dout=1):dh = 0for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):dscore = l0.backward(dout)dh += l1.backward(dscore)return dh