赞
踩
参考:《深度学习入门之Pytorch》
词嵌入到底是什么意思呢?其实很简单,对于每个词,可以使用一个高维向量去表示它,这里的高维向量和one-hot的区别在于,这个向量不再是0和1的形式,向量的每一位都是一些实数,而这些实数隐含着这个单词的某种属性。这样解释可能不太直观,先举四个例子,下面有4段话:
(1)The cat likes playing ball.
(2)The kitty likes playing wool.
(3)The dog likes playing ball.
(4)The boy likes playing ball.
重点分析里面的4个词,cat、kitty、dog和boy。如果使用onehot,那么cat就可以表示成(1,0,0,0),kitty就可以表示成(0,1,0,0),但是cat和kitty其实都表示小猫,所以这两个词语义是接近的,但是one-hot并不能体现这个特点。
下面使用词嵌入的方式来表示这4个词,假如使用一个二维向量(a,b)来表示一个词,其中a,b分别代表这个词的一种属性,比如a代表是否喜欢玩球,b代表是否喜欢玩毛线,并且这个数值越大表示越喜欢,这样就能够定义每一个词的词嵌入,并且通过这个来区分语义,下面来解释一下原因。对于cat,可以定义它的词嵌入是(-1,4),因为它不喜欢玩球,喜欢玩毛线;而对于kitty,它的词嵌入可以定义为(-2,5);那么对于dog,它的词嵌入就是(3,-2),因为它喜欢玩球,不喜欢玩毛线;最后对于boy,它的词向量就是(-2,-3),因为这两样东西他都不喜欢。定义好了这样的词嵌入,怎么去定义它们之间的语义相似度呢?可以通过词向量之间的夹角来定义它们的相似度。下面先将每个词向量都在坐标系中表示出来,如图5.28所示。
对于一个词,怎么知道如何去定义它的词嵌入?如果向量的维数只有5维,可能还能定义出来,如果向量的维数是100维,那么怎么知道每一维体是多少呢?这个问题可以交给神经网络去解决,只需要定义我们想要的维度,比如100维,神经网络就会自己去更新每个词嵌入中的元素。而之前介绍过词嵌入的每个元素表示一种属性,当然对于维数比较低的时候,可能我们能够推断出每一维具体的属性含义,然而维度比较高之后,我们并不需要关心每一维到底代表着什么含义,因为每一维都是网络自己学习出来的属性,只需要知道词向量的夹角越小,表示它们之间的语义更加接近就可以了。
词嵌入在 pytorch 中非常简单,只需要调用 torch.nn.Embedding(m, n) 就可以了,m 表示单词的总数目,n 表示词嵌入的维度,其实词嵌入就相当于是一个大矩阵,矩阵的每一行表示一个单词.
import torch from torch import nn from torch.autograd import Variable # 定义词嵌入 embeds = nn.Embedding(2, 5) # 2 个单词,维度 5 # 得到词嵌入矩阵 print(embeds.weight) ''' 我们通过 weight 得到了整个词嵌入的矩阵,注意,这个矩阵是一个可以改变的 parameter,在网络的训练中会不断更新,同时词嵌入的数值可以直接进行修改,比如我们可以读入一个预训练好的词嵌入等等 ''' # 直接手动修改词嵌入的值 embeds.weight.data = torch.ones(2, 5) print(embeds.weight) # 访问第 50 个词的词向量 embeds = nn.Embedding(100, 10) single_word_embed = embeds(Variable(torch.LongTensor([50]))) print(single_word_embed) ''' 可以看到如果我们要访问其中一个单词的词向量,我们可以直接调用定义好的词嵌入,但是输入必须传入一个 Variable,且类型是 LongTensor '''
Skip Gram 模型是 Word2Vec 这篇论文的网络架构,下面我们来讲一讲这个模型。
skip-gram 模型非常简单,我们在一段文本中训练一个简单的网络,这个网络的任务是通过一个词周围的词来预测这个词,然而我们实际上要做的就是训练我们的词嵌入。
比如我们给定一句话中的一个词,看看它周围的词,然后随机挑选一个,我们希望网络能够输出一个概率值,这个概率值能够告诉我们到底这个词离我们选择的词的远近程度,比如这么一句话 ‘A dog is playing with a ball’,如果我们选的词是 ‘ball’,那么 ‘playing’ 就要比 ‘dog’ 离我们选择的词更近。
参考:https://blog.csdn.net/qq_24003917/article/details/80389976
Word2Vec模型实际上分为了两个部分,第一部分为建立模型,第二部分是通过模型获取嵌入词向量。
Word2Vec的整个建模过程实际上与自编码器(auto-encoder)的思想很相似,即先基于训练数据构建一个神经网络,当这个模型训练好以后,我们并不会用这个训练好的模型处理新的任务,我们真正需要的是这个模型通过训练数据所学得的参数,例如隐层的权重矩阵——后面我们将会看到这些权重在Word2Vec中实际上就是我们试图去学习的“word vectors”。
接下来我们来看看如何训练我们的神经网络。假如我们有一个句子**“The dog barked at the mailman”。
首先我们随机选句子中间的一个词作为我们的输入词**,例如我们选取“dog”作为input word;
有了input word以后,我们再定义一个叫做skip_window的参数,它代表着我们从当前input word的一侧(左边和右边)选取词的数量。如果我们设置skip_window=2,那么我们最终获得窗口中的词(包括input word在内)就是[‘The’, ‘dog’,‘barked’, ‘at’]。skip_window=2代表着选取左input word=‘dog’左侧2个词和右侧2个词进入我们的窗口。另一个参数叫num_skips,它代表着我们从整个窗口中选取多少个不同的词作为我们的output word,当skip_window=2,num_skips=2时,我们将会得到两组 (input word, output word) 形式的训练数据,即 (‘dog’, ‘barked’),(‘dog’, ‘the’)。
神经网络基于这些训练数据将会输出一个概率分布,这个概率代表着我们的词典中的每个词是output word的可能性。第二步中我们在设置skip_window和num_skips=2的情况下获得了两组训练数据。假如我们先拿一组数据 (‘dog’, ‘barked’) 来训练神经网络,那么模型通过学习这个训练样本,会告诉我们词汇表中每个单词是“barked”的概率大小。
模型的输出概率代表着到我们词典中每个词有多大可能性跟input word同时出现。举个栗子,如果我们向神经网络模型中输入一个单词“Soviet“,那么最终模型的输出概率中,像“Union”, ”Russia“这种相关词的概率将远高于像”watermelon“,”kangaroo“非相关词的概率。因为”Union“,”Russia“在文本中更大可能在”Soviet“的窗口中出现。我们将通过给神经网络输入文本中成对的单词来训练它完成上面所说的概率计算。下面的图中给出了一些我们的训练样本的例子。我们选定句子“The quick brown fox jumps over lazy dog”,设定我们的窗口大小为2(window_size=2),也就是说我们仅选输入词前后各两个词和输入词进行组合。下图中,蓝色代表input word,方框内代表位于窗口内的单词。
我们的模型将会从每对单词出现的次数中习得统计结果。例如,我们的神经网络可能会得到更多类似(“Soviet“,”Union“)这样的训练样本对,而对于(”Soviet“,”Sasquatch“)这样的组合却看到的很少。因此,当我们的模型完成训练后,给定一个单词”Soviet“作为输入,输出的结果中”Union“或者”Russia“要比”Sasquatch“被赋予更高的概率。
我们如何来表示这些单词呢?首先,我们都知道神经网络只能接受数值输入,我们不可能把一个单词字符串作为输入,因此我们得想个办法来表示这些单词。最常用的办法就是基于训练文档来构建我们自己的词汇表(vocabulary)再对单词进行one-hot编码。
假设从我们的训练文档中抽取出10000个唯一不重复的单词组成词汇表。我们对这10000个单词进行one-hot编码,得到的每个单词都是一个10000维的向量,向量每个维度的值只有0或者1,假如单词ants在词汇表中的出现位置为第3个,那么ants的向量就是一个第三维度取值为1,其他维都为0的10000维的向量(ants=[0, 0, 1, 0, …, 0])。
还是上面的例子,“The dog barked at the mailman”,那么我们基于这个句子,可以构建一个大小为5的词汇表(忽略大小写和标点符号):(“the”, “dog”, “barked”, “at”, “mailman”),我们对这个词汇表的单词进行编号0-4。那么”dog“就可以被表示为一个5维向量[0, 1, 0, 0, 0]。
模型的输入如果为一个10000维的向量,那么输出也是一个10000维度(词汇表的大小)的向量,它包含了10000个概率,每一个概率代表着当前词是输入样本中output word的概率大小。
下图是我们神经网络的结构:
隐层没有使用任何激活函数,但是输出层使用了sotfmax。
我们基于成对的单词来对神经网络进行训练,训练样本是 ( input word, output word ) 这样的单词对,input word和output word都是one-hot编码的向量。最终模型的输出是一个概率分布。
说完单词的编码和训练样本的选取,我们来看下我们的隐层。如果我们现在想用300个特征来表示一个单词(即每个词可以被表示为300维的向量)。那么隐层的权重矩阵应该为10000行,300列(隐层有300个结点)。
Google在最新发布的基于Google news数据集训练的模型中使用的就是300个特征的词向量。词向量的维度是一个可以调节的超参数(在Python的gensim包中封装的Word2Vec接口默认的词向量大小为100, window_size为5)。
看下面的图片,左右两张图分别从不同角度代表了输入层-隐层的权重矩阵。左图中每一列代表一个10000维的词向量和隐层单个神经元连接的权重向量。从右边的图来看,每一行实际上代表了每个单词的词向量。
所以我们最终的目标就是学习这个隐层的权重矩阵。
我们现在回来接着通过模型的定义来训练我们的这个模型。
上面我们提到,input word和output word都会被我们进行one-hot编码。仔细想一下,我们的输入被one-hot编码以后大多数维度上都是0(实际上仅有一个位置为1),所以这个向量相当稀疏,那么会造成什么结果呢。如果我们将一个1 x 10000的向量和10000 x 300的矩阵相乘,它会消耗相当大的计算资源,为了高效计算,它仅仅会选择矩阵中对应的向量中维度值为1的索引行(这句话很绕),看图就明白。
我们来看一下上图中的矩阵运算,左边分别是1 x 5和5 x 3的矩阵,结果应该是1 x 3的矩阵,按照矩阵乘法的规则,结果的第一行第一列元素为0 x 17 + 0 x 23 + 0 x 4 + 1 x 10 + 0 x 11 = 10,同理可得其余两个元素为12,19。如果10000个维度的矩阵采用这样的计算方式是十分低效的。
为了有效地进行计算,这种稀疏状态下不会进行矩阵乘法计算,可以看到矩阵的计算的结果实际上是矩阵对应的向量中值为1的索引,上面的例子中,左边向量中取值为1的对应维度为3(下标从0开始),那么计算结果就是矩阵的第3行(下标从0开始)—— [10, 12, 19],这样模型中的隐层权重矩阵便成了一个”查找表“(lookup table),进行矩阵计算时,直接去查输入向量中取值为1的维度下对应的那些权重值。隐层的输出就是每个输入单词的“嵌入词向量”。
经过神经网络隐层的计算,ants这个词会从一个1 x 10000的向量变成1 x 300的向量,再被输入到输出层。输出层是一个softmax回归分类器,它的每个结点将会输出一个0-1之间的值(概率),这些所有输出层神经元结点的概率之和为1。
下面是一个例子,训练样本为 (input word: “ants”, output word: “car”) 的计算示意图。
如果两个不同的单词有着非常相似的“上下文”(也就是窗口单词很相似,比如“Kitty climbed the tree”和“Cat climbed the tree”),那么通过我们的模型训练,这两个单词的嵌入向量将非常相似。
那么两个单词拥有相似的“上下文”到底是什么含义呢?比如对于同义词“intelligent”和“smart”,我们觉得这两个单词应该拥有相同的“上下文”。而例如”engine“和”transmission“这样相关的词语,可能也拥有着相似的上下文。
实际上,这种方法实际上也可以帮助你进行词干化(stemming),例如,神经网络对”ant“和”ants”两个单词会习得相似的词向量。
词干化(stemming)就是去除词缀得到词根的过程。
首先介绍N Gram模型的原理和它要解决的问题。在一篇文章中,每一句话都是由很多单词组成的,而且这些单词的排列顺序也是非常重要的。在一句话中,是否可以由前面几个词来预测这些词后面的一个单词?比如在“I lived in France for 10 years,I can speak_.”这句话中,我们希望能够预测最后这个词是French。
知道想要解决的问题后,就可以引出N Gram语言模型了。对于一句话T,它由
w
1
,
w
2
,
⋅
⋅
⋅
w
n
w_1,w_2,· · · w_n
w1,w2,⋅⋅⋅wn这n个词构成,可以得到下面的公式:
但是这样的一个模型存在着一些缺陷,比如参数空间过大,预测一个词需要前面所有的词作为条件来计算条件概率,所以在实际中没办法使用。为了解决这个问题,引入了马尔科夫假设,也就是说这个单词只与前面的几个词有关系,并不是和前面所有的词都有关系,有了这个假设,就能够在实际中使用N Gram模型了。
CONTEXT_SIZE = 2 # 依据的单词数 EMBEDDING_DIM = 10 # 词向量的维度 # 我们使用莎士比亚的诗 test_sentence = """When forty winters shall besiege thy brow, And dig deep trenches in thy beauty's field, Thy youth's proud livery so gazed on now, Will be a totter'd weed of small worth held: Then being asked, where all thy beauty lies, Where all the treasure of thy lusty days; To say, within thine own deep sunken eyes, Were an all-eating shame, and thriftless praise. How much more praise deserv'd thy beauty's use, If thou couldst answer 'This fair child of mine Shall sum my count, and make my old excuse,' Proving his beauty by succession thine! This were to be new made when thou art old, And see thy blood warm when thou feel'st it cold.""".split()
CONTEXT_SIZE表示想由前面的几个单词来预测这个单词,这里设置为2,就是说我们希望通过这个单词的前两个单词来预测这一个单词,EMBEDDING_DIM表示词嵌入的维数。接着建立训练集,遍历所有语料来创建,将数据整理好,需要将单词分三个组,每个组前两个作为传入的数据,而最后一个作为预测的结果。
trigram = [((test_sentence[i], test_sentence[i+1]), test_sentence[i+2])
for i in range(len(test_sentence)-2)]
# 总的数据量
print(len(trigram))
#113
# 取出第一个数据看看
print(trigram[0])
#(('When', 'forty'), 'winters')
# 建立每个词与数字的编码,据此构建词嵌入
vocb = set(test_sentence) # 使用 set 将重复的元素去掉
word_to_idx = {word: i for i, word in enumerate(vocb)}
idx_to_word = {word_to_idx[word]: word for word in word_to_idx}
print(word_to_idx)
#输出: {"'This": 74, 'And': 76, 'How': 93, 'If': 89, 'Proving': 68, 'Shall': 64, 'Then': 70, 'This': 25, 'Thy': 28, 'To': 88, 'Were': 86, 'When': 81, 'Where': 75, 'Will': 45, 'a': 91, 'all': 37, 'all-eating': 4, 'an': 16, 'and': 55, 'answer': 49, 'art': 57, 'asked,': 48, 'be': 96, 'beauty': 60, "beauty's": 40, 'being': 84, 'besiege': 7, 'blood': 54, 'brow,': 21, 'by': 90, 'child': 83, 'cold.': 30, 'couldst': 0, 'count,': 20, 'days;': 2, 'deep': 56, "deserv'd": 8, 'dig': 31, "excuse,'": 61, 'eyes,': 67, 'fair': 36, "feel'st": 53, 'field,': 18, 'forty': 79, 'gazed': 95, 'held:': 3, 'his': 66, 'in': 80, 'it': 47, 'lies,': 34, 'livery': 15, 'lusty': 94, 'made': 73, 'make': 42, 'mine': 71, 'more': 78, 'much': 52, 'my': 29, 'new': 63, 'now,': 77, 'of': 11, 'old': 39, 'old,': 72, 'on': 65, 'own': 32, 'praise': 10, 'praise.': 6, 'proud': 33, 'say,': 1, 'see': 13, 'shall': 26, 'shame,': 27, 'small': 87, 'so': 24, 'succession': 51, 'sum': 82, 'sunken': 23, 'the': 62, 'thine': 44, 'thine!': 43, 'thou': 14, 'thriftless': 22, 'thy': 9, 'to': 59, "totter'd": 69, 'treasure': 35, 'trenches': 85, 'use,': 5, 'warm': 12, 'weed': 38, 'were': 92, 'when': 17, 'where': 50, 'winters': 19, 'within': 41, 'worth': 58, "youth's": 46}
从上面可以看到每个词都对应一个数字,且这里的单词都各不相同。接着我们定义模型,模型的输入就是前面的两个词,输出就是预测单词的概率。
import torch from torch import nn import torch.nn.functional as F from torch.autograd import Variable # 定义N Gram模型 class n_gram(nn.Module): def __init__(self, vocab_size, context_size=CONTEXT_SIZE, n_dim=EMBEDDING_DIM): #所有单词数,预测单词所依赖的单词数CONTEXT_SIZE,词向量的维度 super(n_gram, self).__init__() self.embed = nn.Embedding(vocab_size, n_dim) self.classify = nn.Sequential( nn.Linear(context_size * n_dim, 128), nn.ReLU(True), nn.Linear(128, vocab_size) ) def forward(self, x): voc_embed = self.embed(x) # 得到词嵌入 voc_embed = voc_embed.view(1, -1) # 将两个词向量拼在一起 out = self.classify(voc_embed) return out #最后我们输出就是条件概率,相当于是一个分类问题,我们可以使用交叉熵来方便地衡量误差
网络在向前传播中,首先传入单词得到词向量,模型是根据前面两个词预测第三个词的,所以需要传入两个词,得到的词向量是(2,100),然后将词向量展开成(1,200),接着经过线性变换,经过relu激活函数,再经过一个线性变换,输出的维数是单词总数,最后经过一个logsoftmax激活函数得到概率分布,最大化条件概率,可以用下面的公式表示:
在网络的训练中,不仅会更新线性层的参数,还会更新词嵌入中的参数。
net = n_gram(len(word_to_idx)) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(net.parameters(), lr=1e-2, weight_decay=1e-5) for e in range(100): train_loss = 0 for word, label in trigram: # 使用前 100 个作为训练集 word = Variable(torch.LongTensor([word_to_idx[i] for i in word])) # 将两个词作为输入 label = Variable(torch.LongTensor([word_to_idx[label]])) # 前向传播 out = net(word) loss = criterion(out, label) train_loss += loss # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() if (e + 1) % 20 == 0: print('epoch: {}, Loss: {:.6f}'.format(e + 1, train_loss / len(trigram))) #最后我们可以测试一下结果 net = net.eval() # 测试一下结果 word, label = trigram[19] print('input: {}'.format(word)) print('label: {}'.format(label)) print() word = Variable(torch.LongTensor([word_to_idx[i] for i in word])) out = net(word) pred_label_idx = out.max(1)[1] predict_word = idx_to_word[float(pred_label_idx)] print('real word is {}, predicted word is {}'.format(label, predict_word))
可以看到网络在训练集上基本能够预测准确,不过这里样本太少,特别容易过拟合。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。