文章目录
转载来源:https://zhuanlan.zhihu.com/p/108703757

这篇文章记录词向量的发展历程, 包括tf-idf、word2vec、GloVe、ELMo、OpenAI GPT以及Bert,只记录个人认为比较核心的内容,以及一些值得思考的边角细节。
1、tf-idf
tf-idf是一种比较传统的文本表示方法,它首先为每个词计算出一个值,再组成向量来表示当前文档。它的大小等于词表数。首先tf是词频,也就是当前词在文档中出现的次数,通常会除以文档总词数来做归一化。idf的计算方法是log(语料库中总文档数 / 包含当前词的文档数),可见分子是固定值,idf将随着包含当前词的文档数的增加而减小,也就是说常见词的idf值会相对较小,而当前文档比较有代表性的词发挥更大的作用。tf-idf的缺点是它是词袋模型,无法考虑词的位置信息,上下文信息以及一些分布特征。
2、word2vec
实际上tf-idf就是one-hot的一种优化,还是存在维度灾难以及语义鸿沟的问题。因此后来的工作着重于构建分布式低维稠密词向量。word2vec就是它们的开山之作。我们知道NNLM(语言模型)是一种自监督训练的模型,用上文来预测下一个词的概率,那么词向量就可以作为它的副产物学习到这种基于序列共现的语境信息。word2vec基于这种思想提出了更专注于词向量学习的模型(比如舍弃隐藏层),用滑动窗口来指定固定大小的上下文,试图用当前词来预测上下文(skip-gram)或用上下文来预测当前词(CBOW)。具体细节可以参考这篇论文。
word2vec的两种加速训练策略
- 分层softmax
用哈夫曼树来计算词的概率,每个词对应一个叶节点。非叶节点也各自对应一个向量,词的概率可由它到根节点的唯一路径来计算。
哈夫曼树的构造方法:将词表中的每个词看作只有一个结点的树,用词频来表示它们的权重。选择根节点权重最小的两棵树合并,合并后的父节点权重等于两个子结点之和。下面是一个例子:
为了保证概率相加等于1,在路径上采用sigmoid来计算向左或向右(n表示结点,v表示结点向量):
p
(
n
,
left
)
=
σ
(
v
T
h
)
p
(
n
,
right
)
=
1
−
σ
(
v
T
h
)
=
σ
(
−
v
T
h
)
\begin{array}{c} p(n, \text {left})=\sigma\left(v^{T} \mathbf{h}\right) \\ p(n, \text {right})=1-\sigma\left(v^{T} \mathbf{h}\right)=\sigma\left(-v^{T} \mathbf{h}\right) \end{array}
p(n,left)=σ(vTh)p(n,right)=1−σ(vTh)=σ(−vTh)
那么某个词
w
o
w_{o}
wo 出现的概率就是:
p
(
w
=
w
O
)
=
∏
j
=
1
L
(
w
)
−
1
σ
(
[
[
n
(
w
,
j
+
1
)
=
ch
(
n
(
w
,
j
)
)
]
]
⋅
v
n
(
w
,
j
)
′
T
h
)
p\left(w=w_{O}\right)=\prod_{j=1}^{L(w)-1} \sigma\left([[n(w, j+1)=\operatorname{ch}(n(w, j))]] \cdot \mathbf{v}_{n(w, j)}^{\prime} T_{\mathbf{h}}\right)
p(w=wO)=j=1∏L(w)−1σ([[n(w,j+1)=ch(n(w,j))]]⋅vn(w,j)′Th)
[[·]]是个1/-1函数,左子结点取1,右子结点取-1。再算cross entropy即可:
E
=
−
log
p
(
w
=
w
O
∣
w
I
)
=
−
∑
j
=
1
L
(
w
)
−
1
log
σ
(
[
[
⋅
]
]
v
j
′
T
h
)
E=-\log p\left(w=w_{O} | w_{I}\right)=-\sum_{j=1}^{L(w)-1} \log \sigma\left(\left[\left[\cdot\left]]\mathbf{v}_{j}^{\prime T} \mathbf{h}\right)\right.\right.\right.
E=−logp(w=wO∣wI)=−j=1∑L(w)−1logσ([[⋅]]vj′Th)
这样就代替了softmax,复杂度从O(N)变成O(log N)。
- 负采样
采样概率:在词频上取0.75次幂,减小词频差异带来的采样影响,即
weight ( w ) = count ( w ) 0.75 ∑ u count ( u ) 0.75 \operatorname{weight}(w)=\frac{\operatorname{count}(w)^{0.75}}{\sum_{u} \operatorname{count}(u)^{0.75}} weight(w)=∑ucount(u)0.75count(w)0.75
那么损失函数为:
E = − log σ ( v w o ′ T h ) − ∑ w N ∈ N E G log σ ( − v w N ′ T h ) E=-\log \sigma\left(v_{w_{o}}^{\prime T} \mathbf{h}\right)-\sum_{w_{N} \in N E G} \log \sigma\left(-v_{w_{N}}^{\prime T} \mathbf{h}\right) E=−logσ(vwo′Th)−wN∈NEG∑logσ(−vwN′Th)
w o w_{o} wo是目标词, w I w_{I} wI 是输入词。对于skip-gram, h = v w I \mathbf{h}=v_{w_{I}} h=vwI,对于CBOW, h = 1 C ∑ c = 1 C v w c \mathbf{h}=\frac{1}{C} \sum_{c=1}^{C} v_{w_{c}} h=C1∑c=1Cvwc
word2vec只能抽取局部特征,词的上下文信息局限于滑动窗口大小。
3、GloVe
细节推荐这篇博客。主要的几个步骤包括:
- 构建共现矩阵
GloVe 指定特定大小的上下文窗口,通过滑动该窗口统计共现矩阵 X(|V|*|V|), X i , j X_{i,j} Xi,j 表示中心词 i 与上下文词 j 的共现次数。同时还定义了衰减函数,令距离为 d 的两个词在计数时乘以 1/d。 - 确定近似目标
作者发现可以用概率之比来建模共现关系。定义条件概率 P:
P i j = P ( j ∣ i ) = X i j X i P_{i j}=P(j | i)=\frac{X_{i j}}{X_{i}} Pij=P(j∣i)=XiXij
表示词 j 出现在 i 上下文的概率。而用词 k 出现在 i 的上下文与它出现在 j 上下文的概率之比:
r a t i o i , j , k = P i k P j k r a t i o_{i, j, k}=\frac{P_{i k}}{P_{j k}} ratioi,j,k=PjkPik
来表示 i,j,k 三个词之间的共现关系。当 i,k 和 j,k 相关程度近似时,该比率趋近于 1;i,k 相关度大于 j,k 相关度时该比率值较大,反之则较小。GloVe 的目标就是使学习到的词向量满足这样的规律,既有自身上下文信息,又能和其它词联系起来。目标函数:
F ( w i , w j , w k ) = P i k P j k F\left(w_{i}, w_{j}, w_{k}\right)=\frac{P_{i k}}{P_{j k}} F(wi,wj,wk)=PjkPik
要同时满足三个词的约束关系,训练复杂度会变得很高。作者通过一系列变(硬)换(凑),把上式转换成了两个词的约束目标:
F ( w i , w j , w k ) = exp ( ( w i − w j ) T w k ) = exp ( w i T w k − w j T w k ) = exp ( w i T w k ) exp ( w j T w k ) = P i k P j k \begin{aligned} F\left(w_{i}, w_{j}, w_{k}\right) &=\exp \left(\left(w_{i}-w_{j}\right)^{T} w_{k}\right) \\ &=\exp \left(w_{i}^{T} w_{k}-w_{j}^{T} w_{k}\right) \\ &=\frac{\exp \left(w_{i}^{T} w_{k}\right)}{\exp \left(w_{j}^{T} w_{k}\right)}=\frac{P_{i k}}{P_{j k}} \end{aligned} F(wi,wj,wk)=exp((wi−wj)Twk)=exp(wiTwk−wjTwk)=exp(wjTwk)exp(wiTwk)=PjkPik
由此
P i k = exp ( w i T w k ) P_{i k}=\exp \left(w_{i}^{T} w_{k}\right) Pik=exp(wiTwk)
成为新的目标函数。然而这种内积计算方式具有对称性,为了避免这种错误的性质,作者继续变(硬)换(凑):
P i k = X i k X i = exp ( w i T w k ) log P i k = log ( X i k ) − log X i = w i T w k w i T w k + log X i = log ( X i k ) \begin{array}{c} P_{i k}=\frac{X_{i k}}{X_{i}}=\exp \left(w_{i}^{T} w_{k}\right) \\ \log P_{i k}=\log \left(X_{i k}\right)-\log X_{i}=w_{i}^{T} w_{k} \\ w_{i}^{T} w_{k}+\log X_{i}=\log \left(X_{i k}\right) \end{array} Pik=XiXik=exp(wiTwk)logPik=log(Xik)−logXi=wiTwkwiTwk+logXi=log(Xik)
将 l o g X i logX_{i} logXi 看作常数项,再添加一个偏置,目标函数最终形式为:
w i T w k + b i + b k = log ( X i k ) w_{i}^{T} w_{k}+b_{i}+b_{k}=\log \left(X_{i k}\right) wiTwk+bi+bk=log(Xik)
其中 X i k X_{ik} Xik是共现矩阵中的值。 - 构造损失函数(平方损失)
J = ∑ i , k = 1 V f ( X i k ) ( w i T w k + b i + b k − log ( X i k ) ) 2 J=\sum_{i, k=1}^{V} f\left(X_{i k}\right)\left(w_{i}^{T} w_{k}+b_{i}+b_{k}-\log \left(X_{i k}\right)\right)^{2} J=i,k=1∑Vf(Xik)(wiTwk+bi+bk−log(Xik))2
其中 f ( X i j ) f(X_{ij}) f(Xij)是关于共现矩阵的权重函数,
f ( x ) = { ( x / x max ) α if x < x max 1 otherwise f(x)=\left\{\begin{array}{ll} \left(x / x_{\max }\right)^{\alpha} & \text { if } x<x_{\max } \\ 1 & \text { otherwise } \end{array}\right. f(x)={(x/xmax)α1 if x<xmax otherwise
也就是说共现次数越少,对它们的相关性约束越小。
Glove 和 Word2vec 比较
- word2vec 面向局部特征,基于滑动窗口,而 GloVe 综合了全局语料。
- word2vec 可以增量学习,而 Glove 是由固定语料计算的共现矩阵。
4、Fasttext
Fasttext 最早其实是一个文本分类算法,后续加了一些改进来训练词向量。概括了几点:
- fasttext 在输入时对每个词加入了 n-gram 特征,在输出时使用分层 softmax 加速训练。
- fasttext 将整篇文章的词向量求平均作为输入得到文档向量,用文本分类做有监督训练,对输出进行 softmax 回归,词向量为副产品。
- fasttext 也可以无监督训练词向量,与 CBOW 非常相似。
5、ELMo
之前那些方法构造的都是独立于上下文的 word embedding,也就是无论下游任务是什么,输入的 embedding 始终是固定的,这就无法解决一词多义,以及在不同语境下有不同表现的需求。所以后续的 ELMo,GPT 以及 BERT 都是针对于这类问题提出的,通过预训练和 fine-tune 两个阶段来构造 context-dependent 的词表示。
ELMo 使用双向语言模型来进行预训练,用两个分开的双层 LSTM 作为 encoder。biLM 的 loss 是:
L = ∑ k = 1 N ( log p ( t k ∣ t 1 , … , t k − 1 ; Θ ⃗ L S T M , Θ s ) + log p ( t k ∣ t k + 1 , … , t N ; Θ ← L S T M , Θ s ) ) L=\sum_{k=1}^{N}\left(\log p\left(t_{k} | t_{1}, \ldots, t_{k-1} ; \vec{\Theta}_{L S T M}, \Theta_{s}\right)+\log p\left(t_{k} | t_{k+1}, \ldots, t_{N} ; \overleftarrow{\Theta}_{L S T M}, \Theta_{s}\right)\right) L=k=1∑N(logp(tk∣t1,…,tk−1;ΘLSTM,Θs)+logp(tk∣tk+1,…,tN;ΘLSTM,Θs))
其中 Θ s \Theta_{s} Θs 是 softmax 层参数。作者认为第一层学到的是句法信息,第二层学到的是语义信息。这两层 LSTM 的隐状态以及初始的输入加权求和就得到当前词的 embedding。ELMo 还设置了一个参数,不同的下游任务可以取特定的值,来控制 ELMo 词向量起到的作用。总体来说第 k 个 token 得到的预训练 embedding 就是:
E
L
M
o
k
t
a
s
k
=
γ
t
a
s
k
∑
j
=
0
L
s
j
t
a
s
k
h
k
j
L
M
\mathbf{E} \mathbf{L} \mathbf{M} \mathbf{o}_{k}^{t a s k}=\gamma^{t a s k} \sum_{j=0}^{L} s_{j}^{t a s k} \mathbf{h}_{k j}^{L M}
ELMoktask=γtaskj=0∑LsjtaskhkjLM
在面对具体下游任务时,首先固定 biLM 的参数得到一个词表示,再与上下文无关的词表示(word2vec,或者 charCNN 获得的表示)拼接作为模型输入,在反向传播时 fine-tune 所有参数。
原文中提到的一些细节:
- biLM 不同层的 activation 分布不同,在加权求和之前使用 layer normalization 有时会很有效。
- 增加 dropout 和 L2 正则化可能会有用,这对 ELMo 的权重提出了一个归纳偏差,使其接近所有 biLM 层的平均值。
- 在获得上下文无关词表示时,原文采用的方式是先用 2048 个 charCNN 卷积核做卷积,再过两层 highway networks,然后用一个线性层把输出降到 512 维。
6、OpenAI GPT
GPT 和 BERT 与 ELMo 不同,ELMo 使用 LSTM 作为编码器,而这两个用的是编码能力更强的 Transformer。
GPT 也是用语言模型进行大规模无监督预训练,但使用的是单向语言模型,也就是只根据上文来预测当前词。它实现的方式很直观,就是 Transformer 的 decoder 部分,只和前面的词计算 self-attention 来得到表示。在下游任务上,之前的 ELMo 相当于扩充了其它任务的 embedding 层,各个任务的上层结构各不相同,而 GPT 则不同,它要求所有下游任务都要完全与 GPT 的结构保持一致,只在输入输出形式上有所变化:
这是在 NLP 上第一次实现真正的端到端,不同的任务只需要定制不同的输入输出,无需构造内部结构。这样预训练学习到的语言学知识就能直接引入下游任务,相当于提供了先验知识。比如说人在做阅读理解时,先通读一遍全文再根据问题到文章中找回答,这些两阶段模型就类似这个过程。为了防止 fine-tune 时丢失预训练学到的语言知识,损失函数同时考虑下游任务 loss(L_2)和语言模型 loss(L_1):
L
3
(
C
)
=
L
2
(
C
)
+
λ
L
1
(
C
)
L_{3}(C)=L_{2}(C)+\lambda L_{1}(C)
L3(C)=L2(C)+λL1(C)
个人认为 GPT 的最大创新:
- 用足够复杂的模型结构担任不同 NLP 任务的中间框架,启发了统一的端到端实现策略。
- 第二阶段保留语言模型的 loss。
7、BERT
推荐这篇博客(这位大佬的其它文章质量也超高,尤其 Transformer 那篇估计是好多人的入门必看)
GPT 虽然效果很好,但它在预训练时使用的是 transformer 的 decoder 部分,也就是单向语言模型,在计算 attention 时只能看见前面的内容,这样 embedding 获得的上下文信息就不完整。ELMo 虽然是双向语言模型,但实际上是分开执行再组合 loss,这就会带来一定的损失。
7.1 Bert 预训练
与 GPT 不同的是,bert 在预训练时除了语言模型 loss 以外,还增加了一个 “next sentence prediction” 任务,即两个句子组成 sentence pair 同时输入,预测第二句是否是第一个句子的下文,是一个二分类任务。
7.1.1 输入
-
每个位置的输入:
wordpiece-token
词向量,这里的 wordpiece 是将 token 拆分成子词。position emb
位置向量
-
segment emb
句子标识,属于第一个句子则为 0,第二个句子则为 1 -
整体输入:
[CLS]
; sent1 ;[SEP]
; sent2 ;[SEP]
7.1.2 训练任务
1、Masked Language Model
-
所谓双向 LM,就是在预测当前词时同时考虑上文和下文,也就是
p ( t k ∣ t 1 , … , t k − 1 , t k + 1 , … , t N ) p\left(t_{k} | t_{1}, \ldots, t_{k-1}, t_{k+1}, \ldots, t_{N}\right) p(tk∣t1,…,tk−1,tk+1,…,tN)
但 LM 是要逐词预测的,用这种概率计算方法会导致信息泄露,也就是当前词已经在之前的预测中作为下文而暴露了。作者由此提出了 MASK 策略,只选取 15% 的词进行预测,在输入时用 [MASK] 标记替代,而仍然以原词作为训练 target。类似于阅读理解中的 Cloze 任务。当然,预测的词少了,模型收敛速度就会变慢,需要的训练 step 也要相应增加。 -
mask 解决了信息泄露问题,但实际输入(也就是 fine-tune 阶段)不会包含这种标记,导致两阶段不一致,对训练效果产生影响。作者的解决方案是在这随机选取的 15% 词当中,80% 的概率替换为 [MASK],10% 的概率替换成其它词(负采样) ,10% 的概率保留原词。这样有一个好处是,模型不知道当前要预测的词是否被篡改了,迫使其更关注上下文,学习到上下文相关的表示,这正是我们的目的。
作者还在附录里给出了一个扩展实验,对比不同的预训练 mask 策略对后续结果的影响:
可以看到至少在这两个任务中,结果对不同的 mask 法是鲁棒的,差别不大。但从最后两条可以看出,直接去掉 [MASK],80% 或 100% 取负样本的效果相比之下差了很多。按理说使用负样本相当于构建去噪自编码器,到底比 MASK 差在哪?思考了一下,原因很可能是负采样词作为其它词的上下文输入,使得这些词学到的 embedding 融合了错误的信息,对训练造成影响;而[MASK] 本身并没有任何含义,它从未作为 target 出现过,也就没有特定的出现语境,因此其 embedding 没有实际意义,对其它词的影响也就相对较小。
2、 Next Sentence Prediction
0/1 分类任务。从语料中选取两个片段 AB(注意这里是两个 “span”,而不是实际意义上的“句子”,因为希望输入尽可能长)作为一条输入,50% 的概率 AB 连续(1),50% 不连续(0)。输出在[CLS] 处取 FFNN+Softmax 做二分类预测。输入的最大长度是 512,超过则直接截断。
7.1.3 训练细节
-
预训练数据及规模
BooksCorpus (800M words) 加 Wikipedia (2,500M words) -
参数设置
- batch_size: 256 sequences(256*512 tokens) , step 1,000,000 (40 epochs on 3.3 billion word corpus)
- Adam 优化器。lr=1e-4, β 1 \beta_{1} β1 =0.9, β 2 \beta_{2} β2 =0.999,l2 weight decay=0.01
- learning rate warmup:10,000 steps,lr 线性缩减
- 所有层均设 dropout=0.1
- 激活函数:gelu
-
训练 loss
masked LM 与 NSP 的 log likelihood 之和
7.2 Bert Fine-tune
fine-tuning 的任务主要分成基于句子的和基于 token 的。基于句子的一般取 [CLS] 的 embedding 输出预测,基于 token 的则直接取对应位置的输出进行预测。
一般需根据特定的任务重新设置 batch_size, learning rate, epochs 超参数,其余与预训练保持一致即可。
预训练好的 Bert 除了用于 fine-tuning 以外,还可以像 ELMo 一样作为特征抽取器,也就是直接用学习到的 word embeddings 当做其它模型的输入。目前看来最好的选择是最后四层向量拼接。
Bert 与 GPT 的区别:
- GPT 与 Bert 训练数据不同,GPT 使用 BooksCorpus (800M words); BERT 是 BooksCorpus (800M words) 加 Wikipedia (2,500M words)。
- GPT 在预训练时没有 [CLS] 和[SEP],在下游任务时才有
- GPT 在 fine-tuning 时加入 LM 的 loss,而 Bert 是完全使用任务特定的目标函数。
- GPT 的 lr 在两阶段保持一致,Bert 认为任务特定的 lr 效果更好
Bert 最大的创新:
- 用 mask 策略实现了双向语言模型,非常巧妙。
- 预训练除了语言模型,还加入了 next sentence prediction,试图学习更高层面的语言关联性。提供了很好的扩展思路。