词嵌入模型
目录
- 词嵌入模型
- Glove的具体实现
- Glove的训练方式
- 代码
- 使用one-hot表示的缺点
- 特征化的表示
- 使用词嵌入的步骤
- 词嵌入的特性
- 余弦相似度
- 嵌入矩阵
- Word2Vec
- Skip-Gram 模型
- CBOW模型
- 词汇表征(Word Representation)
- 负采样
- Glove词向量
词汇表征(Word Representation)
使用one-hot表示的缺点
1.如果要表示的词语个数比较多的话,one—hot会很占空间。
2.如"I want a glass of orange ()"与"I want a glass of apple ()",填入的词语可以是juice,如果第一句话模型学习到的词语是juice,但是因为词语是one—hot向量表示,所以orange与apple之间的关系同orange与其他词并没有相对拉近,所以不利于模型的泛化。
特征化的表示
所以我们开始使用特征化的向量进行词语的表示,比如词语相对性别的关系:man可能为1,woman就是-1,king就是0.95,queen就是-0.97,而apple与orange没有性别可言。比如词语相对高贵:king与queen就相对分值较高。
假设最后的特征一共有300个,那么对于一个词语的表述就是使用一个300维的特征向量,而且orange与apple的词语表示会十分接近。
这样的表示方法就是词嵌入。
使用词嵌入的步骤
1.第一步,先从大量的文本集中学习词嵌入。一个非常大的文本集,或者可以下载网上预 训练好的词嵌入模型,网上你可以找到不少,词嵌入模型并且都有许可。
2.第二步,你可以用这些词嵌入模型把它迁移到你的新的只有少量标注训练集的任务中, 比如说用这个 300 维的词嵌入来表示你的单词。这样做的一个好处就是你可以用更低维度的特征向量代替原来的 10000 维的 one-hot 向量,现在你可以用一个 300 维更加紧凑的向量。尽管 one-hot 向量很快计算,而学到的用于词嵌入的 300 维的向量会更加紧凑。
3.第三步,当你在你新的任务上训练模型时,在你的命名实体识别任务上,只有少量的标记数据集上,你可以自己选择要不要继续微调,用新的数据调整词嵌入。实际中,只有这个第二步中有很大的数据集你才会这样做,如果你标记的数据集不是很大,通常我不会在微调词嵌入上费力气。
词嵌入的特性
词嵌入的另外一个重要特性就是它还能帮助实现类比推理。意思就是模型已经知道了man对应的单词是woman,那么king对应的单词是什么?类比推理就是用来得出queen的算法。
这个类比推理的主要方法就是基于向量的加减法。
这个结果表示,man 和 woman 主要的差异是 gender(性别)上的差异,而 king 和 queen 之间的主要差异,根据向量的表示,也是 gender(性别)上的差异。
每一个词语经过词嵌入之后的表示方式是向量,在寻找词语queen的过程中,算法要计算的公式是\(e_{man}-e_{woman}\approx e_{king}-e_{w}\),我们希望\(e_{w}\)表示的单词是queen。将\(e_w\)单放到等式的一边变为$e_{w} \approx e_{king}-e_{man}+e_{woman} $,于是我们需要一个能判断式子两边相似度的函数————余弦相似度。
余弦相似度
为了测量两个词的相似程度,我们需要一种方法来测量两个词的两个嵌入向量之间的相 似程度。给定两个向量????和????,余弦相似度定义如下:
\[CosineSimilarity(u,v)=\frac{u,v}{\Vert u\Vert_2 \Vert v\Vert_2 }=cos(\theta) \]
其中????,????是两个向量的点积(或内积),\(\Vert u\Vert_2\)是向量????的范数(或长度),并且????是向量????和????之间的角度。这种相似性取决于角度在向量????和????之间。如果向量????和????非常相似,它 们的余弦相似性将接近1;如不相似,则余弦相似性将取较小的值。
嵌入矩阵
将词嵌入具体化时,实际上是具体化一个嵌入矩阵。
加入我们的词汇表中含有10000个单词,每个单词使用一个300维的向量表示,那么嵌入矩阵的形状为300 * 10000,之前我们使用每个单词的one-hot的形状为10000*1,将两者相乘即为300维的向量,表示当前单词的嵌入向量。
通过嵌入矩阵和one-hot向量相乘的方式,实际上是很耗资源的,我们在实际项目中,需要使用一个单独的函数算法快速的寻找到嵌入矩阵的某列。
Word2Vec
Skip-Gram 模型
Skip-Gram 模型是给定输入词语来预测上下文。
从距离目标词一定词距内随机选取一个词作为目标词的上下文词,构造一个监督学习问题。这样的话我们就是学习一个映射关系(比如orange为10000个单词中的第6257个单词,而juice是第4834个,于是就是要学习一个6257->4834的映射。)
设\(e_c=EO_c\)表示输入的上下文词的嵌入向量,则预测不同目标词的概率为:
\[Softmax:p(t|c)=\frac{e^{\theta^T_te_c}}{\sum^{10000}_{j=1}e^{\theta^T_te_c}} \]
损失函数的表达为:
\[L(\hat{y},y)=-\sum^{10000}_{i=1}y_i\log{\hat{y_i}} \]
Skip-Gram 模型其实分为两个部分,第一部分为建立模型,第二部分是通过模型获取嵌入词向量。他的意思就是首先基于训练数据构建一个神经网络,当这个模型训练好以后,并不会使用这个模型处理新的数据,而是要获取这个模型之前学习到的参数(隐层的权重矩阵),这些参数其实就是我们一直所求的词向量。
第一部分:建立模型
假如我们有一个句子“The dog barked at the mailman”。
1.我们选择dog作为我们的输入词。
2.定义skip_window的大小,它代表从当前input word的一侧(左边或右边)选取词的数量。如果skip_window=2,则窗口中的词就是(包括输入词在内):【The,dog,barked,at】
3.定义num_skips的大小,它代表从整个窗口中选取多少个不同的词作为输出词。
模型的输出概率代表着到我们词典中每个词有多大可能性跟输入词同时出现。
第二部分:获取词向量
开始时的输入为1X10000;到一个10000X300的隐层,输出一个1X300的词向量;再到输出层(softmax分类器)输出每个结点的权重值(0~1).
分级softmax分类器
就是开始的时候不会一下子告诉你预测的词属于10000个词中的哪一个词,而是先告诉你预测的词是前5000个词还是后5000个词;第二个词则判断对应的词是前2500还是后2500.像这样有一个树形的分类器,意味着树上内部的每一个节点都可以是一个二分类器, 比如逻辑回归分类器,所以你不需要再为单次分类,对词汇表中所有的 10,000 个词求和了。 而且较为常用的词都会在搜索树的上面就被判断,而较为生僻的词就会在数的底部,这样更快他完成词语的预测。
CBOW模型
它获得中间词两边的上下文,然后用周围的词去预测中间的词。
负采样
之前的skip-Gram模型拥有大规模的权重矩阵,所有的这些权重需要通过我们数以亿计的训练样本来进行调整,这是非常消耗计算资源的,并且实际中训练起来会非常慢,而负采样则是一个更加有效的学习算法。
它是用来提高训练速度并且改善所得到词向量的质量的一种方法。不同于原本每个训练样本更新所有的权重,负采样每次让一个训练样本仅仅更新一小部分的权重,这样就会降低梯度下降过程中的计算量。
当我们用训练样本 ( input word: "fox",output word: "quick") 来训练我们的神经网络时,“fox”和“quick”都是经过one-hot编码的。如果我们的vocabulary大小为10000时,在输出层,我们期望对应“quick”单词的那个神经元结点输出1,其余9999个都应该输出0。在这里,这9999个我们期望输出为0的神经元结点所对应的单词我们称为negative word。
当使用负采样时,我们将随机选择一小部分的negative words(比如选5个negative words)来更新对应的权重。我们也会对我们的“positive” word进行权重更新(在我们上面的例子中,这个单词指的是“quick”)。
在论文中,作者指出指出对于小规模数据集,选择5-20个negative words会比较好,对于大规模数据集可以仅选择2-5个negative words。
我们使用“一元模型分布(unigram distribution)”来选择“negative words”。
要注意的一点是,一个单词被选作negative sample的概率跟它出现的频次有关,出现频次越高的单词越容易被选作negative words。采样的函数为:
\[p(w_i)=\frac{f(w_i)^{\frac{3}{4}}}{\sum^{10000}_{j=1}f(w_i)^{\frac{3}{4}}} \]
所以不使用一个巨大的 10,000 维度的 softmax,因为计算成本很高,而是把它转变为 10,000 个二分类问题,每个都很容易计算,每次迭代我们要做的只是训练它们其中的 5 个,一般而言 就是???? + 1个,其中????个负样本和 1 个正样本。这也是为什么这个算法计算成本更低,因为 只需更新???? + 1个逻辑单元,???? + 1个二分类问题,相对而言每次迭代的成本比更新 10,000 维 的 softmax 分类器成本低。
Glove词向量
Glove的全称是Global Vectors for Word Representation,中文意思是全局词向量,它是一个基于全局词频统计(count-based&overall statistics)的词表征(word representation)工具。
跟word2vec一样,它可以把一个单词表示成一个由实数组成的向量,向量可以捕捉单词之间的一些语义特性,如相似性(similarity)、类比性(analogy)。并且通过对向量的运算,如欧几里得距离或cosine相似度,可以计算两个单词之间的语义相似性。
Glove的具体实现
根据语料库构建一个共现矩阵,矩阵中的每一个元素\(X_{ij}\)代表单词i与单词j在特定大小的上下文窗口内共同出现的次数。一般,这个次数的最小值是1,但Glove做了进一步处理:它根据两个单词在上下文窗口的距离d,提出了一个衰减函数\(decay=1/d\)用于计算权重,也就是说距离越远的两个单词所占总计数的权重越小。
词向量与共现矩阵之间的近似关系:
\[w^T_iw^2_j+b_i+b^2_j=\log(X_{ij}) \]
其中\(w^T_i\)与\(w^2_j\)时要求的词向量,\(b_i\)与\(b^2_j\)分别是词向量的偏置项。
loss function:
\[J=\sum^V_{i,j=1}f(X_{ij})(w^T_iw^2_j+b_i+b^2_j-\log(X_{ij}))^2 \]
这个损失函数是最简单的mean square loss,只不过在此基础上增加了一个权重函数\(f(X_{ij})\)。它的作用在于:对于在一个语料库中经常一起出现的单词(frequent occurrence)
- 这些单词的权重要大于那些很少在一起出现的单词(rare occurrence),所以这个函数是非递减函数;
- 这些单词的权重也不能太大(overweighted),当到达一定程度之后应该不再增加;
- 如果两个单词没有在一起出现,即\(X_{ij}=0\),那它们不应该参与到loss function中的计算中去,即\(f(0)=0\)
文中,作者采用了符合上述条件的分段函数:
\[f(x)=\left\{ \begin{aligned}(x/x_{max})^\alpha,if x < x_{max}\\ 1, otherwise \end{aligned} \right. \]
\(\alpha\)取值为0.75,\(x_{max}\)的取值为1000.
Glove的训练方式
虽然很多人声称Glove是一种无监督(unsupervised learning)的学习方式,即不需要人工标注数据,但实际上它还是有标签的,这个标签就是\(\log(X_{ij})\),而向量\(x_i\)与\(x_j^2\)就 是要不断更新学习的参数。因此,本质上它的训练方式和监督学习的训练方式没有什么不同,都是基于梯度下降的。
训练的具体做法是:采用AdaGrad的梯度下降算法,对矩阵\(X\)中的所有非零元素进行随机采样,学习率设置为0.05,在vector size小于300的情况下迭代50次,其他大小的vector size迭代100次,直至收敛。
因为\(X\)是对称的,所以最终学到的两个词向量\(w_i\)和\(w_j^2\)应该也是对称的,等价的,只不过由于初始值不一样,导致最终的值不一样。为了提高鲁棒性,最终选择两者之和\(w_i+w_j^2\)作为最终的词向量(两者的初始化不同相当于加了不同的随机噪声所以能提高鲁棒性)。
代码
import numpy as np DEFAULT_FILE_PATH = "utils/datasets/glove.6B.50d.txt" def loadWordVectors(tokens, filepath=DEFAULT_FILE_PATH, dimensions=50): """Read pretrained GloVe vectors""" wordVectors = np.zeros((len(tokens), dimensions)) with open(filepath) as ifs: for line in ifs: line = line.strip() if not line: continue row = line.split() token = row[0] if token not in tokens: continue data = [float(x) for x in row[1:]] if len(data) != dimensions: raise RuntimeError("wrong number of dimensions") wordVectors[tokens[token]] = np.asarray(data) return wordVectors