OK,今天的话,我们来说一下这个Word2Vector,其实这个东西咱们在说AI助手的时候呢其实已经说了,但是说的不是很仔细,有一些东西也没有说清楚,一方面是这边博文:还在调API写所谓的AI“女友”,唠了唠了,教你基于python咱们“new”一个(深度学习)说实话,压根没打算就说像以前写的YOLO系列一样,因为这里面涉及到的前期准备工作和后面的一些东西就是给那些有NLP基础懂点这玩意的小伙伴的一个稍微完整一点的dome。也就是说默认你是知道的,我只是提一下里面比较关键的点(而且当时为了便于理解也做了不少妥协)。所以这就给我填坑的机会(水博文)。那么今天就略微详细一点儿儿去说一下这玩意。
当然这玩意确实不是啥新玩意,在2013年的时候:《Efficient Estimation of Word Representation in Vector Space》就说了,只是说这玩意用的爽,效果也不错,也是一个基础须知。
OK,还是那句话,先来问问这个是啥?啥是Word2Vector,这玩意有啥用,怎么用。首先用最简单的文本分类来说吧。我们假设此时我们选择使用NN也就是神经网络来做这件事情。
.
那么问题来了,我们是输入了一个文本,文本和我们原来说的图像可不一样,这玩意是文本,不是数字啊,计算机很聪明,但是也很呆,它只会算,只会做计算,完全不知道你的文字是什么,因为文字无法直接参与运算啊,算不了,那就不爱了,这显然不行。因此我们需要想办法对我们的文本进行一个处理。最直接的方案就是把我们的文字变成一个向量,只有这样才能够让计算机去运算,能算出来就等价于能够理解了。
所以这个时候呢,我们的想法,核心就是如何把文字这个玩意给变成数字。更形象的说法就是如何把我们的文字,给“翻译”为向量。
那么在这边我们肯定最容易想到的方案就是做一个编码,首先我们需要知道的,就是我们的文本里面,我们是由句子组成。也就是说,我们
其实非常简单。
看下面一组图就明白了:
我们通过一个词典其实就可以完成一个向量的映射。
看到了吧,我们这个时候我们只需要对一个句子进行分词,之后将每一个词进行标号,这样一来就可以实现把一个句子转化为一个向量。
此时我们得到了一组序列,但是这个序列的表达能力是在是太弱了,只能表示出一个标号,不能表示出其他的特点。或者说,只有一个数字表示一个词语实在是太单调了,1个词语也应该由一个序列组成。那么这个时候one-hot编码就出来了。他是这样做的:
首先一个词,一个字,我们叫做token,那么编码的很简单。其实就是这样:
但是这样是有问题的,那就是说,我们虽然实现了一个词到向量的表示。但是这个表示方法显然是太大了,假设有10000个词语,那么按照这种方式进行标号的话,那么1个词就是10000个维度。这样显然是不行的。所以这块需要优化一下。
那么此外的话,还有什么问题呢。很明显词语与词语之前其实还是有联系的,或者说,我们现在做的这个东西太生硬了,因为语言是非常复杂的,在不同情况下,我们词的表述是不一定的,比如有些中性词,在好的场合和好的一些词搭配,那么这个词表达的就是好的意思,但是反过来,他表达的可能就是不好的意思,甚至他还是中性词。
再举一个例子就是比如我们的一个英语单词,你随便搜索一个,他基本上都不可能只有一个意思,他可能有很多意思,我们要做的是把文本翻译为数字,那么作为翻译的家伙就必须搞懂这个词的全部意思,然后结合语境,也就是上下文给出可能性最高的意思
所以说,如果直接使用到one-hot,一方面确实向量太大了如果词比较多的话,另一方面太单一了。说到底还是key-value,我们要的是key-value1,value2,value3…这样的结构,并且我还希望,能够结合不同的“语境”给我不同的意思。
所以的话问题就是怎么做了呗。
所以的话,这个Word2Vector这个玩意出来了。
OK,我们终于到了正文了,首先这万一是咋做的呢(这块的话我要反思一下就是原来我写那个博文的时候把这个玩意的一些东西,给混到FastText里面去了,就是串了一点儿,没办法它们的处理手段有点类似,只是有无监督的问题,而且作者都是同一个大哥)
它的核心思想是通过词的上下文得到词的向量化表示,有两种方法:CBOW(通过附近词预测中心词)、Skip-gram(通过中心词预测附近的词):
大概就是这样的一个处理方法,当时我写混了的就是那个FastText里面的N-gram和这里的Skip-gram。写混头了(评论区也没提醒,怎么回事!)
首先我们来看到这个CBOW,说点不是人的话:
通过目标词的上下文的词预测目标词,图中就是取大小为2的窗口,通过目标词前后两个词预测目标词。具体的做法是,设定词向量的维度d,对所有的词随机初始化为一个d维的向量,然后要对上下文所有的词向量编码得到一个隐藏层的向量,通过这个隐藏层的向量预测目标词,CBOW中的做法是简单的相加,然后做一个softmax的分类,例如词汇表中一个有V个不同的词,就是隐藏层d维的向量乘以一个W矩阵(
)转化为一个V维的向量,然后做一个softmax的分类。由于V词汇的数量一般是很大的,每次训练都要更新整个W矩阵计算量会很大,同时这是一个样本不均衡的问题,不同的词的出现次数会有很大的差异,所以论文中采用了两种不同的优化方法多层Softmax和负采样。
其实很简单,其实当初小学,初中最烦的,那个啥,给个句子选单词的题目。就这种破玩意:
现在我们的目的是为了得到一个“翻译器”用来学会我们文本转化到数学向量的情况。所以我们得让计算机模拟一下我们人类的学习方法,毕竟我们的大脑是净化了几亿年的超级神经网络,我们都是这样学滴,所以让机器也感受一下嘛,毕竟也没有啥比较好的东西可以参考了。
之后的话就是我们的Skip-gram其实也是一样的,只是说我们把挖空的位置换了一下。
之后的话我们再来聊一聊我们的这个计算,或者说是输出吧。我们看前面的图也应该明白了,我们做Word2Vec的时候其实是相当于训练了一个玩意来充当“翻译官”的角色。那么此时看着图上面的东西来说的话,我们是按照分类的方案去训练它的,最后的输出应该走了一个softmax,假设有1W个词,最后输出的最后一个维度也应该是1W还是挺大的呀,这样输入到神经网络的话,那么好像虽然具备了那种动态的感觉,但是维度还是很高啊,那么这里的话维高的话我们后面其实加一个线性层就好了。那么这个是在用的角度,现在我们还是来讨论在训练的角度,也就是说对应训练我们怎么做,对应最后一个要怎么做概率的一个计算一个softmax要怎么做。
这个的话其实在FastText提过,当然还是那句话作者都是一个大哥,没啥好说的。那么层次化softmax其实也很好理解,
那么我们这里简单说一下这个层次化softmax。其实这玩意的本质其实就是在玩概率组合。
首先我们通过哈夫曼树,将对应的标签构造出一棵树。
每次,把多分类的softmax变成了二分类的,此时你甚至可以直接使用sigmod代替softmax函数。用一个公式总结其实就是:
当然的话,其实我们还有一个思路其实就是,我们的目的其实就是单纯的得到一个概率其实就够了,我们直接就是最后的输出是一个概率值,这个啥意思呢,就是最后面我们不是直接预测全部词的概率了,然后最后用交叉熵之类的,我就是看做一个回归问题,也就是直接就是树,我就猜测我们预测出来是这个target的一个概率。并且他肯定是越接近1越好。这样一来其实也是可以处理的,而且先前的YOLO其实也是这样干的,以前提到fast-rcnn的时候呢,他还是分开一个是分类预测,预测类别,一个是目标框,人家YOLO,直接全回归,但是人家回归还是预测了每一个类别的概率。但是我们这边不管是层次化还是全回归其实都已经是在做二分类问题了。
此外YOLO当初不这样干的话,还有原因我认为其实还和负样本有点关系,当然咱们这边就不讨论这个了,我们来看到这个东西。那么为什么要说这个呢,原因很简单,变成一个二分类问题以后,我们所有的target,其实都是期望得到的概率是1.也就是变成二分类以后虽然好算了,但是呢有个问题。就比如我们刚刚的全回归思路。
我们是这样的,我们现在预测的是概率,对吧:那么这样它的损失函数就变成了这个
我们target的概率就是1.所以问题就来了啊,都是1,那么网络直接瞎蒙1或者往1靠拢那么损失绝对小啊,于是就是变成了先前提到扩散模型的例子了,学生骗老师啊。所以我们得做一个负采样,也就搞一点错误的答案,来看你有没有学会。
那么负采样的话是这样的:
根据词出现的频率进行采样,出现频率越高,越可能被采样到,原文中是根据出现频率的3/4次方然后做一个归一化作为概率进行采样。
生成的负样本大概是这样的:
这里面的话,怎么说呢,不是所有的都要嘛,
OK,那么现在的话,我们来看看大概是怎么一个玩法。当然我们这边都是写简单版本。但是我们这边要说明的就是,这个其实是一种方式,方法,一种思想。说白了其实就是我们可以去使用到一个NN去理解句子,词语之间的含义。充当好翻译官,我们这边只是尽可能去模仿论文描述的做法而已,不要被定死了。
那么我们这边可以开始准备数据,我们这边的话就来做一下这个Skip-gram
import torch
import re
import numpy as np
txt=[] #文本数据
with open('peter_rabbit.txt',encoding='utf-8') as f:
for line in f.readlines():
l=line.strip()
spilted_sentence=re.split(" |;|-|,|!|\'",l)
for w in spilted_sentence:
if w !='':
txt.append(w.lower())
vol=list(set(txt)) #单词表
n=len(vol) #单词表单词数
vol_dict=dict(zip(vol,np.arange(n))) #单词索引
'''
这里使用词袋模型
每次从文本中选取序列长度为9,输入单词数为,8,输出单词数为1,
中心词位于序列中间位置。并且采用pytorch中的emdedding和自己设计embedding两种方法
词嵌入维度为100。
'''
data=[]
label=[]
for i in range(content_size):
in_words=txt[i:i+4]
in_words.extend(txt[i+6:i+10])
out_word=txt[i+5]
in_one_hot=np.zeros((8,n))
out_one_hot=np.zeros((1,n))
out_one_hot[0,vol_dict[out_word]]=1
for j in range(8):
in_one_hot[j,vol_dict[in_words[j]]]=1
data.append(in_one_hot)
label.append(out_one_hot)
class dataset:
def __init__(self):
self.n=ci=config.content_size
def __len__(self):
return self.n
def __getitem__(self, item):
traindata=torch.tensor(np.array(data),dtype=torch.float32)
trainlabel=torch.tensor(np.array(label),dtype=torch.float32)
return traindata[item],trainlabel[item]
代码就是这样的,5个A带一个小王。
当然这里还是有个one-hot,还是那个问题嘛
首先我们来一个embedding,我们先来定义一下这个玩意:
class embedding(nn.Module):
def __init__(self,in_dim,embed_dim):
super().__init__()
self.embed=nn.Sequential(nn.Linear(in_dim,200),
nn.ReLU(),
nn.Linear(200,embed_dim),
nn.Sigmoid())
def forward(self,input):
b,c,_=input.shape
output=[]
for i in range(c):
out=self.embed(input[:,i])
output.append(out.detach().numpy())
return torch.tensor(np.array(output),dtype=torch.float32).permute(1,0,2)
这个玩意是啥呢,其实就是我们要用来变幻维度的玩意,原来我们是one-hot太大了,要降低维度,那么这个结构其实就可以,而且作为理解词语含义的一个部分。当然我们实际上要用的是这个东西。
之后我们开始定义我们的理解词语的网络结构了:
class model(nn.Module):
def __init__(self):
super().__init__()
self.embed=embedding(num_word,100)
self.fc1=nn.Linear(num_word,1000)
self.act1=nn.ReLU()
self.fc2=nn.Linear(1000,num_word)
self.act2=nn.Sigmoid()
def forward(self,input):
b,_,_=input.shape
out=self.embed (input).view(b,-1)
out=self.fc1 (out)
out=self.act1(out)
out=self.fc2(out)
out=self.act2(out)
out=out.view(b,1,-1)
return out
这里的话我们就简单一点,那负样本我也不做了,要做的话,也好办是吧。
然后我们直接训练就好了:
if __name__=='__main__':
pre_model=model()
optim=torch.optim.Adam(params=pre_model.parameters())
Loss=nn.MSELoss()
traindata=DataLoader(dataset(),batch_size=5,shuffle=True)
for i in range(100):
print('the {} epoch'.format(i))
for d in traindata:
p=model(d[0])
loss=Loss(p,d[1])
optim.zero_grad()
loss.backward()
optim.step()
这样一来的话,我们就得到了一个“翻译”要用的话我们可以直接拿到embedding部分来用。而且其实这个更像是一个预训练处理的部分。实际上,我们可以设置多损失,也就是两部分损失,在正式使用的时候也进行一个训练,训练按照咱们的这个方式来,也就是把这两个家伙看做两个部分。或者你看做一个部分,也就是直接用那个embedding,然后硬刚其实也是可以的,都是一种思想,这玩意本来就没有固定一定要这样做,毕竟不是传统机器学习,那么严密。