词嵌入基础

循环神经网络的从零开始实现中使用 one-hot 向量表示单词,虽然它们构造起来很容易,但通常并不是一个好选择。

原因:

one-hot 词向量无法准确表达不同词之间的相似度,如我们常常使用的余弦相似度

任意单词间的余弦相似度都为零。

Word2Vec 词嵌入工具的提出正是为了解决上面这个问题,它将每个词表示成一个定长的向量,并通过在语料库上的预训练使得这些向量能较好地表达不同词之间的相似和类比关系,以引入一定的语义信息。

基于两种概率模型的假设,介绍以下两种 Word2Vec 模型:

  1. Skip-Gram 跳字模型:假设背景词由中心词生成,即建模 P ( w o ∣ w c ) P(w_o\mid w_c) P(wowc),其中 w c w_c wc 为中心词, w o w_o wo 为任一背景词;
  1. CBOW (continuous bag-of-words) 连续词袋模型:假设中心词由背景词生成,即建模 P ( w c ∣ W o ) P(w_c\mid \mathcal{W}_o) P(wcWo),其中 W o \mathcal{W}_o Wo 为背景词的集合。

在这里我们主要介绍 Skip-Gram 模型的实现,CBOW 实现与其类似,读者可之后自己尝试实现。后续的内容将大致从以下四个部分展开:

  1. PTB 数据集
  2. Skip-Gram 跳字模型
  3. 负采样近似
  4. 训练模型
import collections
import math
import random
import sys
import time
import os
import numpy as np
import torch
from torch import nn
import torch.utils.data as Data

PTB 数据集

简单来说,Word2Vec 能从语料中学到如何将离散的词映射为连续空间中的向量,并保留其语义上的相似关系。

为了训练 Word2Vec 模型,我们就需要准备一个自然语言语料库,模型将从中学习各个单词间的关系,这里我们使用经典的 PTB 语料库进行训练。

载入数据集

数据集训练文件 ptb.train.txt 示例:

aer banknote berlitz calloway centrust cluett fromstein gitano guterman ...
pierre  N years old will join the board as a nonexecutive director nov. N
mr.  is chairman of  n.v. the dutch publishing group
...
with open('path to ptb.train.txt', 'r') as f:lines = f.readlines() # 该数据集中句子以换行符为分割raw_dataset = [st.split() for st in lines] # st是sentence的缩写,单词以空格为分割
print('# sentences: %d' % len(raw_dataset))# 对于数据集的前3个句子,打印每个句子的词数和前5个词
# 句尾符为 '' ,生僻词全用 '' 表示,数字则被替换成了 'N'
for st in raw_dataset[:3]:print('# tokens:', len(st), st[:5])
Result:
# sentences: 42068
# tokens: 24 ['aer', 'banknote', 'berlitz', 'calloway', 'centrust']
# tokens: 15 ['pierre', '<unk>', 'N', 'years', 'old']
# tokens: 11 ['mr.', '<unk>', 'is', 'chairman', 'of']

建立词语索引

counter = collections.Counter([tk for st in raw_dataset for tk in st]) # tk是token的缩写
counter = dict(filter(lambda x: x[1] >= 5, counter.items())) # 只保留在数据集中至少出现5次的词idx_to_token = [tk for tk, _ in counter.items()]
token_to_idx = {tk: idx for idx, tk in enumerate(idx_to_token)}
dataset = [[token_to_idx[tk] for tk in st if tk in token_to_idx]for st in raw_dataset] # raw_dataset中的单词在这一步被转换为对应的idx
num_tokens = sum([len(st) for st in dataset])
'# tokens: %d' % num_tokens

二次采样

文本数据中一般会出现一些高频词,如英文中的“the”“a”和“in”。通常来说

在一个背景窗口中,一个词(如“chip”)和较低频词(如“microprocessor”)同时出现比和较高频词(如“the”)同时出现对训练词嵌入模型更有益。

因此,训练词嵌入模型时可以对词进行二次采样。 具体来说,数据集中每个被索引词 w i w_i wi 将有一定概率被丢弃,该丢弃概率为

P ( w i ) = max ⁡ ( 1 − t f ( w i ) , 0 ) P(w_i)=\max(1-\sqrt{\frac{t}{f(w_i)}},0) P(wi)=max(1f(wi)t

,0)

其中 f ( w i ) f(w_i) f(wi) 是数据集中词 w i w_i wi 的个数与总词数之比,常数 t t t 是一个超参数(实验中设为 1 0 − 4 10^{−4} 104)。

