语言模型
语言(人说的话)+模型(完成某个任务)
任务:
- 概率评估任务:在两句话中,判断哪句话出现的概率大(哪句话在自然语言中更合理)
- 生成任务 :预测词语,我明天要
____
统计语言模型
用统计的方法解决上述的两个任务
核心思想
给定一个词序列,计算该序列出现的概率
比如句子:"判断这个词的词性",分词得到"判断","这个","词","的","词性"
这句话是有顺序的(是一个序列),怎么理解(?)
就是自然语言我们所说的语序
条件概率链式法则
用于解决第一个任务
统计语言模型的核心是计算一个句子 P(w_1,w_2,...,w_n) 的联合概率,即一句话出现的概率(w_i就是单个词)\\ P(w_1,w_2,\\cdots,w_n) = P(w_1)\\cdot P(w_2\|w_1)\\cdots P(w_n\|w_1,w_2,\\cdots,w_{n-1}) = \\prod_{i}P(w_i \| w_1,w_2,\\cdots,w_{i-1})\\
(?)理解:求出每个词wi的概率,然后连乘,就是这个句子的概率;
为什么是条件概率(?)
因为句子是一个序列,计算当前词的概率需要在前面的词已经确定的前提下(每个词是基于上下文的)
词库/词典
解决第二个任务:生成任务 我明天要____
令要生成的词为wnext,其概率为(1)P(wnext∣"我","明天","要") 令要生成的词为w_{next},其概率为\\ (1) P(w_{next} | "我","明天","要") 令要生成的词为wnext,其概率为(1)P(wnext∣"我","明天","要")
思想
- 建立一个词典V,把所有词都装入V
- 把V中每个词都进行(1)式计算
缺点
- 计算太复杂了
- 词典很大,要匹配很多次计算
- 句子很复杂,分词后非常长,计算也很复杂
前面没有给出这个情境下的条件概率怎么计算
P(wnext∣"我","明天","要")=count(wnext,"我","明天","要")count("我","明天","要")count():表示某个词或词序列在语料库中出现的次数eg:我 明天 要 去 学校我 明天 要 去 玩我 明天 要 睡觉我 明天 要 上班我 明天 要 吃饭count("我","明天","要")=5 −我 明天 要 这三个词同时出现的次数 P(w_{next} | "我","明天","要") = \frac{count(w_{next},"我","明天","要")}{count("我","明天","要")} \\ count(): 表示某个词或词序列在语料库中出现的次数\\ eg:\\ 我 \ 明天\ 要\ 去\ 学校\\ 我\ 明天\ 要\ 去\ 玩\\ 我 \ 明天\ 要 \ 睡觉\\ 我 \ 明天\ 要 \ 上班\\ 我 \ 明天 \ 要\ 吃饭\\ count("我","明天","要")=5 \ \ \ -我\ 明天\ 要\ 这三个词同时出现的次数 P(wnext∣"我","明天","要")=count("我","明天","要")count(wnext,"我","明天","要")count():表示某个词或词序列在语料库中出现的次数eg:我 明天 要 去 学校我 明天 要 去 玩我 明天 要 睡觉我 明天 要 上班我 明天 要 吃饭count("我","明天","要")=5 −我 明天 要 这三个词同时出现的次数
这count()怎么算的(?)
在语料库中词序列出现次数,比如count("我","明天","要")要把"我","明天","要"
当成序列,再找出现次数,而不是分开每个词寻找
n-gram模型/n元统计语言模型
n-gram模型
用于预测一个词在给定前n-1个词的情况下出现的概率
引入
比如对于这样一个生成任务:
判断这个词的___
wnext="词性","火星"最原始的式子:P(wnext∣"判断","这个","词","的")但是也可以这样:P(wnext∣这个","词","的")还可以这样:P(wnext∣"词","的")三个式子都能达到选出最佳生成词语的作用,但是很显然最后一个式子要更加高效 w_next = {"词性","火星"}\\ 最原始的式子:P(w_{next}|"判断","这个","词","的")\\ 但是也可以这样:P(w_{next}|这个","词","的")\\ 还可以这样:P(w_{next}|"词","的")\\ 三个式子都能达到选出最佳生成词语的作用,但是很显然最后一个式子要更加高效 wnext="词性","火星"最原始的式子:P(wnext∣"判断","这个","词","的")但是也可以这样:P(wnext∣这个","词","的")还可以这样:P(wnext∣"词","的")三个式子都能达到选出最佳生成词语的作用,但是很显然最后一个式子要更加高效
定义
n-gram
是指连续的n
个词构成的序列。n
元模型(n-gram model)是基于前 n−1 个词来预测第 n 个词的模型。- 常见的有:
- unigram(n=1):不考虑上下文,只统计词频。
- bigram(n=2):基于前一个词预测下一个词。
- trigram(n=3):基于前两个词预测下一个词。
- 4-gram、5-gram 等:基于更多上下文。
最大似然估计MLE
P(wi∣wi−n+1,⋯ ,wi−1)=count(wi−n+1,⋯ ,wi−1,wi)count(wi−n+1,⋯ ,wi−1) P(w_i | w_{i-n+1},\cdots,w_{i-1}) = \frac{count(w_{i-n+1},\cdots,w_{i-1},w_i)}{count(w_{i-n+1},\cdots,w_{i-1})} P(wi∣wi−n+1,⋯,wi−1)=count(wi−n+1,⋯,wi−1)count(wi−n+1,⋯,wi−1,wi)
缺点:
- 有现实意义的文本,通常会出现数据稀疏的情况;例如训练时未出现的单词,在测试时出现
- 这样就会导致MLE的分子/分母为0,造成计算上的问题,我们需要引入一些平滑策略来避免0的出现
平滑策略
拉普拉斯平滑/加一平滑
P(wi∣wi−n+1,⋯ ,wi−1)=count(wi−n+1,⋯ ,wi−1,wi)+1count(wi−n+1,⋯ ,wi−1)+∣V∣∣V∣:词库大小 P(w_i | w_{i-n+1},\cdots,w_{i-1}) = \frac{count(w_{i-n+1},\cdots,w_{i-1},w_i)+1}{count(w_{i-n+1},\cdots,w_{i-1})+|V|}\\ |V|:词库大小 P(wi∣wi−n+1,⋯,wi−1)=count(wi−n+1,⋯,wi−1)+∣V∣count(wi−n+1,⋯,wi−1,wi)+1∣V∣:词库大小
加k平滑
P(wi∣wi−n+1,⋯ ,wi−1)=count(wi−n+1,⋯ ,wi−1,wi)+kcount(wi−n+1,⋯ ,wi−1)+k∣V∣k:一个小于1的数,可以使用交叉验证进行选择∣V∣:词库大小 P(w_i | w_{i-n+1},\cdots,w_{i-1}) = \frac{count(w_{i-n+1},\cdots,w_{i-1},w_i)+k}{count(w_{i-n+1},\cdots,w_{i-1})+k|V|}\\ k:一个小于1的数,可以使用交叉验证进行选择\\ |V|:词库大小 P(wi∣wi−n+1,⋯,wi−1)=count(wi−n+1,⋯,wi−1)+k∣V∣count(wi−n+1,⋯,wi−1,wi)+kk:一个小于1的数,可以使用交叉验证进行选择∣V∣:词库大小
Kneser-Ney 平滑
核心思想:一个词是否可以作为多个上下文的结尾
PKN(wi∣context)=max(count(wi,context),D,0)count(context)+λ(context)Pcont(wi)D:折扣因子λ:归一化系数λ=D⋅N1+(context)C(context)N1+(Context):表示有多少个不同的wi使得C(wi,context)>0Pcont(wi):词wi作为上下文结尾的概率Pcont(wi)=Cprefix(wi)∑w′Cprefix(w′)Cprefix(wi):有多少个不同的词可以跟在wi前面组成bigram(n=2,根据前一个词预测下一个词)∑w′Cprefix(w′):所有词的Cprefix总和 P_{KN}(w_i|context) = \frac{\max(count(w_i,context),D,0)}{count(context)}+\lambda(context)P_{cont}(w_i)\\ D:折扣因子\\ \lambda:归一化系数\\ \lambda=\frac{D\cdot N_{1+}(context)}{C(context)}\\ N_{1+}(Context):表示有多少个不同的 w_i 使得 C(w_i,context)>0\\ P_{cont}(w_i):词w_i作为上下文结尾的概率\\ P_{cont}(w_i) = \frac{C_{prefix}(w_i)}{\sum_{w'}C_{prefix}(w')}\\ C_{prefix}(w_i):有多少个不同的词可以跟在w_i前面组成bigram(n=2,根据前一个词预测下一个词)\\ \sum_{w'}C_{prefix}(w'):所有词的C_{prefix}总和\\ PKN(wi∣context)=count(context)max(count(wi,context),D,0)+λ(context)Pcont(wi)D:折扣因子λ:归一化系数λ=C(context)D⋅N1+(context)N1+(Context):表示有多少个不同的wi使得C(wi,context)>0Pcont(wi):词wi作为上下文结尾的概率Pcont(wi)=∑w′Cprefix(w′)Cprefix(wi)Cprefix(wi):有多少个不同的词可以跟在wi前面组成bigram(n=2,根据前一个词预测下一个词)w′∑Cprefix(w′):所有词的Cprefix总和
例子:
我 明天 要 去 玩我 明天 要 去 学校生成bigram列表{(我,明天),(明天,要),(要,去),(去,玩)}{(我,明天),(明天,要),(要,去),(去,学校)}∑w′Cprefix(w′)=8Cprefix("明天")=2Pcont("明天")=28=14 我 \ 明天\ 要\ 去\ 玩\\ 我\ 明天\ 要\ 去\ 学校\\ \\ 生成bigram列表\\ \{(我,明天),(明天,要),(要,去),(去,玩)\}\\ \{(我,明天),(明天,要),(要,去),(去,学校)\}\\ \sum_{w'}C_{prefix}(w') = 8\\ C_{prefix}("明天")=2 \\ P_{cont}("明天")=\frac{2}{8}=\frac{1}{4} 我 明天 要 去 玩我 明天 要 去 学校生成bigram列表{(我,明天),(明天,要),(要,去),(去,玩)}{(我,明天),(明天,要),(要,去),(去,学校)}w′∑Cprefix(w′)=8Cprefix("明天")=2Pcont("明天")=82=41
问题(?)
公式的理解,对prefix一开始理解不太透彻,prefix就是:有多少个不同的词可以跟在w_i前面组成bigram(n=2,根据前一个词预测下一个词)
神经网络语言模型NNLM
这里提到NNLM是为了接下来引入Word2Vec
余弦相似度
cos(θ)=A⋅B∣∣A∣∣B∣∣=∑i=1nAiBi∑i=1nAi2∑i=1nBi2 cos(\theta) = \frac{A\cdot B}{||A||B||} = \frac{\sum_{i=1}^{n}A_iB_i}{\sqrt{\sum_{i=1}^{n}A_i^2}\sqrt{\sum_{i=1}^{n}B_i^2}} cos(θ)=∣∣A∣∣B∣∣A⋅B=∑i=1nAi2 ∑i=1nBi2 ∑i=1nAiBi
one-hot编码表示词的缺点

- 维度灾难:假设有m=10000个词
- 每个词的向量长度都是m,整体大小太大,存储开销很大
- 不能表示出词与词之间的关系
- 例如Man和Woman关系比较近,Man和Apple关系比较远,但是其实任取2个向量的内积都是0,也就是余弦相似度为0
流程

输入层/Embedding层
先随机生成矩阵Q,后面对Q进行训练
wi⋅Q=ci词矩阵C=[c1,c2,⋯ ,cn]w1×V:词的one−hot编码的向量QV×M:嵌入矩阵,类似于权重矩阵W(可学习)V:词典大小M:新的词向量大小 w_i \cdot Q = c_i \\ 词矩阵C = [c_1,c_2,\cdots,c_n]\\ w_{1\times V}:词的one-hot编码的向量\\ Q_{V\times M}:嵌入矩阵,类似于权重矩阵W(可学习)\\ V:词典大小\\ M:新的词向量大小 wi⋅Q=ci词矩阵C=[c1,c2,⋯,cn]w1×V:词的one−hot编码的向量QV×M:嵌入矩阵,类似于权重矩阵W(可学习)V:词典大小M:新的词向量大小
隐藏层/第一层感知机
h=Tanh(CW+b1) h = Tanh(CW+b_1) h=Tanh(CW+b1)
输出层/第二层感知机
O1×V=SoftMax(Uh+b2)=Softmax(U(Tanh(CW+b1))+b2) O_{1\times V} = SoftMax(Uh+b_2) = Softmax(U(Tanh(CW+b_1))+b_2) O1×V=SoftMax(Uh+b2)=Softmax(U(Tanh(CW+b1))+b2)
词向量 && 词嵌入-Word Embedding
能对one-hot编码的向量起到压缩的作用
(把一个维数为所有词的数量的高维空间嵌入到一个维数低得多的连续向量空间中,每个单词或词组被映射为实数域上的向量)
w1×V⋅QV×M=c1×Mci就称为词向量 w_{1\times V}\cdot Q_{V\times M} = c_{1\times M}\\ c_i就称为词向量 w1×V⋅QV×M=c1×Mci就称为词向量
词向量:用一个向量表示一个单词;所以one-hot编码也是词向量的一种形式
解决了任务:
- 概率评估任务:将词转换成词向量了,那么就可以输入到神经网络中,是二分类或者多分类问题
- 生成任务:NNLM中的softmax给出的概率分布

特点
- 能够体现出词与词之间的关系
- 比如用Man-Woman或者Apple-Orange,都能得到一个向量
- 能够找到相似词
- 例如Man-Woman = King-? (?=Queen)
Word2Vec
也可以理解成一种神经网络语言模型
与NNLM的区别
- NNLM:重点是预测下一个词
- Word2Vec:重点是得到
Q矩阵
重要假设:文本中离得越近的词语相似度越高

skip-gram(跳字模型)
使用中心词预测上下文词

例子
假设文本序列有5个词["The", "man", "loves", "his", "son"],中心词wt="love",背景窗口大小skip-window=2
那么上下文词(背景词)为"The", "man", "his", "son"
skip-gram
做的是:通过中心词wc,生成距离它不超过m=skip-window的背景词,用公式表示就是:
p=P(context∣wc)=P(wc−m,wc−m+1,⋯ ,wc+m−1,wc+m∣wc)=P(wc1,wc2,⋯ ,wc2m∣wc)如果在给定中心词的情况下,背景词是互相独立的,那么上面的公式可以写成:P(context∣wt)=P(wc1∣wc)P(wc2∣wc)⋯P(wc2m−1∣wt)P(wc2m∣wt)wcj:第j个上下文词wncj:第j个非上下文词 \begin{align} p = P(context | w_c) = P(w_{c-m},w_{c-m+1},\cdots,w_{c+m-1},w_{c+m}| w_c) = P(w_{c1},w_{c2},\cdots,w_{c2m}|w_c)\\ 如果在给定中心词的情况下,背景词是互相独立的,那么上面的公式可以写成:\\ P(context |w_t) = P(w_{c1}|w_c)P(w_{c2}|w_c)\cdots P(w_{c2m-1}|w_t)P(w_{c2m}|w_t)\\ w_{cj}:第j个上下文词\\ w_{ncj}:第j个非上下文词 \end{align} p=P(context∣wc)=P(wc−m,wc−m+1,⋯,wc+m−1,wc+m∣wc)=P(wc1,wc2,⋯,wc2m∣wc)如果在给定中心词的情况下,背景词是互相独立的,那么上面的公式可以写成:P(context∣wt)=P(wc1∣wc)P(wc2∣wc)⋯P(wc2m−1∣wt)P(wc2m∣wt)wcj:第j个上下文词wncj:第j个非上下文词
思想
使用一个词预测另一个词,尽量使这两个词的词向量接近
skip-gram
在迭代过程中,调整词向量:
- 中心词(目标词)的词向量与上下文词(背景词)的词向量尽可能接近
- 中心词词向量与非上下文词词向量尽可能远离

在skip-gram
中,衡量两个词向量相似程度,采用点积的方式
点积衡量了两个向量在同一方向上的强度,点积越大,两个向量越相似,他们对应的词语的语义就越接近
结构

流程
和CBOW的流程其实很相似
-
one-hot编码,将中心词转换成one-hot编码形式输入第一层嵌入层,用于计算词向量
-
word embedding;计算中心词的词向量v
vi=wi⋅W得到形状为1×d的中心词词向量vi v_i = w_i\cdot W \\ 得到形状为1\times d的中心词词向量v_i vi=wi⋅W得到形状为1×d的中心词词向量vi -
skip-gram模型训练,计算上下文词的词向量u
ui=vi⋅W′W′是d×V的矩阵,每一列对应单词作为上下文词的词向量ui \begin{align} u_i = v_i\cdot W' \\ W'是d\times V的矩阵,每一列对应单词作为上下文词的词向量u_i \end{align} ui=vi⋅W′W′是d×V的矩阵,每一列对应单词作为上下文词的词向量ui -
softmax计算概率分布
P(wc∣wt)=exp(ucT⋅vt)∑i=1Vexp(uiT⋅vt)vt:当前中心词词向量uc:当前中心词的上下文词的词向量 \begin{align} P(w_c |w_t) = \frac{exp(u_c^T\cdot v_t)}{\sum_{i=1}^{V}exp(u_i^T\cdot v_t)} \\ v_t:当前中心词词向量\\ u_c:当前中心词的上下文词的词向量 \end{align} P(wc∣wt)=∑i=1Vexp(uiT⋅vt)exp(ucT⋅vt)vt:当前中心词词向量uc:当前中心词的上下文词的词向量 -
极大化概率(求损失函数)
L=−logP(wc∣wt)=−(log(wt−m∣wt)+log(wt−m+1∣wt)+⋯+log(wt+m−1∣wt)+log(wt+m∣wt)) \begin{align} L = -logP(w_c|w_t) = -(log(w_{t-m}|w_t)+log(w_{t-m+1}|w_t)+\cdots+log(w_{t+m-1}|w_t)+log(w_{t+m}|w_t)) \end{align} L=−logP(wc∣wt)=−(log(wt−m∣wt)+log(wt−m+1∣wt)+⋯+log(wt+m−1∣wt)+log(wt+m∣wt)) -
反向传播更新参数矩阵
优化
在模型中出现的单词对或短语当成一个词
比如有些短语/单词对的意思和一个一个单词拼写出的意思完全不一样,将他们作为一个词
二次采样
在一个很大的语料库中,有一些出现频率很高的词(比如冠词a,the
,中文的的,地,得,了
之类的)
但是这些词,提供的信息不多,会导致我们很多训练是没用的
因此我们引入二次采样 的思路:每个单词都有一定概率被丢弃,这个概率为:
P(wi)=1−tf(wi)t:一个选定的阈值,通常是1e−5f(wi):单词wi出现的频率(次数与总单词数的比值) \begin{align} P(w_i) = 1-\sqrt{\frac{t}{f(w_i)}} \\ t:一个选定的阈值,通常是1e-5\\ f(w_i):单词w_i出现的频率(次数与总单词数的比值)\\ \end{align} P(wi)=1−f(wi)t t:一个选定的阈值,通常是1e−5f(wi):单词wi出现的频率(次数与总单词数的比值)
为什么t
取1e-5(?)
这是在 Word2Vec 原始论文和 Google 的实现中推荐的默认值
负采样
在 Word2Vec
的两个算法中,每接收一个训练样本,就需要调整隐藏层和输出层的权重参数 W,W'
,
来使神经网络预测的更加准确;也就是说,每个训练样本都将会调整所有神经网络中的参数
负采样的思路是:每次让样本仅仅更新一小部分的权重参数,从而降低梯度下降过程中的计算量
如何去理解?
在skip-gram
中,我们的目标是学习中心词和上下文词之间的语义关系,可以分为两种词:
- 正词/正样本(positive word):真正出现在当前词上下文的词(即给定的中心词wtw_twt及其上下文词wcw_cwc)
- 负词/负样本(negative word):随机从词汇表中采样出的词,不是当前词的上下文词,即与当前词没用上下文关系
那么为什么这样就可以更新小部分的权重矩阵了呢(?)
原本我们需要更新所有词对应的权重,现在我们只要更新正词以及采样的负词对应的权重
这里的权重指的是输出嵌入层参数W'
负样本的选择
选择的经验公式为:
P(wi)=f(wi)3/4∑j=1Vf(wj)3/4 P(w_i) = \frac{f(w_i)^{3/4}}{\sum_{j=1}^{V}f(w_j)^{3/4}} P(wi)=∑j=1Vf(wj)3/4f(wi)3/4
3/4是纯基于经验的选择
正样本概率
给定中心词w和上下文词qpq^=σ(xwT⋅θq)xw∈R1×N:中心词w的低维密集向量(词嵌入后的词向量)θq∈RN×1:输出参数矩阵W'∈RN×V中q对应的上下文向量对于正样本,我们希望pq^越大越好 \begin{align} 给定中心词w和上下文词q\\ \hat{p_q} = \sigma(x_w^T\cdot \theta^q) \\ x_w \in R^{1\times N}:中心词w的低维密集向量(词嵌入后的词向量) \\ \theta^q \in R^{N\times 1}:输出参数矩阵W' \in R^{N\times V} 中q对应的上下文向量\\ 对于正样本,我们希望\hat{p_q}越大越好 \end{align} 给定中心词w和上下文词qpq^=σ(xwT⋅θq)xw∈R1×N:中心词w的低维密集向量(词嵌入后的词向量)θq∈RN×1:输出参数矩阵W'∈RN×V中q对应的上下文向量对于正样本,我们希望pq^越大越好
为什么不再使用Softmax
而是使用sigmoid
了(?)
- 引入负采样后,问题可以被看成是一个二分类:是否是正样本?
- 而且
Softmax
分母需要计算所有词的exp()
,sigmoid
不用
负样本概率
\\begin{align} 选择负样本词u\\ \\hat{p_u} = \\sigma(w_w\^T\\cdot \\theta\^u) \\ 我们希望\\hat{p_q}越小越好 \\end{align}
总样本
给定(w,context(w)),可构造∣(w,context(w))∣个正样本对于每个正样本(w,q),q∈context(w),采样一组负样本NEG(q)进行对比学习则构造出∣NEG(q)∣个负样本,负样本为(w,u),u∈NEG(q)每个中心词w的损失函数设计为:Lw=∏q∈context(w)(pq^∏u∈NEG(q)(1−pu^))对于语料库C:LC=∏w∈CLw为了计算方便,进行取对数操作:Lc=∑w∈ClogLw=∑w∈C∑q∈context(w)log(pq^∏u∈NEG(q)(1−pu^))=∑w∈C∑q∈context(w)(logpq^+∑u∈NEG(q)log(1−pu^)) \begin{align} 给定(w,context(w)),可构造|(w,context(w))|个正样本\\ 对于每个正样本(w,q),q \in context(w),采样一组负样本NEG(q)进行对比学习\\ 则构造出|NEG(q)|个负样本,负样本为(w,u),u \in NEG(q) \\ 每个中心词w的损失函数设计为: \\ L_w = \prod_{q \in context(w)}(\hat{p_q} \prod_{u \in NEG(q)}(1-\hat{p_u}))\\ 对于语料库C:\\ L_C = \prod_{w \in C}L_w\\ 为了计算方便,进行取对数操作:\\ L_c = \sum_{w \in C}logL_w \\ =\sum_{w \in C}\sum_{q \in context(w)}log(\hat{p_q}\prod_{u \in NEG(q)}(1-\hat{p_u}))\\ =\sum_{w \in C}\sum_{q \in context(w)}(log\hat{p_q}+\sum_{u \in NEG(q)}log(1-\hat{p_u}))\\ \end{align} 给定(w,context(w)),可构造∣(w,context(w))∣个正样本对于每个正样本(w,q),q∈context(w),采样一组负样本NEG(q)进行对比学习则构造出∣NEG(q)∣个负样本,负样本为(w,u),u∈NEG(q)每个中心词w的损失函数设计为:Lw=q∈context(w)∏(pq^u∈NEG(q)∏(1−pu^))对于语料库C:LC=w∈C∏Lw为了计算方便,进行取对数操作:Lc=w∈C∑logLw=w∈C∑q∈context(w)∑log(pq^u∈NEG(q)∏(1−pu^))=w∈C∑q∈context(w)∑(logpq^+u∈NEG(q)∑log(1−pu^))
CBOW(连续词袋模型)
使用上下文词预测中心词

前向传播
embeddings层
embeddings层其实是输入层
和投影层
的结合,负责接受输入的词索引,并映射成词向量后输出
输入的one−hot编码向量为w1×Vembeddings层是一个V×M的矩阵Q,其中:V:词典中词语个数M:词向量维度Q就是我们最终希望得到的嵌入矩阵 \begin{align} 输入的one-hot编码向量为w_{1\times V}\\ embeddings层是一个V\times M的矩阵Q,其中:\\ V:词典中词语个数\\ M:词向量维度\\ Q就是我们最终希望得到的嵌入矩阵\\ \end{align} 输入的one−hot编码向量为w1×Vembeddings层是一个V×M的矩阵Q,其中:V:词典中词语个数M:词向量维度Q就是我们最终希望得到的嵌入矩阵
一个词的上下文会包含多个词语,这些词语会被同时输入embeddings层,每个词语都会转换成一个词向量
w1Q=c1w2Q=c2⋮wkQ=ckC=[c1,c2,⋯ ,ck]C是序列对应的词向量矩阵,每个元素ci(每一行)对应一个词的word embedding输入embeddings层k个上下文词,得到k个对应的词向量需要对多个上下文词进行统一表示:h=1k∑i=1kci \begin{align} w_1Q = c_1 \\ w_2Q = c_2\\ \vdots\\ w_kQ = c_k\\ C = [c_1,c_2,\cdots,c_k]\\ C是序列对应的词向量矩阵,每个元素c_i(每一行)对应一个词的word\ embedding\\ 输入embeddings层k个上下文词,得到k个对应的词向量\\ 需要对多个上下文词进行统一表示:\\ h = \frac{1}{k}\sum_{i=1}^{k}c_i \end{align} w1Q=c1w2Q=c2⋮wkQ=ckC=[c1,c2,⋯,ck]C是序列对应的词向量矩阵,每个元素ci(每一行)对应一个词的word embedding输入embeddings层k个上下文词,得到k个对应的词向量需要对多个上下文词进行统一表示:h=k1i=1∑kci
embeddings层的输出结果就是:将语义信息平均的向量h
embeddings层的本质就是一个查找表(look up table
),因为其每一行都对应着一个词向量(如图所示)

优点
- embeddings层是可训练的,也就是说每一行对应的词向量可以更新,使最终词与词之间的语义关系能更好的表示
- 可以不进行矩阵运算,仅进行查表操作,效率高很多
线性层/输出层
特点:不设置激活函数
前面的embeddings层输入进来的向量h∈R1×M线性层的输出的结果是一个词向量v∈R1×V所以线性层的权重矩阵W∈RM×Vv1×V=h⋅Wv=[u1,u2,⋯ ,uV]第j个词的得分uj=WjThWj:线性层权重矩阵的第i行线性层输出U=[u1,u2,⋯ ,um] \begin{align} 前面的embeddings层输入进来的向量h \in R^{1\times M} \\ 线性层的输出的结果是一个词向量v \in R^{1\times V} \\ 所以线性层的权重矩阵W \in R^{M\times V} \\ v_{1\times V} =h\cdot W\\ v = [u_1,u_2,\cdots,u_V] \\ 第j个词的得分u_j = W_j^Th\\ W_j:线性层权重矩阵的第i行\\ 线性层输出U = [u_1,u_2,\cdots,u_m] \end{align} 前面的embeddings层输入进来的向量h∈R1×M线性层的输出的结果是一个词向量v∈R1×V所以线性层的权重矩阵W∈RM×Vv1×V=h⋅Wv=[u1,u2,⋯,uV]第j个词的得分uj=WjThWj:线性层权重矩阵的第i行线性层输出U=[u1,u2,⋯,um]
为什么CBOW
中不设置激活函数Tanh
或者ReLU
呢(?)
LLNM
中设置激活函数(第一层感知机)是为了让预测下一个词变得更准确- 但是
CBOW
中,我们只是想得到嵌入矩阵Q,然后进行正常的反向传播更新Q和W就可以- 并且embeddings层输出时,是进行了求平均向量这步线性操作,如果使用了非线性的激活函数,会破坏词向量之间的语义关系
Softmax层
将线性层的输出向量转换为概率分布
P(yj∣context)=exp(uj)∑i=1Vexp(ui) P(y_j | context) = \frac{exp(u_j)}{\sum_{i=1}^{V}exp(u_i)} P(yj∣context)=∑i=1Vexp(ui)exp(uj)
损失函数
L=−logP(wt∣wc)=−logPtruewt:中心词wc:所有上下文词Ptrue:Softmax输出的对应目标词位置的概率 \begin{align} L = -logP(w_t | w_c) = -logP_{true} \\ w_t:中心词\\ w_c:所有上下文词\\ P_{true}:Softmax输出的对应目标词位置的概率 \end{align} L=−logP(wt∣wc)=−logPtruewt:中心词wc:所有上下文词Ptrue:Softmax输出的对应目标词位置的概率
使用mini-batch进行训练:
L=−1B∑i=1BlogP(wt(i)∣wc(i))B:Batch_size;一个Batch的样本数(i):第i个样本 \begin{align} L = -\frac{1}{B}\sum_{i=1}^{B}logP(w_t^{(i)}|w_c^{(i)}) \\ B:Batch\_size;一个Batch的样本数\\ ^{(i)}:第i个样本 \end{align} L=−B1i=1∑BlogP(wt(i)∣wc(i))B:Batch_size;一个Batch的样本数(i):第i个样本
反向传播
先回顾一下前向传播;embeddings层输出:h=1V∑i=1Vwi⋅Q线性层:uj=WjThU=[u1,u2,⋯ ,um]Softmax层:pj=P(wj∣context)=exp(uj)∑i=1Vexp(ui) \begin{align} 先回顾一下前向传播;\\ embeddings层输出:h = \frac{1}{V}\sum_{i=1}^{V}w_i\cdot Q \\ 线性层:u_j = W_j^Th\\ U = [u_1,u_2,\cdots,u_m]\\ Softmax层:p_j = P(w_j|context) = \frac{exp(u_j)}{\sum_{i=1}^{V}exp(u_i)}\\ \end{align} 先回顾一下前向传播;embeddings层输出:h=V1i=1∑Vwi⋅Q线性层:uj=WjThU=[u1,u2,⋯,um]Softmax层:pj=P(wj∣context)=∑i=1Vexp(ui)exp(uj)
对于损失函数:
E=−logP(wt∣wc)=−log(exp(ut)∑i=1Vexp(ui))=−log(exp(WtT⋅h)∑i=1Vexp(WiT⋅h))=−(WtT⋅h−log∑i=1Vexp(WiT⋅h))=−WtT⋅h+log∑i=1Vexp(WiT⋅h)=−WtT⋅∑i=1Vwi⋅QV+log∑i=1Vexp(WiT⋅∑i=1Vwi⋅Q)V \begin{align} E = -logP(w_t | w_c)\\ =-log(\frac{exp(u_{t})}{\sum_{i=1}^{V}exp(u_i)})\\ = -log(\frac{exp(W^T_{t}\cdot h)}{\sum_{i=1}^{V}exp(W_i^T\cdot h)})\\ =-(W_t^T\cdot h - log\sum_{i=1}^{V}exp(W_i^T\cdot h))\\ =-W_t^T\cdot h + log\sum_{i=1}^{V}exp(W_i^T\cdot h)\\ =-\frac{W_t^T\cdot \sum_{i=1}^{V}w_i\cdot Q}{V}+log\sum_{i=1}^{V}\frac{exp(W_i^T\cdot \sum_{i=1}^{V}w_i\cdot Q)}{V} \end{align} E=−logP(wt∣wc)=−log(∑i=1Vexp(ui)exp(ut))=−log(∑i=1Vexp(WiT⋅h)exp(WtT⋅h))=−(WtT⋅h−logi=1∑Vexp(WiT⋅h))=−WtT⋅h+logi=1∑Vexp(WiT⋅h)=−VWtT⋅∑i=1Vwi⋅Q+logi=1∑VVexp(WiT⋅∑i=1Vwi⋅Q)
计算梯度
首先是对线性层的权重矩阵W求梯度:对当前的中心词wt∇Wt=∂E∂Wt=∂∂Wt(−WtT⋅h+log∑i=1Vexp(WiT⋅h))=−h+∂∂Wtexp(WtT⋅h)∑i=1Vexp(WiT⋅h)=−h+exp(WtT⋅h)⋅h∑i=1Vexp(WiT⋅h)=−h+pt⋅h=(pt−1)⋅h对其他上下文词wc∇Wc=∂E∂Wc=∂∂Wc(−WtT⋅h+log∑i=1Vexp(WiT⋅h))=pc⋅h可以得到一个统一形式:∇W=(pi−yi)⋅h (yi=1,当i=t) \begin{align} 首先是对线性层的权重矩阵W求梯度:\\ 对当前的中心词w_t\\ \nabla_{W_t} = \frac{\partial E}{\partial W_t}=\frac{\partial}{\partial W_t}(-W_t^T\cdot h + log\sum_{i=1}^{V}exp(W_i^T\cdot h))\\ = -h +\frac{\frac{\partial}{\partial W_t}exp(W_t^T\cdot h)}{\sum_{i=1}^{V}exp(W_i^T\cdot h)} = -h+\frac{exp(W_t^T\cdot h)\cdot h}{\sum_{i=1}^{V}exp(W_i^T\cdot h)} = -h+p_t\cdot h = (p_t-1)\cdot h\\ 对其他上下文词w_c\\ \nabla_{W_c} = \frac{\partial E}{\partial W_c}=\frac{\partial}{\partial W_c}(-W_t^T\cdot h + log\sum_{i=1}^{V}exp(W_i^T\cdot h))\\ =p_c\cdot h\\ 可以得到一个统一形式:\\ \nabla_{W} = (p_i-y_i)\cdot h\ \ (y_i=1,当i=t) \end{align} 首先是对线性层的权重矩阵W求梯度:对当前的中心词wt∇Wt=∂Wt∂E=∂Wt∂(−WtT⋅h+logi=1∑Vexp(WiT⋅h))=−h+∑i=1Vexp(WiT⋅h)∂Wt∂exp(WtT⋅h)=−h+∑i=1Vexp(WiT⋅h)exp(WtT⋅h)⋅h=−h+pt⋅h=(pt−1)⋅h对其他上下文词wc∇Wc=∂Wc∂E=∂Wc∂(−WtT⋅h+logi=1∑Vexp(WiT⋅h))=pc⋅h可以得到一个统一形式:∇W=(pi−yi)⋅h (yi=1,当i=t)
对embeddings层的嵌入矩阵Q求梯度:∇Q=∂E∂U∂U∂h∂h∂Q=(pi−yi)⋅Wi⋅∂∂Q(1V∑i=1Vwi⋅Q)=1V((pi−yi)⋅Wi⋅∑i=1Vwi)因为wi是one−hot编码得到的二进制向量∑i=1Vwi相当于是一个长度为V的全1向量所以得到最终的梯度:∇Q=1V(pi−yi)⋅Wi \begin{align} 对embeddings层的嵌入矩阵Q求梯度:\\ \nabla_Q = \frac{\partial E}{\partial U}\frac{\partial U}{\partial h}\frac{\partial h}{\partial Q}\\ =(p_i-y_i)\cdot W_i\cdot \frac{\partial}{\partial Q}(\frac{1}{V}\sum_{i=1}^{V}w_i\cdot Q )\\ =\frac{1}{V}((p_i-y_i)\cdot W_i\cdot \sum_{i=1}^{V}w_i) \\ 因为w_i是one-hot编码得到的二进制向量 \\ \sum_{i=1}^{V}w_i相当于是一个长度为V的全1向量\\ 所以得到最终的梯度:\\ \nabla_Q = \frac{1}{V}(p_i-y_i)\cdot W_i \end{align} 对embeddings层的嵌入矩阵Q求梯度:∇Q=∂U∂E∂h∂U∂Q∂h=(pi−yi)⋅Wi⋅∂Q∂(V1i=1∑Vwi⋅Q)=V1((pi−yi)⋅Wi⋅i=1∑Vwi)因为wi是one−hot编码得到的二进制向量i=1∑Vwi相当于是一个长度为V的全1向量所以得到最终的梯度:∇Q=V1(pi−yi)⋅Wi
层次Softmax(Hierarchical Softmax)
前置知识
哈夫曼树
哈夫曼树是保证所有叶子节点的带权路径长度最小的一种树
比如对于这样一颗二叉树

首先找出两个最小的叶子节点,组成一颗新的树
找出下一个最小的叶子节点,与新树组成树

以此类推,最终组成哈夫曼树

优化点
-
类似于
CBOW
的embeddings层,从输入层到隐藏层的映射,不是像神经网络那样线性变换+激活函数的操作,而是对词向量求平均 -
原本我们计算Softmax:
P(wc∣wt)=exp(ucT⋅vt)∑i=1Vexp(uiT⋅vt)vt:当前中心词词向量uc:当前中心词的上下文词的词向量 \begin{align} P(w_c |w_t) = \frac{exp(u_c^T\cdot v_t)}{\sum_{i=1}^{V}exp(u_i^T\cdot v_t)} \\ v_t:当前中心词词向量\\ u_c:当前中心词的上下文词的词向量 \end{align} P(wc∣wt)=∑i=1Vexp(uiT⋅vt)exp(ucT⋅vt)vt:当前中心词词向量uc:当前中心词的上下文词的词向量
需要遍历整个词汇表,对于大词汇表来说,这个做法的效率太灾难了
因此,层次Softmax
通过将词汇表构造成树形结构,从而减少了计算量,特别是在计算时只需要经过树的部分路径,而不是整个词汇表
工作原理

将词汇表V中的所有词汇根据词频f(wi))f(w_i))f(wi))表示成一颗哈夫曼树
,词频越大,路径越短,编码信息更少;其中叶子节点表示对应的词wiw_iwi,非叶子节点表示决策路径(二分类决策),每个叶子节点都有唯一的 从根节点到该节点的路径path
比如对于叶子节点w2w_2w2,从根节点出发到w2w_2w2的路径为:
n(w2,1),n(w2,2),n(w2,3),w2n(wi,j):词wi的path的第j个节点 \begin{align} n(w_2,1),n(w_2,2),n(w_2,3),w_2\\ n(w_i,j):词w_i的path的第j个节点 \end{align} n(w2,1),n(w2,2),n(w2,3),w2n(wi,j):词wi的path的第j个节点
叶子节点词的概率表示
在Hierarchical Softmax
中,我们的目标是计算根节点到叶子节点的路径的概率,并最大化路径上每个节点的概率;根节点到叶子节点有很多中间节点nkn_knk,每个中间节点的决策都是二分类问题
那么就有正类(+)与负类(-)的区分:在这里规定向左为负(-)
使用sigmoid
判断正负类:
P(+)=σ(vnT′⋅h)vnT′:中间节点词向量h:隐藏层输出的中心词的上下文向量 \begin{align} P(+) = \sigma(v_n^T{'} \cdot h) \\ v_n^T{'}:中间节点词向量\\ h:隐藏层输出的中心词的上下文向量 \end{align} P(+)=σ(vnT′⋅h)vnT′:中间节点词向量h:隐藏层输出的中心词的上下文向量
还是计算上面根节点到w2w_2w2路径的概率,我们应最大化下式:
P(w2)=∏i=13P(n(w2,i))=P(n(w2,1),−)⋅P(n(w2,2),−)⋅P(n(w2,3),+)=σ(−vn(w2,1)T′⋅h)⋅σ(−vn(w2,2)T′⋅h)⋅σ(vn(w2,3)T′⋅h) \begin{align} P(w_2) = \prod_{i=1}^{3}P(n(w_2,i)) = P(n(w_2,1),-)\cdot P(n(w_2,2),-)\cdot P(n(w_2,3),+) \\ =\sigma(-v_{n(w_2,1)}^T{'} \cdot h)\cdot\sigma(-v_{n(w_2,2)}^T{'} \cdot h)\cdot\sigma(v_{n(w_2,3)}^T{'} \cdot h) \end{align} P(w2)=i=1∏3P(n(w2,i))=P(n(w2,1),−)⋅P(n(w2,2),−)⋅P(n(w2,3),+)=σ(−vn(w2,1)T′⋅h)⋅σ(−vn(w2,2)T′⋅h)⋅σ(vn(w2,3)T′⋅h)
损失函数:
E=−logP(w2)=−∑i=13logP(n(w2,i))=−∑i=13logσ(signi⋅vn(w2,i)T′⋅h)signi:表示向左(−1)向右(+1) \begin{align} E = -logP(w_2) = -\sum_{i=1}^{3}logP(n(w_2,i)) = -\sum_{i=1}^{3}log\sigma(sign_i \cdot v_{n(w_2,i)}^T{'} \cdot h)\\ sign_i:表示向左(-1)向右(+1) \end{align} E=−logP(w2)=−i=1∑3logP(n(w2,i))=−i=1∑3logσ(signi⋅vn(w2,i)T′⋅h)signi:表示向左(−1)向右(+1)
通过梯度下降更新vnT′和hv_n^T{'} 和 hvnT′和h
案例
设备
python
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"current device:{device}")
数据
python
# 句子数据
sentences = [
# 动物行为
"dogs chase cats", "cats drink milk", "dogs love bones",
"cats hate water", "dogs bark loudly", "cats climb trees",
"dogs play fetch", "cats purr softly", "dogs guard houses",
"cats hunt mice", "dogs wag tails", "cats nap all day",
"birds fly high", "fish swim fast", "birds sing in morning",
"tigers roar at night", "elephants walk slowly", "monkeys swing on trees",
# 食物特征
"apples are sweet", "bananas taste creamy", "apples grow on trees",
"bananas have peels", "apples make juice", "bananas are yellow",
"apples can be red", "bananas rich in potassium",
"oranges are juicy", "grapes are sour", "strawberries are red",
"bread is soft", "cheese tastes salty", "eggs are nutritious",
"rice is sticky", "noodles are long", "chocolate is sweet",
# 日常生活
"kids eat apples", "monkeys love bananas", "dogs enjoy meat",
"cats prefer fish", "people drink milk", "children like fruit",
"breakfast has milk", "dogs drink water", "cats avoid dogs",
"people walk to work", "buses run on time", "cars drive fast",
"students study hard", "teachers explain clearly", "doctors help patients",
"cooks prepare meals", "babies cry at night", "seniors rest peacefully",
# 情感与动作
"he feels happy", "she looks tired", "they laugh together",
"he cries softly", "she dances gracefully", "they argue loudly",
"he hugs his dog", "she kisses her baby", "they shake hands",
"he smiles kindly", "she yells in anger", "they cheer with joy",
# 自然与天气
"sun rises early", "moon shines at night", "stars twinkle above",
"clouds float in sky", "rain falls gently", "wind blows strongly",
"snow covers mountains", "ice melts in sun", "lightning flashes bright",
"thunder rumbles loudly", "fog covers the fields", "dew drops sparkle",
# 人物与职业
"doctors heal patients", "teachers teach students", "engineers build bridges",
"writers write stories", "artists draw pictures", "singers sing songs",
"actors perform plays", "pilots fly planes", "farmers grow crops",
"chefs cook meals", "drivers drive cars", "scientists make discoveries",
# 更多组合
"apple pie is delicious", "banana smoothie is healthy",
"dog food contains nutrients", "cat food has tuna flavor",
"apple and banana salad", "dog and cat live together",
"milk with banana shake", "fish for cat dinner",
"bread with cheese", "rice with chicken", "noodles with beef",
"coffee with milk", "tea with sugar", "juice with ice"
]
构造词汇表V以及索引与词之间映射关系的字典
python
# 分词
word_list = "".join(sentences).split()
# 转换成set去重
vocab = set(list(word_list))
# print(vocal)
# 构建索引
v_to_idx = {c: i for i, c in enumerate(vocab)}
idx_to_v = {i: c for i, c in enumerate(vocab)}
# 构建词频
f_word = Counter(vocab)
# print(f"f(w)={f_word}")
# 所有词的频度和其实就是对原始句子数据分词后得到的长度
f_total = len(word_list)
# print(f"\sumf(w)={f_total}")
构造负采样的概率
python
# 计算负采样概率
prob_neg_samples = {w: (cnt / f_total) ** 0.75 for w, cnt in f_word.items()}
# print(f"prob_neg_samples = {prob_neg_samples}")
# 转换成np.array的形式,其中放的是负采样概率
prob_neg_samples = np.array([prob_neg_samples[w] for w in vocab])
# print(f"prob_neg_samples = {prob_neg_samples}")
存在问题?
当前负采样概率和不为1,后续选择负样本时会出错
解决:进行归一化
Python
# 归一化
prob_neg_samples /= prob_neg_samples.sum()
修正后的构造负采样的代码为:
Python
# 计算负采样概率
prob_neg_samples = {w: (cnt / f_total) ** 0.75 for w, cnt in f_word.items()}
# print(f"prob_neg_samples = {prob_neg_samples}")
# 转换成np.array的形式,其中放的是负采样概率
prob_neg_samples = np.array([prob_neg_samples[w] for w in vocab])
# print(f"prob_neg_samples = {prob_neg_samples}")
# 归一化
prob_neg_samples /= prob_neg_samples.sum()
生成带负采样的训练数据
python
# 窗口大小
window_size = 2
# 1个正例对4个反例
neg_count = 4
# 生成负采样训练样本
def generate_neg_sample():
skip_gram = [] # 存放((正/反例),flag) flag=True为正例
for idx in range(window_size, len(word_list) - window_size):
centre = v_to_idx[word_list[idx]] # 中心词
# 上下文词
contexts = [v_to_idx[word_list[i]] for i in range(idx - window_size, idx + window_size + 1) if i != idx]
for c in contexts:
# 对中心词-上下文词,作为正样本加入
skip_gram.append((centre, c, True))
# 负样本对应词索引集合
negs = []
# 随机选择负样本
while len(negs) < neg_count:
neg_idx = np.random.choice(len(vocab), p=prob_neg_samples)
if neg_idx != c:
negs.append(neg_idx)
# 负样本加入
for ni in negs:
skip_gram.append((centre, ni, False))
return skip_gram
转换为tensor
python
# 将负采样训练样本转换为torch.tensor
data = generate_neg_sample()
centres = torch.tensor([d[0] for d in data], dtype=torch.long).to(device)
# print(f"centres={centres}")
contexts = torch.tensor([d[1] for d in data], dtype=torch.long).to(device)
# print(f"contexts ={contexts}")
labels = torch.tensor([d[2] for d in data], dtype=torch.float32).to(device)
# print(f"labels={labels}")
构建数据集以及分批加载
python
# 使用TensorDataset将三个张量合成一个数据集对象,每个样本是一个三元组(centres, contexts, labels)
dataset = Data.TensorDataset(centres, contexts, labels)
# 按批次加载对象
dataloader = Data.DataLoader(dataset, batch_size, shuffle=True)
模型
python
class NegSkipGram(nn.Module):
def __init__(self, vocab_size, embedding_dim):
"""
:param vocab_size: 词汇表长度
:param embedding_dim: 嵌入矩阵维度
"""
super().__init__()
# 得到中心词词嵌入向量
self.centre_embedding = nn.Embedding(vocab_size, embedding_dim)
# 得到上下文词词嵌入向量
self.context_embedding = nn.Embedding(vocab_size, embedding_dim)
# 初始化两个embedding层中的权重矩阵,使用正态分布进行原地填充(直接修改原tensor的值)
nn.init.normal_(
self.centre_embedding.weight, # 输出中心词word embedding的权重矩阵
mean=0, # 均值为0
std=0.01 # 标准差
)
nn.init.normal_(
self.context_embedding.weight, # 输出上下文词word embedding的权重矩阵
mean=0,
std=0.01
)
def forward(self, centres, contexts):
"""
:param centres: 中心词词向量 shape=[batch_size]
:param contexts: 上下文词词向量 shape=[batch_size]
:return: 余弦相似度
"""
# 中心词词向量shape=[batch_size,embedding_dim]
vec_centre = self.centre_embedding(centres)
# print(f"vec_centre.shape={vec_centre.shape}")
# 上下文词词向量shape=[batch_size,embedding_dim]
vec_context = self.context_embedding(contexts)
# print(f"vec_context.shape={vec_context.shape}")
# print(f"vec_context={vec_context}")
# 余弦相似度
return torch.cosine_similarity(vec_centre, vec_context, dim=1)
模型训练
python
# 一批训练多少个样本
batch_size = 128
# word-embedding维度
embedding_dim = 20
# 模型
model = NegSkipGram(vocab_size, embedding_size).to(device)
# 二分类交叉熵损失
criterion = nn.BCEWithLogitsLoss()
# adam优化
optimiser = torch.optim.Adam(model.parameters(), lr=0.005)
epochs = 100
for i in range(epochs):
# 在dataloader中取出每一批的中心词向量,上下文词向量,标签
for batch_centre, batch_context, batch_label in dataloader:
# 获得前向传播分数
cos_score = model(batch_centre, batch_context)
# 获取损失函数值
loss = criterion(cos_score, batch_label)
# 清空优化器中所有参数的梯度缓存
optimiser.zero_grad()
loss.backward()
# 更新参数
optimiser.step()
可视化:使用sklearn.manifold.TSNE
进行降维,以及plt
进行绘图
python
# 中心词嵌入层参数矩阵的numpy形式 shape=(vocab_size,embedding_dim)
word_vectors = (
# 中心词嵌入层的权重W shape=Tensor[vocab_size,embedding_dim]
model.centre_embedding.weight
.cpu() # 获取的权重矩阵转存到cpu中
.detach() # 切断历史梯度
.numpy()
)
# TSNE降维
tsne = TSNE(
n_components=2, # 词向量维度降到2D
random_state=42,
perplexity=min(5, len(vocab) - 1) # perplexity可以理解为每个点邻居数量的估计值
)
# 降维
word_vectors_2d = tsne.fit_transform(word_vectors)
plt.figure(figsize=(12, 10))
for i in range(vocab_size):
plt.scatter(word_vectors_2d[i, 0], word_vectors_2d[i, 1])
plt.annotate(
idx_to_v[i],
xy=(word_vectors_2d[i, 0], word_vectors_2d[i, 1]),
xytext=(5, 2), # 标签文字对点的偏移量
textcoords='offset points', # 指定xytext单位是点
ha='right',
va='bottom'
)
plt.show()
问题
为什么嵌入层返回的形状跟之前公式里学习的都不太一样(?)
因为在torch
中,我们使用了批量处理的方法,一次处理一个batch而不是一个词;
在嵌入矩阵中,每输入一个词,就会输出一个形状为[,embedding_dim]的向量,那么对于batch_size
个词,就会输出形状为[batch_size,embedding_dim]的矩阵
为什么计算余弦相似度时要指定维度(?)
因为torch
返回的词向量的shape是[batch_size,embedding_dim],对embedding_dim
这一维度求余弦相似度,才能正确计算这一个batch中每个词的余弦相似度评分