区块链技术博客
www.b2bchain.cn

第四课.词向量

这篇文章主要介绍了第四课.词向量的讲解,通过具体代码实例进行16907 讲解,并且分析了第四课.词向量的详细步骤与相关技巧,需要的朋友可以参考下https://www.b2bchain.cn/?p=16907

本文实例讲述了2、树莓派设置连接WiFi,开启VNC等等的讲解。分享给大家供大家参考文章查询地址https://www.b2bchain.cn/7039.html。具体如下:

WordEmbedding

  • 词向量发展
  • Skip-Gram
  • Pytorch实现Skip-Gram
    • 随机种子和超参数设置
    • 语料处理
    • 使用dataloader
      • 实现Dataset对象
      • dataloader封装
    • 定义模型
    • 训练
    • 保存模型参数

词向量发展

在自然语言处理问题中,会涉及到一个重要部分:词向量;
词向量:我希望将单词word转为一个vector,因为机器不能直接感知到人类所说的语言,只有将单词转换为数值对象才能进行后续处理;
在早期NLP领域,单词的编码使用one-hot,这样的编码只是为了区分各个词,完全没有语义信息;
后来出现了TF-IDF,为每个word增加权重信息,常见的word权重大,罕见的word权重小,这样处理后,稍微看到了一点语义信息;
单词的编码还需要包含相似性,比如美洲豹和剑齿虎的编码在欧氏空间应该有较近的距离:
第四课.词向量
伴随着这样的思考,诞生了分布式表示(Distributed Representation):用一个词附近的词表示该词,这个想法是NLP的重大转折点,比如之后出现了word2vec(CBOW和Skip-Gram),CBOW可以理解为用周围的词预测中间的词,Skip-Gram可以理解为用中间的词预测周围的词,在paper中,也指出了Skip-Gram的效果更好

Skip-Gram

首先需要明白,词向量本身只是一个矩阵,对于同样的词汇表和同样的目标任务,不管是CBOW还是Skip-Gram,它们训练出来的词向量都是一个同样形状的二维张量,唯一不同在于其训练的方式;


关于词向量如何将单词编码
需要让输入的单词都是one-hot编码的形式,假设词汇表的单词数量为3000,则表中某个词bird将编码为[0,0,0,1,0,...,0],该向量中只有一个1,其余都是0,共3000位元素,bird的one-hot编码形状为[1,3000];
如果要将编码压缩为100个元素的向量,词向量形状则为[3000,100],当bird的one-hot编码与词向量相乘,就得到bird的向量[1,100],这个向量携带了一定的语义信息,准确来说,[1,100]这个向量才是词向量;
之前的二维张量叫做词向量也是有原因的,编码bird的过程等价于从张量索取了bird对应的行,所以这个二维张量相当于是词汇表中所有单词的编码集合


