词嵌入(Word Embedding)原理详解

  词嵌入模型是自然语言处理(NLP)中语言模型与表征学习技术的统称。在自然语言处理过程中,我们需要将单词(word)映射到对应的向量,从而能够用于模型训练。通常情况下可以使用one-hot向量来表示单词,但是one-hot向量长度为单词表所有单词的个数,数量过于庞大,并且各个单词之间相似度为0,这与我们日常生活是很不符的(不同的单词之间可能会比较相近,在文本中经常在一起出现;也可能不相近,在文本距离较远)。所以选择新的方法来表示单词便显得尤为重要。

  对于上述问题,可以用词嵌入的方法来解决。使用词嵌入的基本思路是:先用one-hot编码等方法来标记单词,然后构建一个包含embedding层的神经网络,模型的输入和输出一般为在文本中位置相近的单词one-hot向量。训练完成中,将单词one-hot向量输入到embedding中,embedding的输出向量即为该单词的新的嵌入表示。这些向量一般情况下远远小于one-hot向量长度,并且可以用于度量各个单词之间的相似与类比关系。

一:基本原理

  兔兔先以下面的一个文本为例:

“rabbit are very lovely animals, and we should not hurt them  ”

  对于这样的文本,它包含11种单词,所以把单词按一定顺序排列,则每个单词可以由长度为11的向量表示。

  若单词表:[rabbit,are,very,lovely,animals,and,we,should,not,hurt,them],此时rabbit的one-hot表示为[1,0,0,0,0,0,0,0,0,0,0]

  之后,我们需要构造一个模型,以文本中相邻的单词为模型输入与输出来进行训练。然而,模型的输入与输出需要固定个数。在这里,兔兔引入Word2vec中常用的两种模型:跳词模型与连续词袋模型。

1.跳词模型

  跳词模型,它是通过文本中某个单词来推测前后几个单词。例如,根据‘rabbit’来推断前后的单词可能为‘a’,'is','eating','carrot'。在训练模型时我们在文本中选取若干连续的固定长度的单词序列,把前后的一些单词作为输出,中间的某个位置的单词作为输入。

2.连续词袋模型

  连续词袋模型与跳词模型恰好相反,它是根据文本序列中周围单词来预测中心词。在训练模型时,把序列中周围单词作为输入,中心词作为输出。

  对于词嵌入,emdedding层是模型的核心部分,它一般在整个网络第一层。在Pytorch中有专门的nn.embedding层来实现该部分,但实际上,embedding层的结构可以非常多样,最简单的使用仅仅一层全连接层也是可行的,embedding层理论上也可以是一个层数很多的网络。训练结束后,把某个单词的one-hots输入到embedding,embedding的输出为该单词嵌入表示。(embedding在训练时输入单词数固定,但是预测时可以输入一个单词,也可以接受多个单词输入,这也说明embedding结构较为特殊,在后面兔兔会详细讲述)

二:方法实现

  兔兔在下面案例中使用连续词袋模型,每次从文本中选取序列长度为9,输入单词数为,8,输出单词数为1,中心词位于序列中间位置。并且采用pytorch中的emdedding和自己设计embedding两种方法,词嵌入维度为50。文本节选自《The Tale Of Peter Rabbit》(彼得兔的故事),本文词总数:793,词汇数:401,需要的同学可以在资源中下载。

  对于文本数据,我们不考虑文本中标点符号,为了避免同一单词大小写不同的差异,一律将单词转成小写。

文本数据处理部分:

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))) #单词索引

data=[]
label=[]

for i in range(784):
    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=784 #训练样本数
    def __len__(self):
        return self.n
    def __getitem__(self, item):
        traindata=torch.tensor(np.array(data),dtype=torch.float32) #运行model1此处用long,model2用float32.
        trainlabel=torch.tensor(np.array(label),dtype=torch.float32)
        return traindata[item],trainlabel[item]

  这部分代码兔兔保存在dataset.py文件下,它将原始文本进行拆分并以一定顺序拆成需要训练的输入数据(data)与输出数据(label),并以one-hot表示。

1.使用nn.Embedding构建模型

  对于nn.Embedding(),至少需要两个参数:num_embeddings与embedding_dim,表示词汇表大小与词嵌入维度。在这里参数为(401,50)。

  理论上说,nn.Embedding应该类似于一个输入维度为词汇表长度、输出为词嵌入维度的全连接层。我们可以通过以下方法验证。

embed=nn.Embedding(401,200)
print(list(embed.parameters())[0].data)
print(list(embed.parameters())[0].data.shape)

  最终的参数的确表明它是一个单层的全连接网络。与普通全连接网络不同的是:它的每个神经元输入是一个词向量而不是数,这样它的输入维度可以是[batch_size,num_input_word,one_hot_dim]。所以它更像是共享相同参数的num_word个平行的全连接层,类似于卷积神经网络中的通道channel。所以embedding层输出的维度[batch,num_input_word,embed_dim]。将这个输入下一个全连接层时,需要改变embedding输出数据维度。该部分代码保存在model1.py中。

import torch
from torch import nn
from torch.utils.data import DataLoader
from dataset import dataset

class model(nn.Module):
    def __init__(self):
        super().__init__()
        self.embed=nn.Embedding(401,50)
        self.fc1=nn.Linear(160400,100)
        self.act1=nn.ReLU()
        self.fc2=nn.Linear(100,401*1)
        self.act2=nn.Sigmoid()
    def forward(self,input):
        b,_,_=input.shape
        out=self.embed(input).view(b,1,-1)
        out=self.fc1(out)
        out=self.act1(out)
        out=self.fc2(out)
        out=self.act2(out)
        return out
if __name__=='__main__':
    model=model()
    optim=torch.optim.Adam(params=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:
            yp=model(d[0])
            loss=Loss(yp,d[1])
            optim.zero_grad()
            loss.backward()
            optim.step()
    torch.save(model,'model_1.pkl')

2.自己构造embedding

  兔兔这里定义了一个含有层数为2的全连接层为embedding层,方法与前面embedding的效果相近。该部分代码保存在model2.py中。

import torch
from torch import nn
from torch.utils.data import DataLoader
from dataset import dataset
import numpy as np
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)


class model(nn.Module):
    def __init__(self):
        super().__init__()
        self.embed=embedding(401,50)
        self.fc1=nn.Linear(400,4000)
        self.act1=nn.ReLU()
        self.fc2=nn.Linear(4000,401*1)
        self.act2=nn.Sigmoid()
    def forward(self,input):
        b,_,_=input.shape
        out=self.embed (input).reshape(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__':
    model=model()
    optim=torch.optim.Adam(params=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:
            yp=model(d[0])
            loss=Loss(yp,d[1])
            optim.zero_grad()
            loss.backward()
            optim.step()
    torch.save(model,'model_2.pkl')

三:总结

  词嵌入模型作为自然语言处理的一种方法,其种类广泛,并且不局限于兔兔本文讲述的方法。而这些词嵌入方法的思想基本大致相同。从本质上来说,词嵌入与自编码器有诸多相似之处,它都是将数据维度降低,并且通常以更加合理的方式来表示数据,在实际应用总,这类方法对于深度学习模型的训练及优化具有重要意义。

猜你喜欢

转载自blog.csdn.net/weixin_60737527/article/details/127015770