可见,只有当 f ( w i ) > t f(w_i)>t f(wi)>t 时,我们才有可能在二次采样中丢弃词 w i w_i wi,并且越高频的词被丢弃的概率越大。具体的代码如下:

def discard(idx):'''@params:idx: 单词的下标@return: True/False 表示是否丢弃该单词'''return random.uniform(0, 1) < 1 - math.sqrt(1e-4 / counter[idx_to_token[idx]] * num_tokens)subsampled_dataset = [[tk for tk in st if not discard(tk)] for st in dataset]
print('# tokens: %d' % sum([len(st) for st in subsampled_dataset]))def compare_counts(token):return '# %s: before=%d, after=%d' % (token, sum([st.count(token_to_idx[token]) for st in dataset]), sum([st.count(token_to_idx[token]) for st in subsampled_dataset]))print(compare_counts('the'))
print(compare_counts('join'))

提取中心词和背景词

def get_centers_and_contexts(dataset, max_window_size):'''@params:dataset: 数据集为句子的集合,每个句子则为单词的集合,此时单词已经被转换为相应数字下标max_window_size: 背景词的词窗大小的最大值@return:centers: 中心词的集合contexts: 背景词窗的集合,与中心词对应,每个背景词窗则为背景词的集合'''centers, contexts = [], []for st in dataset:if len(st) < 2:  # 每个句子至少要有2个词才可能组成一对“中心词-背景词”continuecenters += stfor 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, contextsall_centers, all_contexts = get_centers_and_contexts(subsampled_dataset, 5)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)

注:数据批量读取的实现需要依赖负采样近似的实现,故放于负采样近似部分进行讲解。

Skip-Gram 跳字模型

在跳字模型中,每个词被表示成两个 d d d 维向量,用来计算条件概率。

假设这个词在词典中索引为 i i i ,当它为中心词时向量表示为 v i ∈ R d \boldsymbol{v}_i\in\mathbb{R}^d viRd,而为背景词时向量表示为 u i ∈ R d \boldsymbol{u}_i\in\mathbb{R}^d uiRd 。设中心词 w c w_c wc 在词典中索引为 c c c,背景词 w o w_o wo 在词典中索引为 o o o

我们假设给定中心词生成背景词的条件概率满足下式:

P ( w o ∣ w c ) = exp ⁡ ( u o ⊤ v c ) ∑ i ∈ V exp ⁡ ( u i ⊤ v c ) P(w_o\mid w_c)=\frac{\exp(\boldsymbol{u}_o^\top \boldsymbol{v}_c)}{\sum_{i\in\mathcal{V}}\exp(\boldsymbol{u}_i^\top \boldsymbol{v}_c)} P(wowc)=iVexp(uivc)exp(uovc)

PyTorch 预置的 Embedding 层

embed = nn.Embedding(num_embeddings=10, embedding_dim=4)
print(embed.weight)x = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.long)
print(embed(x))

PyTorch 预置的批量乘法

X = torch.ones((2, 1, 4))
Y = torch.ones((2, 4, 6))
print(torch.bmm(X, Y).shape)

Skip-Gram 模型的前向计算

def skip_gram(center, contexts_and_negatives, embed_v, embed_u):'''@params:center: 中心词下标,形状为 (n, 1) 的整数张量contexts_and_negatives: 背景词和噪音词下标,形状为 (n, m) 的整数张量embed_v: 中心词的 embedding 层embed_u: 背景词的 embedding 层@return:pred: 中心词与背景词(或噪音词)的内积,之后可用于计算概率 p(w_o|w_c)'''v = embed_v(center) # shape of (n, 1, d)u = embed_u(contexts_and_negatives) # shape of (n, m, d)pred = torch.bmm(v, u.permute(0, 2, 1)) # bmm((n, 1, d), (n, d, m)) => shape of (n, 1, m)return pred

负采样近似

问题:

    由于 softmax 运算考虑了背景词可能是词典 V \mathcal{V} V 中的任一词,对于含几十万或上百万词的较大词典,就可能导致计算的开销过大。

我们将以 skip-gram 模型为例,介绍负采样 (negative sampling) 的实现来尝试解决这个问题。