前面说过,Skip-Gram可以理解为用中间的词预测周围的词,根据Distributed Representation可以理解,其本质应该是中心词与周围词的关系更密切,这种关系被形象地可视化为:
第四课.词向量
projection并不是一个线性变换,而是指投影,即已编码的中心词w(t)与已编码的周围词w(t+i)投影相乘,结果越大,代表关系越紧密;
另外一提,Skip-Gram本身(对中心词和周围词编码,再投影)并没有意义,但通过Skip-Gram这种方式来训练,可以得到一个好的词向量;
Skip-Gram具体实现过程
词向量的训练实际上属于无监督学习,Skip-Gram需要两个同样shape的二维张量,一个用于编码中心词,另一个用于编码所有在词汇表内的词;
不能共用二维张量的原因:所谓中心词与周围词关系密切不能在同一编码方式下投影,比如"I like NLP",同一种编码下,很难让机器看出like与I和NLP相似;
令中心词编码后的向量为 v c v_{c} vc,某个周围词编码后的向量为 u o u_{o} uo,词汇表内各个词编码后的向量 u w u_{w} uw,编码用到两个张量embed1和embed2,注意, v c v_{c} vc通过张量embed1编码, u o u_{o} uo u w u_{w} uw都是通过embed2编码,词汇表共 W W W个单词;
则基于softmax可以求出相似度的概率表达:
p ( u o ∣ v c ) = e x p ( u o T v c ) ∑ w = 1 W e x p ( u w T v c ) p(u_{o}|v_{c})=frac{exp(u_{o}^{T}v_{c})}{sum_{w=1}^{W}exp(u_{w}^{T}v_{c})} p(uovc)=w=1Wexp(uwTvc)exp(uoTvc)
可以看出,中心词与周围词越相似,则输出概率值越大,Skip-Gram要用中心词与在窗口内的所有周围词计算相似度,假设窗口内前后各有 c c c个词,随机从语料库选择 T T T个中心词,则训练目标为:
m i n − 1 T ∑ t = 1 T ∑ − c ⩽ j ⩽ c , j ≠ 0 l o g ( p ( w t + j ∣ w t ) ) min-frac{1}{T}sum_{t=1}^{T}sum_{-cleqslant jleqslant c,jneq 0}^{}log(p(w_{t+j}|w_{t})) minT1t=1Tcjc,j=0log(p(wt+jwt))
p ( w t + j ∣ w t ) p(w_{t+j}|w_{t}) p(wt+jwt)取对数可以将连乘转为累加,避免反向传播计算梯度时出现梯度消失现象;
在上述计算中,softmax分母计算量很大, v c v_{c} vc要与每个word的编码求点积,为了加速计算,改进出简化版本,核心做法在于随机从词汇表中选出负例样本,计算规模得到缩减,softmax也改成sigmoid函数:
p ( u o T v c ) = 1 1 + e x p ( − u o T v c ) p(u_{o}^{T}v_{c})=frac{1}{1+exp(-u_{o}^{T}v_{c})} p(uoTvc)=1+exp(uoTvc)1
假设从词汇表随机采样 K K K个单词的编码 u k u_{k} uk作为负样本,则训练目标改写为:
m i n − l o g ( p ( u o T v c ) ) + ∑ k = 1 K l o g ( p ( u k T v c ) ) min-log(p(u_{o}^{T}v_{c}))+sum_{k=1}^{K}log(p(u_{k}^{T}v_{c})) minlog(p(uoTvc))+k=1Klog(p(ukTvc))
上式希望中心词与周围词越紧密越好,与随机采样的负样本关系越疏远越好;
注意一个细节,sigmoid函数将值映射到0-1之间,但log(0->1)的值是小于零的,所以 ∑ k = 1 K l o g ( p ( u k T v c ) ) sum_{k=1}^{K}log(p(u_{k}^{T}v_{c})) k=1Klog(p(ukTvc))会是负值且绝对值较大, − l o g ( p ( u o T v c ) ) -log(p(u_{o}^{T}v_{c})) log(p(uoTvc))虽是正值,但比较小,最终的目标值将会小于零,这对人类视角看待最小化问题有一些别扭,所以,根据sigmoid的单调性,目标改写为:
m i n − [ l o g ( p ( u o T v c ) ) + ∑ k = 1 K l o g ( p ( − u k T v c ) ) ] min-[log(p(u_{o}^{T}v_{c}))+sum_{k=1}^{K}log(p(-u_{k}^{T}v_{c}))] min[log(p(uoTvc))+k=1Klog(p(ukTvc))]
这样一来,目标值将必然大于零,所以最小化的结果越接近零越好;接下来,我将根据这个负采样的做法实现Skip-Gram训练,通过反向传播的梯度更新张量embed1和embed2,最后取出embed1即词向量

Pytorch实现Skip-Gram

随机种子和超参数设置

为了使实验可以复现,设置随机种子,并固定超参数:

import torch import torch.nn as nn import torch.nn.functional as F  from collections import Counter#统计单词出现次数 import numpy as np  #为了确保实验可以复现,设置随机种子 import random  #都设置一遍随机种子 random.seed(53113) np.random.seed(53113) torch.manual_seed(53113)  USE_CUDA=torch.cuda.is_available()  if USE_CUDA:     #GPU也设置随机种子     torch.cuda.manual_seed(53113)      #设置超参数hyper parameters C=3 #context window K=100 #负例采样数量 NUM_EPOCHS=1 MAX_VOCAB_SIZE=30000 #英语中,有3万个常见单词 BATCH_SIZE=128 LEARNING_RATE=0.2 EMBEDDING_SIZE=100 

语料处理

首先获取文本组成的语料数据,确保已删除所有标点符号,单词用空格分开,然后可以用split()分词得到字符串列表:

#用于对语料数据进行分词 def word_tokenize(text):     #S.split(sep=None, maxsplit=-1) -> list of strings     #split的seq默认为所有空字符     return text.split() 

下一步创建单词表vocab{word:counts},vocab设置容量3万个单词MAX_VOCAB_SIZE=30000(英语常用单词有3万个),在这3万个单词中,有一个比较特殊:用于代表语料库中所有不常见单词,记为"<unk>";
一般训练一个良好的词向量需要超大的数据,我的小数据集效果应该不会太好,注意:这个数据集已经去除过所有标点符号;
分词获取列表:

with open("./DataSet/textset/texttrain.txt","r") as f:     text=f.read()  #对语料进行分词 text=word_tokenize(text.lower()) 

借助from collections import Counter统计单词出现次数:

