目录
目录
1.读写数据集
2.重采样
3.建立datasetLoader
4.搭建skip-gram模型
5.训练
1.读写数据集
使用的是一份英文数据集。其网盘地址如下:
实现工具:Jupyter
提取码:7m14
之前看了许多博主和教学视频都是训练中文词向量,但是中文词向量有一个很麻烦的事情就是分词。他们几乎都毫不犹豫的选择jieba分词,然而jieba分词是基于1阶马尔科夫随机场分词,这种分词方法在精确率、召回率以及F1的指标上表现较结构化感知机与条件随机场分词差很多不说,而且对于未登陆词的识别能力也十分有限。其次,就算我们采用了分词效果很好的结构化感知分词器或CRF分词器,但是这些分词器在训练的时候,他们的字典里面也不会存在许多人名或者文言文等词语。若要真正想分词好某一领域,应该做好相关领域的标注以后进行分词训练,然后在用训练好的模型在相关领域上分词。目前,大部分分词器都是基于白话文分词,所以网上有很多案例都是基于四大名著原著文言文分词,用jieba分词器在这上面分词效果真的很差很差。而分词效果不好对于词向量的训练影响也是巨大的。比如:“结婚的和尚未结婚的”,正确的分词应该为:“结婚 的 尚未 结婚 的”,还有“川普通话普京讨论推广四川普通话和银川普通话”。正确的分词为“川普 通话 普京 讨论 推广 四川 普通话 和 银川 普通话”。词都错了,那么训练出来更是让人没法接受。
基于分词存在的种种不便,所以在在第一次尝试编写词向量的时候,应该选择英文,因为英文分词基于空格分词,分词简单方便,而且错误也比较少,而中文分词后词向量训练的结果很可能你会发现高纬之间相互靠近的向量语义上不相似的,那时候就不清楚是代码的原因还是分词的原因...
首先我们读写数据集,并且将词切好:
from collections import Counter#用于统计词频
#读写语料库
with open("train.txt","r",encoding="utf-8") as f:
corpus=f.read()
#语料库切分分词
corpus=corpus.lower().split(" ")
#统计语料库中的词频
count=dict(Counter(corpus))
2.重采样
根据齐夫定律可知,高频词存在的信息较少,因此我们应该丢弃高频词。其计算公式如下:
我的做法是:计算每个词被丢弃的概率,高于0.8的将会有几率被丢弃:
import math#用于删除概率的计算
import random#用于随机产生一个概率
t=1e-5#t值
threshold=0.9#高频的阈值
#计算词的频率
word_freq={w:c/len(corpus)for w,c in count.items()}
#计算每个词被删除的概率
word_del_prob={w:max(0,1-math.sqrt(t/c)) for w,c in word_freq.items()}
#删除高频词
del_word=[]
for w,c in word_del_prob.items():
if(c>=threshold):#大于此阈值,则有几率被删除
prob=random.random()#随机产生一个(0,1)概率,若大于删除概率大于此,则删除
if(c>prob):
del_word.append(w)
#删除高频词
newcorpus=[w for w in corpus if w not in del_word]
#释放不在使用变量
del corpus,del_word,word_del_prob,word_freq
这里我自己查看了一下需要删除的词,结果如下,发现删除的词其所携带的信息确实很少。
['is',
'the',
'as',
'its',
'more',
'are',
'and',
'after',
'or',
'some',
'of',
'it',
'has',
'a',
'was',
'first',
...]
3.建立datasetLoader
这一步我认为是从代码技术层面上来看最难掌握的,因为其包含了负采样,还有一系列表索引建立,最后包括数据集生成正负样本。
import torch
from torch.utils.data.dataset import Dataset #打包中心词,背景词,负采样词
from torch.utils.data.dataloader import DataLoader #生成batch数据
我们首先要建立词和id的相互索引,id是为了embedding嵌入的时候选择对应的词向量,这里的id是多少(1,2,...),embedding嵌入的时候会自动选择某一行,这样就省去我们自己构造one-hot编码。
#重新统计词频
count=Counter(newcorpus)
#建立词与索引相互映射表
idx2word=[word for word in count.keys()]
word2idx={w:step for step,w in enumerate(idx2word)}
下面要重新计算每个词作为负样本被采样的概率,计算公式为:
#计算每个词被当作为负样本采样的概率
#其坐标必须对应idx2word,否则负采样会出错这,千万别遍历字典统计!
word_freq=[(count[w]/len(newcorpus))**(3./4.) for w in idx2word]
#归一化因子
Z=sum(word_freq)
#重新计算后的负采样频率
for i in range(len(word_freq)):
word_freq[i]=word_freq[i]/Z
构造我们数据生成器类:
skip_window=2
batch_size=256
k=10
class myDataset(Dataset):
def __init__(self,word_freq,corpus,word2idx):
super(myDataset,self).__init__()
self.corpus_encode=[word2idx[w] for w in corpus]#对语料库进行编码
self.corpus_encode=torch.tensor(self.corpus_encode)#语料库的编码
self.word_freq=torch.Tensor(word_freq)
def __len__(self):
return len(self.corpus_encode)
def __getitem__(self,idx):
#中心词
center_word=self.corpus_encode[idx]
#背景词
bg_word_indice=list(range(idx-skip_window,idx))+list(range(idx+1,idx+1+skip_window))
bg_word_indice=[i%len(self.corpus_encode) for i in bg_word_indice]#%为了语料库末端防止越界
bg_word=self.corpus_encode[bg_word_indice]
#负采样词
#循环是为了防止负采样词是背景词
while(True):
#采样参照的频率是self.word_freq,采样k*len(bg_word)个词,False表示不放回
#最后返回的是采样word_freq的下标,若word_freq是字典统计则返回可能报错,因为字典存储无顺序
neg_word=torch.multinomial(self.word_freq,k*len(bg_word),False)
#采样到背景词的标志位
flag=True
for w in neg_word:
if(w in bg_word):
flag=True
if(flag):
break
return center_word,bg_word,neg_word
这样我们的数据生成器就可以自动提供一个中心词,2*skip-window背景词以及2*skip-window*个负采样词。下面我们进行测试
dataset=myDataset(word_freq,newcorpus,word2idx)
dataloader=DataLoader(dataset,batch_size,True,drop_last=True)
for x,y,z in dataloader:
print(x.shape,y.shape,z.shape)
break
输出结果为:
torch.Size([256]) torch.Size([256, 4]) torch.Size([256, 40])
4.搭建skip-gram模型
上一节推导,在模型一次前向传播喂负采样推导损失的公式为:
import torch.nn as nn
from torch.nn.modules import Module
d_model=100
vocab_size=len(count)
class skip_gram(Module):
def __init__(self,vocab_size,d_model):
super(skip_gram,self).__init__()
self.vocab_size=vocab_size
self.d_model=d_model
self.inEmbeding=nn.Embedding(vocab_size,d_model)
self.outEmbeding=nn.Embedding(vocab_size,d_model)
def forward(self,cen_word,bg_word,neg_word):
#获取中心词向量
v_t=self.inEmbeding(cen_word)
#获取背景词向量
u_t_j=self.outEmbeding(bg_word)
#获取负采样词向量
u_k=self.outEmbeding(neg_word)
#计算损失,结合公式细看
#cen_word[bacth,d_moel],bg_word[bacth,2*skip_windows,d_moel],neg_word[bacth,2*k*skip_window,d_moel]
log_pos=torch.bmm(u_t_j,v_t.unsqueeze(2)).squeeze()
log_neg=torch.bmm(u_k,-v_t.unsqueeze(2)).squeeze()
loss=nn.functional.logsigmoid(log_pos).sum(1)+nn.functional.logsigmoid(log_neg).sum(1)
return -loss
def get_inbeding(self):
return self.inEmbeding.weight.data.cpu().numpy()
写个测试,看看模型是否有误:
model=skip_gram(vocab_size,d_model)
x=x.long()
y=y.long()
z=z.long()
print(model(x,y,z).shape)
输出结果为:
torch.Size([256])
5.训练
接下来无非就是水到渠成的事情,设置好优化器,迭代次数等即可:
from torch.optim import SGD #随机梯度下降优化器
import scipy
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
计算高维空间中最接近的10个词:
#寻找当前词最接近的10个词
def find_nearest(word):
index = word2idx[word]
#获取词向量
embedding = embedding_weights[index]
#计算余弦距离(也可以理解为余弦相似度)
cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])
return [idx2word[i] for i in cos_dis.argsort()[:10]]#从小到大排序取出前10对应的词
训练
epochs=10#迭代次数
lr=0.2 #学习率
optim=SGD(model.parameters(),lr)
cuda=torch.cuda.is_available()#判断gpu是否可用
if cuda:
model=model.cuda()
for epoch in range(epochs):
for step,(cen_word,bg_word,neg_word) in enumerate(dataloader):
cen_word=cen_word.long()
bg_word=bg_word.long()
neg_word=neg_word.long()
if cuda:
cen_word=cen_word.cuda()
bg_word=bg_word.cuda()
neg_word=neg_word.cuda()
#梯度清零
optim.zero_grad()
#计算损失
loss=model(cen_word,bg_word,neg_word).mean()
#反向传播
loss.backward()
#梯度更新
optim.step()
allloss+=loss
if ((step+1) % 5== 0):
print("epoch:{}, iter:{}, loss:{}\n".format(epoch, step+1 ,loss))
if ((step+1)% 2000 == 0):
embedding_weights = model.get_inbeding()
print("epoch: {}, iteration: {}, nearest to two: {}\n".format(epoch, step+1, find_nearest("two")))