负采样方法用以下公式来近似条件概率 P ( w o ∣ w c ) = exp ⁡ ( u o ⊤ v c ) ∑ i ∈ V exp ⁡ ( u i ⊤ v c ) P(w_o\mid w_c)=\frac{\exp(\boldsymbol{u}_o^\top \boldsymbol{v}_c)}{\sum_{i\in\mathcal{V}}\exp(\boldsymbol{u}_i^\top \boldsymbol{v}_c)} P(wowc)=iVexp(uivc)exp(uovc)

P ( w o ∣ w c ) = P ( D = 1 ∣ w c , w o ) ∏ k = 1 , w k ∼ P ( w ) K P ( D = 0 ∣ w c , w k ) P(w_o\mid w_c)=P(D=1\mid w_c,w_o)\prod_{k=1,w_k\sim P(w)}^K P(D=0\mid w_c,w_k) P(wowc)=P(D=1wc,wo)k=1,wkP(w)KP(D=0wc,wk)

其中 P ( D = 1 ∣ w c , w o ) = σ ( u o ⊤ v c ) P(D=1\mid w_c,w_o)=\sigma(\boldsymbol{u}_o^\top\boldsymbol{v}_c) P(D=1wc,wo)=σ(uovc)σ ( ⋅ ) \sigma(\cdot) σ() 为 sigmoid 函数。对于一对中心词和背景词,我们从词典中随机采样 K K K 个噪声词(实验中设 K = 5 K=5 K=5)。


根据 Word2Vec 论文的建议,噪声词采样概率 P ( w ) P(w) P(w) 设为 w w w 词频与总词频之比的 0.75 0.75 0.75 次方。


def get_negatives(all_contexts, sampling_weights, K):'''@params:all_contexts: [[w_o1, w_o2, ...], [...], ... ]sampling_weights: 每个单词的噪声词采样概率K: 随机采样个数@return:all_negatives: [[w_n1, w_n2, ...], [...], ...]'''all_negatives, neg_candidates, i = [], [], 0population = list(range(len(sampling_weights)))for contexts in all_contexts:negatives = []while len(negatives) < len(contexts) * K:if i == len(neg_candidates):# 根据每个词的权重(sampling_weights)随机生成k个词的索引作为噪声词。# 为了高效计算,可以将k设得稍大一点i, neg_candidates = 0, random.choices(population, sampling_weights, k=int(1e5))neg, i = neg_candidates[i], i + 1# 噪声词不能是背景词if neg not in set(contexts):negatives.append(neg)all_negatives.append(negatives)return all_negativessampling_weights = [counter[w]**0.75 for w in idx_to_token]
all_negatives = get_negatives(all_contexts, sampling_weights, 5)

*注:除负采样方法外,还有层序 softmax (hiererarchical softmax) 方法也可以用来解决计算量过大的问题

批量读取数据

class MyDataset(torch.utils.data.Dataset):def __init__(self, centers, contexts, negatives):assert len(centers) == len(contexts) == len(negatives)self.centers = centersself.contexts = contextsself.negatives = negativesdef __getitem__(self, index):return (self.centers[index], self.contexts[index], self.negatives[index])def __len__(self):return len(self.centers)def batchify(data):'''用作DataLoader的参数collate_fn@params:data: 长为batch_size的列表,列表中的每个元素都是__getitem__得到的结果@outputs:batch: 批量化后得到 (centers, contexts_negatives, masks, labels) 元组centers: 中心词下标,形状为 (n, 1) 的整数张量contexts_negatives: 背景词和噪声词的下标,形状为 (n, m) 的整数张量masks: 与补齐相对应的掩码,形状为 (n, m) 的0/1整数张量labels: 指示中心词的标签,形状为 (n, m) 的0/1整数张量'''max_len = max(len(c) + len(n) for _, c, n in data)centers, contexts_negatives, masks, labels = [], [], [], []for center, context, negative in data:cur_len = len(context) + len(negative)centers += [center]contexts_negatives += [context + negative + [0] * (max_len - cur_len)]masks += [[1] * cur_len + [0] * (max_len - cur_len)] # 使用掩码变量mask来避免填充项对损失函数计算的影响labels += [[1] * len(context) + [0] * (max_len - len(context))]batch = (torch.tensor(centers).view(-1, 1), torch.tensor(contexts_negatives),torch.tensor(masks), torch.tensor(labels))return batchbatch_size = 512
num_workers = 0 if sys.platform.startswith('win32') else 4dataset = MyDataset(all_centers, all_contexts, all_negatives)
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True,collate_fn=batchify, num_workers=num_workers)
for batch in data_iter:for name, data in zip(['centers', 'contexts_negatives', 'masks','labels'], batch):print(name, 'shape:', data.shape)break