#统计词数,len(Counter(text).keys()) 远大于 MAX_VOCAB_SIZE vocab=dict(Counter(text).most_common(MAX_VOCAB_SIZE-1))  #最后一个统一为UNK vocab["<unk>"]=len(text)-np.sum(list(vocab.values())) 

构建mapping,为词建立序号,以便于one-hot编码:

#构建mapping,为词建立序号,以便于one-hot编码 idx_to_word=[word for word in vocab.keys()] word_to_idx={word:i for i,word in enumerate(idx_to_word)} 

获取词频,用于后续训练的采样:

#计算词频率 word_counts=np.array([count for count in vocab.values()],dtype=np.float32) word_freqs=word_counts/np.sum(word_counts)  #word2vec论文中增加了一个细节 word_freqs=word_freqs**(3./4.) word_freqs=word_freqs/np.sum(word_freqs) 

注意:word_to_idx,idx_to_word,word_freqs,word_counts的顺序都是一致对应的

使用dataloader

dataloader是pytorch中的数据加载器,用于高效生成训练需要的batch data,在使用dataloader前,先定义dataset,一般流程为:实现dataset对象,用dataloader封装dataset产生batch

实现Dataset对象

dataset继承自torch.utils.data.Dataset,对于NLP的简单任务,至少需要在dataset类中完成三个魔法方法__init__(),__len__(),__getitem__();
在本次实现中,dataset应该做到将text列表内的单词进行编码(基于word_to_idx),__getitem__()能够根据输入的index从经过编码的text返回一个中心词和 2 c 2c 2c个周围词, 2 c K 2cK 2cK个负例词:

import torch.utils.data as tud  class WordEmbeddingDataset(tud.Dataset):     # word_to_idx,idx_to_word,word_freqs,word_counts的顺序都是一致对应的     def __init__(self,text,word_to_idx,idx_to_word,word_freqs,word_counts):         super().__init__()         #获得text的每个单词在word_to_idx中的序号,D.get(k[,d]) -> D[k] if k in D, else d.         self.text_encoded=[word_to_idx.get(word,word_to_idx["<unk>"]) for word in text]               self.text_encoded=torch.tensor(self.text_encoded,dtype=torch.long)                  self.word_to_idx=word_to_idx         self.idx_to_word=idx_to_word         self.word_freqs=torch.tensor(word_freqs,dtype=torch.float)          def __len__(self):         #数据集的item数量         #数据集就是text的每个词通过word_to_idx进行了编码         return len(self.text_encoded)               def __getitem__(self,location):         #给一个词在text_encoded中的位置,返回一串训练数据(前后的词)         center_word=self.text_encoded[location]         #周围词在text_encoded中的位置,range左闭右开         pos_indices=list(range(location-C,location))+list(range(location+1,location+C+1))         #目前的pos_indices可能会超出text_encoded的边界,所以通过取余避免         pos_indices=[i%len(self.text_encoded) for i in pos_indices]         #类似于numpy的快速索引         pos_words=self.text_encoded[pos_indices]                  #根据词出现的频率随机采样,借助torch.multinomial         """         torch.multinomial(input, num_samples, replacement=False,)->LongTensor         replacement指的是取样时是否是有放回的取样         返回的是input的索引组成的tensor         """         #由于word_freqs顺序与word_to_idx顺序一致,相当于采样返回的也是idx         neg_words=torch.multinomial(self.word_freqs,K*pos_words.shape[0],replacement=True)                  return center_word,pos_words,neg_words   dataset=WordEmbeddingDataset(text,word_to_idx,idx_to_word,word_freqs,word_counts) 

dataloader封装

使用dataloader包装dataset,读取占用更少内存,高效产生batch:

dataloader=tud.DataLoader(dataset,batch_size=BATCH_SIZE,shuffle=True,num_workers=4) 

定义模型

首先明确:Embedding本质就是一个(vocab_size,embed_size)的tensor;
按照前面的说明,此次Embedding层需要两个二维tensor,在前面所说的embed1命名为embed_in,embed2命名为embed_out,像第二课所说的,__init__()中定义要计算梯度的层,前向传播过程定义在forward内;
本次实现新增一个实例方法get_weight,用于获取词向量embed_in;


torch.nn.Embedding
在nn中已经有Embedding层,输入并不需要真的转换到one-hot向量,而是像CrossEntropyLoss那样,使用了索引的方式计算,这也是为什么dataset类里的self.text_encoded元素类型为torch.long的原因;
对于nn.Embedding(vocab_size,embed_size)
Embedding的输入可以是一个在0到vocab_size-1之间的整数,输出是一个包含embed_size个元素的向量


模型定义如下:

