word2vec
WordEmbedding
对词汇进行多维度的描述,形成一个密集的矩阵。这样每两个词之间的相似性可以通过进行内积的大小体现出来。越大说明距离越远,则越不相似。
Analogies(类比):将男-女和国王-女王做类比,比如将表示‘男’的词向量与‘女’的词向量相减,同理国王和女王也相减,得出来的矩阵近乎相等,则说明这两个对比在某种维度上是相似的。
Cosine similarities: 求解相似度。给出男、女、国王,找到女王的索引。
初始化E,与one-hot矩阵相称,得到对应的embedding。将各个词(一个batch)的embedding做线性变换,且经过softmax后得到某个词的embedding,做交叉熵。再反向传播更新E。
![](https://img2018.cnblogs.com/blog/1813159/202002/1813159-20200225200735313-1776453916.png)
![](https://img2018.cnblogs.com/blog/1813159/202002/1813159-20200225201202583-1399183875.png)
数据处理
二次采样
文本数据中一般会出现一些高频词,如英文中的“the”“a”和“in”。通常来说,在一个背景窗口中,一个词(如“chip”)和较低频词(如“microprocessor”)同时出现比和较高频词(如“the”)同时出现对训练词嵌入模型更有益。因此,训练词嵌入模型时可以对词进行二次采样 [2]。
具体来说,数据集中每个被索引词(w_i)将有一定概率被丢弃,该丢弃概率为
其中 (f(w_i)) 是数据集中,词(w_i)的个数与总词数之比,常数(t)是一个超参数(实验中设为(10^{-4}))。可见,只有当(f(w_i) > t)时,我们才有可能在二次采样中丢弃词(w_i),并且越高频的词被丢弃的概率越大,低频词会被完整的保留下来。
提取中心词和背景词
将与中心词距离不超过背景窗口大小的词作为它的背景词。下面定义函数提取出所有中心词和它们的背景词。它每次在整数1和max_window_size
(最大背景窗口)之间随机均匀采样一个整数作为背景窗口大小。
def get_centers_and_contexts(dataset, max_window_size):
centers, contexts = [], []
for st in dataset:
if len(st) < 2: # 每个句子至少要有2个词才可能组成一对“中心词-背景词”
continue
centers += st
for center_i in range(len(st)):
window_size = random.randint(1, max_window_size)
indices = list(range(max(0, center_i - window_size),
min(len(st), center_i + 1 + window_size)))
indices.remove(center_i) # 将中心词排除在背景词之外
contexts.append([st[idx] for idx in indices])
return centers, contexts
下面我们创建一个人工数据集,其中含有词数分别为7和3的两个句子。设最大背景窗口为2,打印所有中心词和它们的背景词。
tiny_dataset = [list(range(7)), list(range(7, 10))]
print('dataset', tiny_dataset)
for center, context in zip(*get_centers_and_contexts(tiny_dataset, 2)):
print('center', center, 'has contexts', context)
输出:
dataset [[0, 1, 2, 3, 4, 5, 6], [7, 8, 9]]
center 0 has contexts [1, 2]
center 1 has contexts [0, 2, 3]
center 2 has contexts [1, 3]
#...
负采样
我们使用负采样来进行近似训练。对于一对中心词和背景词,我们随机采样(K)个噪声词(实验中设(K=5))。根据word2vec论文的建议,噪声词采样概率(P(w))设为(w)词频与总词频之比的0.75次方 [2]。
choices(population, weights=None, *, cum_weights=None, k=1):从population中进行K次随机选取,每次选取一个元素(注意会出现同一个元素多次被选中的情况),weights是相对权重值,population中有几个元素就要有相对应的weights值,cum_weights是累加权重值,例如,相对权重〔10, 5, 30,5〕相当于累积权重〔10, 15, 45,50〕。在内部,在进行选择之前,相对权重被转换为累积权重,因此提供累积权重节省了工作。返回一个列表。
这部分的populiation我一直有点疑问。不过要是想解释得通的话,大概就是:在该数据集中,没有重复的word序列长度为9000多,同时我们已经把它key-value相对应好了,并且sub下存储的是二次采样后的数据集的所对应word的序列。
所以population取值味sampling_weights的长度,而sampling_weights也代表了每个词出现的相对频率。
choices函数,根据相对概率,从population中抽取一个值,总共抽取100,000次,肯定会有重复的数字。
假设矩阵第一行,长度为5,所以本行的噪声词为25个。i = 25
然后来到下一行,长度还是为5,就再接着读取25个。结束时 i = 50。
之后以此类推。。
如果候选词全都被选择完了了,那么重新再生成1e5个。
def get_negatives(all_contexts, sampling_weights, K):
all_negatives, neg_candidates, i = [], [], 0
population = list(range(len(sampling_weights)))
# 1 ~ 9000多
for contexts in all_contexts:
negatives = []
#噪声词是本层长度的5倍!!这不是废话吗!
#一个中心词会有多个配对的背景词,每个中心词选择5个背景词,那肯定就是5倍咯!
while len(negatives) < len(contexts) * K:
#候选词不够了就补充
if i == len(neg_candidates):
# 根据每个词的权重(sampling_weights)随机生成k个词的索引作为噪声词。
# 为了高效计算,可以将k设得稍大一点
# candidate:选择100,000个词作为其候选的上下文。 干扰因素 相当于分类问题 0
i, neg_candidates = 0, random.choices(
population, sampling_weights, k=int(1e5))
#然后在nevigate个数不满足的情况下,从candidate中挑选前T个。最后再加入
neg, i = neg_candidates[i], i + 1
# 噪声词不能是背景词
if neg not in set(contexts):
negatives.append(neg)
#最后再加入所有噪声词中,此时len(all_contexts) 对应了 len(all_contexts) * 5
#即每个中心词对应5个噪声词。
all_negatives.append(negatives)
return all_negatives
#该词词频 ** 0.75
sampling_weights = [counter[w]**0.75 for w in idx_to_token]
all_negatives = get_negatives(all_contexts, sampling_weights, 5)
跳字模型
嵌入层
获取词嵌入的层称为嵌入层,在PyTorch中可以通过创建nn.Embedding
实例得到。嵌入层的权重是一个矩阵,其行数为词典大小(num_embeddings
),列数为每个词向量的维度(embedding_dim
)。我们设词典大小为20,词向量的维度为4。
如果每次取一个batch进行处理的话,那么词典大小也就相当于batchsize了
embed = nn.Embedding(num_embeddings=20, embedding_dim=4)
嵌入层的输入为词的索引。输入一个词的索引(i),嵌入层返回权重矩阵的第(i)行作为它的词向量。下面我们将形状为(2, 3)的索引输入进嵌入层,由于词向量的维度为4,我们得到形状为(2, 3, 4)的词向量。
x = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.long)
embed(x)
跳字模型的前向计算
在前向计算中,跳字模型的输入包含中心词索引center
以及连结的背景词与噪声词索。引contexts_and_negatives
。其中center
变量的形状为(批量大小, 1, 1),而contexts_and_negatives
变量的形状为(批量大小, max_len
, 1)。这两个变量先通过词嵌入层分别由词索引变换为词向量,再通过小批量乘法得到形状为(批量大小, 1, max_len
)的输出。输出中的每个元素是中心词向量与背景词向量或噪声词向量的内积。
想了我好久一直没明白它们是干嘛使的。好像有点懂了。
先把center转化为词向量,它在转化过程中的参数是随机的,所以尽管它是个300维的向量,结果可能和我们期望的vector表示有很大的区别,所以需要在backpropagation中优化W参数。
与此同时,也要把背景词和噪声词的组合转化为词向量,同样也是300维的。求得两者的内积。
def skip_gram(center, contexts_and_negatives, embed_v, embed_u):
v = embed_v(center)
u = embed_u(contexts_and_negatives)
#permute交换维度
pred = torch.bmm(v, u.permute(0, 2, 1))
return pred
损失函数的定义
我们可以通过掩码变量指定小批量中参与损失函数计算的部分预测值和标签:当掩码为1时,相应位置的预测值和标签将参与损失函数的计算;当掩码为0时,相应位置的预测值和标签则不参与损失函数的计算。掩码变量可用于避免填充项对损失函数计算的影响。
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()
应用词嵌入模型
torch.topk()
torch.topk(input, k, dim=None, largest=True, sorted=True, out=None) -> (Tensor, LongTensor)
沿给定dim维度返回输入张量input中 k 个最大值。
如果不指定dim,则默认为input的最后一维。
如果为largest为 False ,则返回最小的 k 个值。
返回一个元组 (values,indices),其中indices是原始输入张量input中测元素下标。
如果设定布尔值sorted 为_True_,将会确保返回的 k 个值被排序。
参数:
input (Tensor) – 输入张量
k (int) – “top-k”中的k
dim (int, optional) – 排序的维
largest (bool, optional) – 布尔值,控制返回最大或最小值
sorted (bool, optional) – 布尔值,控制返回值是否排序
out (tuple, optional) – 可选输出张量 (Tensor, LongTensor) output buffer