训练模型

损失函数

应用负采样方法后,我们可利用最大似然估计的对数等价形式将损失函数定义为如下

∑ t = 1 T ∑ − m ≤ j ≤ m , j ≠ 0 [ − log ⁡ P ( D = 1 ∣ w ( t ) , w ( t + j ) ) − ∑ k = 1 , w k ∼ P ( w ) K log ⁡ P ( D = 0 ∣ w ( t ) , w k ) ] \sum_{t=1}^T\sum_{-m\le j\le m,j\ne 0} [-\log P(D=1\mid w^{(t)},w^{(t+j)})-\sum_{k=1,w_k\sim P(w)^K}\log P(D=0\mid w^{(t)},w_k)] t=1Tmjm,j=0[logP(D=1w(t),w(t+j))k=1,wkP(w)KlogP(D=0w(t),wk)]

根据这个损失函数的定义,我们可以直接使用二元交叉熵损失函数进行计算:

class SigmoidBinaryCrossEntropyLoss(nn.Module):def __init__(self):super(SigmoidBinaryCrossEntropyLoss, self).__init__()def forward(self, inputs, targets, mask=None):'''@params:inputs: 经过sigmoid层后为预测D=1的概率targets: 0/1向量,1代表背景词,0代表噪音词@return:res: 平均到每个label的loss'''inputs, targets, mask = inputs.float(), targets.float(), mask.float()res = nn.functional.binary_cross_entropy_with_logits(inputs, targets, reduction="none", weight=mask)res = res.sum(dim=1) / mask.float().sum(dim=1)return resloss = SigmoidBinaryCrossEntropyLoss()pred = torch.tensor([[1.5, 0.3, -1, 2], [1.1, -0.6, 2.2, 0.4]])
label = torch.tensor([[1, 0, 0, 0], [1, 1, 0, 0]]) # 标签变量label中的1和0分别代表背景词和噪声词
mask = torch.tensor([[1, 1, 1, 1], [1, 1, 1, 0]])  # 掩码变量
print(loss(pred, label, mask))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))

模型初始化

embed_size = 100
net = nn.Sequential(nn.Embedding(num_embeddings=len(idx_to_token), embedding_dim=embed_size),nn.Embedding(num_embeddings=len(idx_to_token), embedding_dim=embed_size))

训练模型

def train(net, lr, num_epochs):device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')print("train on", device)net = net.to(device)optimizer = torch.optim.Adam(net.parameters(), lr=lr)for epoch in range(num_epochs):start, l_sum, n = time.time(), 0.0, 0for batch in data_iter:center, context_negative, mask, label = [d.to(device) for d in batch]pred = skip_gram(center, context_negative, net[0], net[1])l = loss(pred.view(label.shape), label, mask).mean() # 一个batch的平均lossoptimizer.zero_grad()l.backward()optimizer.step()l_sum += l.cpu().item()n += 1print('epoch %d, loss %.2f, time %.2fs'% (epoch + 1, l_sum / n, time.time() - start))train(net, 0.01, 5)

*注:最好在GPU上运行

测试模型

def get_similar_tokens(query_token, k, embed):'''@params:query_token: 给定的词语k: 近义词的个数embed: 预训练词向量'''W = embed.weight.datax = W[token_to_idx[query_token]]# 添加的1e-9是为了数值稳定性cos = torch.matmul(W, x) / (torch.sum(W * W, dim=1) * torch.sum(x * x) + 1e-9).sqrt()_, topk = torch.topk(cos, k=k+1)topk = topk.cpu().numpy()for i in topk[1:]:  # 除去输入词print('cosine sim=%.3f: %s' % (cos[i], (idx_to_token[i])))get_similar_tokens('chip', 3, net[0])