class EmbeddingLayer(nn.Module):     def __init__(self,vocab_size,embed_size):         super().__init__()         self.vocab_size=vocab_size         self.embed_size=embed_size                  self.in_embed=nn.Embedding(self.vocab_size,self.embed_size,sparse=False)         self.out_embed=nn.Embedding(self.vocab_size,self.embed_size,sparse=False)                  #对embedding层的权重进行初始化,使训练一开始的loss较小         initrange=0.5/self.embed_size         self.in_embed.weight.data.uniform_(-initrange,initrange)         self.out_embed.weight.data.uniform_(-initrange,initrange)                       def forward(self,input_labels,pos_labels,neg_labels):         # input_labels: [batch_size]         # pos_labels: [batch_size,C*2]         # neg_labels: [batch_size,C*2*K]                  #nn.Embedding的输入是一组索引列表         input_embedding=self.in_embed(input_labels) #[batch_size, embed_size]         pos_embedding=self.out_embed(pos_labels)   #[batch_size, C*2, embed_size]         neg_embedding=self.out_embed(neg_labels)   #[batch_size, C*2*K, embed_size]                  #为了bmm,增加一个维度         input_embedding=input_embedding.unsqueeze(2) #[batch_size, embed_size, 1]         #批处理张量乘法bmm:batch matrix multiplication(b,m,n)*(b,n,p)->(b,m,p)         pos_dot=torch.bmm(pos_embedding,input_embedding) #[batch_size, C*2, 1]         #去除最后一个维度         pos_dot=pos_dot.squeeze(2) #[batch_size,C*2]                  neg_dot=torch.bmm(neg_embedding,input_embedding).squeeze(2) #[batch_size,C*2*K]                  #顺便计算目标函数,借助logsigmoid,即log(sigmoid(value))         log_pos=F.logsigmoid(pos_dot).sum(dim=1)         log_neg=F.logsigmoid(-neg_dot).sum(dim=1)                  loss=-log_pos-log_neg                  return loss #[batch_size]          def get_weight(self):         #获取词向量层的权重,用于np保存数据         return self.in_embed.weight.data.cpu().numpy() 

生成对象:

# 模型实例化 model=EmbeddingLayer(MAX_VOCAB_SIZE,EMBEDDING_SIZE) if USE_CUDA:     model=model.cuda() 

训练

基于dataloader,类似于keras中的生成器产生batch,可以方便的产生batch data,比如:

for i,(input_labels,pos_labels,neg_labels) in enumerate(dataloader): 

选择优化方法并训练:

optimizer=torch.optim.SGD(model.parameters(),lr=LEARNING_RATE)  loss_embedding=[] for epoch in range(NUM_EPOCHS):     for i,(input_labels,pos_labels,neg_labels) in enumerate(dataloader):         #print(input_labels,pos_labels,neg_labels)         input_labels=input_labels.long()         pos_labels=pos_labels.long()         neg_labels=neg_labels.long()                  if USE_CUDA:             input_labels=input_labels.cuda()             pos_labels=pos_labels.cuda()             neg_labels=neg_labels.cuda()                  loss=model.forward(input_labels,pos_labels,neg_labels).mean()         if i%100==0:             print(epoch,i,loss.item())             loss_embedding.append(loss.item())                  loss.backward()                  optimizer.step()                  model.zero_grad() 

定期将loss加入列表后,可视化训练过程为:

#绘制学习曲线 import matplotlib.pyplot as plt %matplotlib inline  plt.figure(figsize=(10,6)) plt.subplot(1,1,1) #后面的loss基本变化微小,取0:300绘图,效果更好 nploss=np.array(loss_embedding[:300]) series=np.arange(len(nploss)) plt.plot(series,nploss) plt.xlabel("series") plt.ylabel("loss") plt.title("learning cruve") plt.show() 

第四课.词向量
在实际训练中,通常不会过度关注loss,而是看spearmanr是否在上升


spearmanr:用模型计算人工词语的相似度,再与人工给出的相似度对比


保存模型参数

在定义模型时,可以通过方法get_weight()取出张量embed_in,我将用numpy的方式保存参数:

#np保存模型的参数 embedding_weights = model.get_weight() np.save("embedding-{}".format(EMBEDDING_SIZE),embedding_weights) 

也可以用torch保存参数,在保存时,需要明确保存的对象为model.state_dict()model.state_dict()是模型所有可学习的参数名与张量组成的字典:

torch.save(model.state_dict(), "embedding-{}.th".format(EMBEDDING_SIZE)) 

这样将会保存embed_in和embed_out,通过重新生成一个对象,并导入参数可验证:
第四课.词向量

本文转自互联网,侵权联系删除第四课.词向量

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » 第四课.词向量
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们