Word2Vec是一种词向量的表示方式,是在论文《Efficient Estimation of Word Representations in
Vector Space》中提出,有两种网络模型来训练(Skip-gram和CBOW模型)。由Word2Vec训练出来的词向量可以提升下游任务的性能、效果。
语言模型是计算一个句子是句子的概率模型,即判断该句子是否合乎语义和语法。语言模型的建立方法有:基于专家语法规则的语言模型、统计语言模型、神经网络语言模型。
**基于专家语法规则的语言模型:**是由语言学专家设置一套规则而形成的语言模型,例如主谓宾结构的。
**统计语言模型:**基于统计概率的语言模型,即判断这个句子形成的概率。P(S)是句子的概率,P(W_i)是每个单词的概率。
P ( S ) = P ( W 1 , W 2 , . . . , W n ) = P ( W 1 ) P ( W 2 ∣ W 1 ) P ( W 3 ∣ W 1 W 2 ) . . . P ( W n ∣ W 1 W 2 W 3 . . . W n − 1 ) P{\left({S}\right)}=P{\left({W_1,W_2,...,W_n}\right)}=P{\left({W_1}\right)}P{\left( {W_2\left|W_1\right. }\right)}P{\left( {W_3\left|W_1W_2\right. }\right)}...P{\left( {W_n\left|W_1W_2W_3...W_n-1\right. }\right)} P(S)=P(W1,W2,...,Wn)=P(W1)P(W2∣W1)P(W3∣W1W2)...P(Wn∣W1W2W3...Wn−1)
每个单词的概率是基于一定文档(语料)来求取的,即有一定数量的文档,统计每个单词在所有文档中出现的次数,再除以所有单词的总数,进而求得该单词的频率,用频率代替概率。
P ( W i ) = P ( c o u n t ( W i ) ) / N P{\left({W_i}\right)}=P{\left({count(W_i)}\right)}/N P(Wi)=P(count(Wi))/N
统计语言模型中的平滑操作: 由于基于一定数量的文档而形成的词典并不能囊括世界上所有的单词,再者由于新词的产生,一定会出现一些词或词组是在语料中没有出现的,但是在现实中这种词或词组是真实存在的。为解决这一问题,可使用平滑操作,即对所有词初始状态就有一个很小的概率。方法:Laplace Smoothing(加1平滑),即每个词在原先出现次数的基础上加1.
马尔可夫假设: 即下一个词的出现仅仅依赖于前面的一个词或几个词。基于该假设有经典的语言模型:N-gram模型。常见的N-gram模型有:1-gram、2-gram、3-gram模型。下一个词的出现依赖于前N-1个单词。
语言模型的评价指标:困惑度
P ( S ) = P ( W 1 , W 2 , . . . , W n ) = P ( W 1 ) P ( W 2 ∣ W 1 ) P ( W 3 ∣ W 1 W 2 ) . . . P ( W n ∣ W 1 W 2 W 3 . . . W n − 1 ) P{\left({S}\right)}=P{\left({W_1,W_2,...,W_n}\right)}=P{\left({W_1}\right)}P{\left( {W_2\left|W_1\right. }\right)}P{\left( {W_3\left|W_1W_2\right. }\right)}...P{\left( {W_n\left|W_1W_2W_3...W_n-1\right. }\right)} P(S)=P(W1,W2,...,Wn)=P(W1)P(W2∣W1)P(W3∣W1W2)...P(Wn∣W1W2W3...Wn−1)
困惑度:
P P ( S ) = P ( W 1 , W 2 , . . . , W n ) − 1 / n PP(S)=P{\left({W_1,W_2,...,W_n}\right)}^{-1/n} PP(S)=P(W1,W2,...,Wn)−1/n
句子概率越大,语言模型越号,困惑度越小。
one-hot表示 形成一个V维的词表,每个词在其中一个位置上表示为1,其他位置为0.
分布式词表示 每个词都是一个维度为D的一维向量,这种表示方法可以判断单词之间的相似度。
NNLM,即前馈神经网络语言模型。根据前n-1个单词来预测第n个单词的概率。
RNNLM,基于循环神经网络的语言模型。在每个时间步预测一个单词,在预测第n个单词时使用了前n-1个单词的信息。
Word2Vec中有两种网络模型结构:skip-gram和cbow。Word2Vec的基本思想是句子中相近的词之间是由联系的,所以用词来预测词。其中skip-gram模型是中心词预测周围词,cbow模型是使用周围词预测中心词。
P ( W i − 1 ∣ W i ) = e x p ( u w i − 1 T ∗ v w i ) ∑ w = 1 V e x p ( u w T ∗ v w i ) P{\left( {W_{i-1}\left|W_i\right. }\right)}=\frac{exp(u_{w_{i-1}}^T*v_{w_i})}{\begin{matrix} \sum_{w=1}^V exp(u_w^T*v_{w_i})\end{matrix}} P(Wi−1∣Wi)=∑w=1Vexp(uwT∗vwi)exp(uwi−1T∗vwi)
使用关键技术来降低模型复杂度:层次softmax和负采样。
层次Softmax的基本思想是将求Softmax的计算转换为求Sigmoid的计算,采用Huffman树的方式,即带权重路径最短二叉树。
层次softmax的构建
Skip-gram模型
Cbow模型
负采样的基本思想是:增大正样本的概率,减小负样本的概率。
单词采样频率的确定:
归一化的用处在于:减少频率大的词的抽样概率,增加频率小的词的抽样概率。
负采样方法:
重采样:
模型复杂度:O=E✖T✖Q
其中:O是训练复杂度
E是训练迭代次数
T是数据集大小
Q是模型计算复杂度
网络模型的计算复杂度是由网络中的参数个数决定的。
Skip-Gram-NEG模型实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
class SkipGramModel(nn.Module):
def __init__(self,vocab_size,embed_size):#定义一个词表大小、词向量维度大小
super(SkipGramModel,self).__init__()
self.vocab_size = vocab_size
self.embed_size = embed_size
self.w_embeddings = nn.Embedding(vocab_size,embed_size)#定义中心词维度
self.v_embeddings = nn.Embedding(vocab_size,embed_size)#定义周围词维度
self._init_emb()
def _init_emb(self):#词向量初始化方法
initrange = 0.5 / self.embed_size
self.w_embeddings.weight.data.uniform_(-initrange,initrange)#符合正态分布
self.v_embeddings.weight.data.uniform_(-0,0)#符合正态分布
def forward(self,pos_w,pos_v,neg_v):
emb_w = self.w_embeddings(torch.LongTensor(pos_w).cuda()) #转化为tensor,大小为 mini_batch_size * emb_dimension
emb_v = self.v_embeddings(torch.LongTensor(pos_v).cuda())
neg_emb_v = self.v_embeddings(torch.LongTensor(neg_v).cuda()) # 转换为tensor,大小为negative_sampling_number * mini_batch_size * emb_dimension
score = torch.mul(emb_w,emb_v)
score = torch.sum(score,dim=1)
score = torch.clamp(score,max=10,min=10)
score = F.logsigmoid(score) #根据skip-gram网络结构计算正例损失函数
neg_score = torch.bmm(neg_emb_v,emb_w.unsqueeze(2))
neg_score = torch.clamp(neg_score,max=10,min=10)
neg_score = F.logsigmoid(-1*neg_score)#计算负例损失函数
loss = -torch.sum(score)-torch.sum(neg_score)#总的损失
return loss
def save_embedding(self,id2word,file_name):#保存词向量
embedding_1 = self.w_embeddings.weight.data.cpu().numpy()
embedding_2 = self.v_embeddings.weight.data.cpu().numpy()
embedding = (embedding_1+embedding_2)/2
fout = open(file_name,'w')
fout.write('%d %d\n' % (len(id2word)))
for wid,w in id2word.items():
e = embedding[wid]
e = ' '.join(map(lambda x:str(x),e))
fout.write('%d %d\n' % (w,e))
CBOW-NGE模型实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class CBOWModel(nn.Module):
def __init__(self, vocab_size, emb_size):
super(CBOWModel, self).__init__()
self.vocab_size = vocab_size
self.emb_size = emb_size
self.u_embeddings = nn.Embedding(self.vocab_size, self.emb_size, sparse=True)
self.w_embeddings = nn.Embedding(self.vocab_size, self.emb_size, sparse=True)
self._init_embedding() # 初始化
def _init_embedding(self):
int_range = 0.5 / self.emb_size
self.u_embeddings.weight.data.uniform_(-int_range, int_range)
self.w_embeddings.weight.data.uniform_(-0, 0)
def forward(self, pos_u, pos_w, neg_w):
pos_u_emb = [] # 上下文embedding
for per_Xw in pos_u:
# 上下文矩阵的第一维不同词值不同,如第一个词上下文为c,第二个词上下文为c+1,需要统一化
per_u_emb = self.u_embeddings(torch.LongTensor(per_Xw).cuda()) # 对上下文每个词转embedding
per_u_numpy = per_u_emb.data.cpu().numpy() # 转回numpy,好对其求和
per_u_numpy = np.sum(per_u_numpy, axis=0)
per_u_list = per_u_numpy.tolist() # 为上下文词向量Xw的值
pos_u_emb.append(per_u_list) # 放回数组
pos_u_emb = torch.FloatTensor(pos_u_emb).cuda() # 转为tensor 大小 [ mini_batch_size * emb_size ]
pos_w_emb = self.w_embeddings(torch.LongTensor(pos_w).cuda()) # 转换后大小 [ mini_batch_size * emb_size ]
neg_w_emb = self.w_embeddings(
torch.LongTensor(neg_w).cuda()) # 转换后大小 [ mini_batch_size*negative_sampling_number * emb_size ]
# 计算梯度上升( 结果 *(-1) 即可变为损失函数 ->可使用torch的梯度下降)
score_1 = torch.mul(pos_u_emb, pos_w_emb) # Xw.T * θu
score_2 = torch.sum(score_1, dim=1) # 点积和
score_3 = F.logsigmoid(score_2) # log sigmoid (Xw.T * θu)
neg_score_1 = torch.bmm(neg_w_emb, pos_u_emb.unsqueeze(2)) # batch_size*negative_sampling_number
neg_score_2 = F.logsigmoid((-1) * neg_score_1)
loss = torch.sum(score_3) + torch.sum(neg_score_2)
return -1 * loss
# 存储embedding
def save_embedding(self, id2word_dict, file_name):
embedding = self.u_embeddings.weight.data.numpy()
file_output = open(file_name, 'w')
file_output.write('%d %d\n' % (self.vocab_size, self.emb_size))
for id, word in id2word_dict.items():
e = embedding[id]
e = ' '.join(map(lambda x: str(x), e))
file_output.write('%s %s\n' % (word, e))