词嵌入之 Word2Vec相关推荐

  1. 深度学习-词嵌入(word2vec)

    词嵌入(word2vec) 自然语言是一套用来表达含义的复杂系统.在这套系统中,词是表义的基本单元.顾名思义,词向量是用来表示词的向量,也可被认为是词的特征向量或表征.把词映射为实数域向量的技术也叫词 ...

  2. 词嵌入之Word2vec

    one-hot向量的局限性 假设词典大小为N(词典中不同词的数量),每个词可以和从0到N−1的连续整数索引一一对应.使用one-hot方式来表示单词时,词向量维度大小为整个词汇表的大小,改词索引位置为 ...

  3. 【NLP】图解词嵌入和Word2vec

    0.导语 词嵌入是自然语言处理(NLP)中语言模型与表征学习技术的统称.概念上而言,它是指把一个维数为所有词的数量的高维空间嵌入到一个维数低得多的连续向量空间中,每个单词或词组被映射为实数域上的向量. ...

  4. 深度学习:词嵌入之word2vec

    http://blog.csdn.net/pipisorry/article/details/76147604 word2vec简介 深度学习在自然语言处理中第一个应用:训练词嵌入.Google 的 ...

  5. 词嵌入(WORD2VEC)

    原创:李孟启 1.前言 ⾃然语⾔是⼀套⽤来表达含义的复杂系统.在这套系统中,词是表义的基本单元.顾名思义,词向量是⽤来表示词的向量,也可被认为是词的特征向量或表征.把词映射为实数域向量的技术也叫词嵌⼊ ...

  6. 【NLP】词嵌入基础和Word2vec

    0.导语 词嵌入是自然语言处理(NLP)中语言模型与表征学习技术的统称.概念上而言,它是指把一个维数为所有词的数量的高维空间嵌入到一个维数低得多的连续向量空间中,每个单词或词组被映射为实数域上的向量. ...

  7. AI基础:词嵌入基础和Word2vec

    0.导语 词嵌入是自然语言处理(NLP)中语言模型与表征学习技术的统称.概念上而言,它是指把一个维数为所有词的数量的高维空间嵌入到一个维数低得多的连续向量空间中,每个单词或词组被映射为实数域上的向量. ...

  8. 图解词嵌入、语言模型、Word2Vec

    嵌入(embedding)是机器学习中最迷人的想法之一. 如果你曾经使用Siri.Google Assistant.Alexa.Google翻译,甚至智能手机键盘进行下一词预测,那么你很有可能从这个已 ...

  9. 吴恩达老师深度学习视频课笔记:自然语言处理与词嵌入

    Word representation:词嵌入(word embedding),是语言表示的一种方式,可以让算法自动理解一些类似的词比如男人.女人,国王.王后等.通过词嵌入的概念,即使你的模型标记的训 ...

最新文章

  1. TensorFlow入门教程
  2. 补天白帽大会:无处不在的无线电攻击
  3. Java程序设计学习笔记(一)
  4. ASP.NET2.0 - ASP.net MVC和ASP.net Web Forms
  5. P3605 [USACO17JAN]Promotion Counting P(树状数组)
  6. VisualStudio卸载后无法再次安装的解决方法
  7. la4080 Warfare And Logistics 罗列+最短
  8. 服装尺寸 html,女装标准尺码对照表,服装尺寸对照表,衣服尺码对照表
  9. uniapp换行符号_第15讲 : uni-app 组件 - 地图组件
  10. oracle mysql 同义词_Oracle中的同义词SYNONYM
  11. 程序人生-哈工大计算机系统大作业2022春
  12. 省协湖北中心 计算机科学与技术,国家知识产权局专利局专利审查协作湖北中心...
  13. 安卓桌面壁纸_让安卓实现 “非线性” 动画壁纸,有iOS内味了
  14. [转载]用J2ME开发企业级无线应用
  15. android 开放聊天室
  16. RFID技术在通道门禁系统的应用
  17. 基于 Win10 平台 搭建 Elasticsearch 集群
  18. 数字电子与微型计算机原理课后答案,数字电子与微型计算机原理(非电类)
  19. stm dfu_如何在Windows上修复dfu-util,STM,WinUSB,Zadig,Bootloader和其他固件刷新问题
  20. python实现给图片添加高斯噪声

热门文章

  1. c语言输出空格问题。
  2. 详解Java四大作用域
  3. 【前端管理】与平台团队一起铺设前端基础
  4. 计算机病毒的4个主要,计算机病毒的危害包含4个方面_第一个计算机病毒_检测计算机病毒的方法...
  5. 简明教程 | 用 PPT 做卡通热图 - eFP Graph?!
  6. CSS文本超出部分利用省略代替
  7. win10 VMI Provider Host占用CPU过高
  8. 鼠标悬停文字断裂字体js特效
  9. 客快物流大数据项目(八十):用户标签开发
  10. 企业邮箱 如何设置邮件列表与正文的排版?