论文链接:https://www.cs.ubc.ca/~amuham01/LING530/papers/radford2018improving.pdf
代码链接:https://github.com/openai/finetune-transformer-lm
参考文档:https://mp.weixin.qq.com/s/VI5hvcZejJL9ftdDcgMZQA
今天阅读的是 OpenAI 2018 年的论文《Improving Language Understanding by Generative Pre-Training》,截止目前共有 600 多引用。
在这篇论文中,作者提出了一种半监督学习方法——Generative Pre-Training(以下简称 GPT),GPT 采用无监督学习的 Pre-training 充分利用大量未标注的文本数据,利用监督学习的 Fine-tuning 来适配具体的具体的 NLP 任务(如机器翻译),并在 12 个 NLP 任务中刷新了 9 个记录。
NLP 领域中只有小部分标注过的数据,而有大量的数据是未标注,如何只使用标注数据将会大大影响深度学习的性能,所以为了充分利用大量未标注的原始文本数据,需要利用无监督学习来从文本中提取特征,最经典的例子莫过于词嵌入技术。
但是词嵌入只能 word-level 级别的任务(同义词等),没法解决句子、句对级别的任务(翻译、推理等)。出现这种问题原因有两个:
为了解决以上问题,作者提出了 GPT 框架,用一种半监督学习的方法来完成语言理解任务,GPT 的训练过程分为两个阶段:Pre-training 和 Fine-tuning。目的是学习一种通用的 Representation 方法,针对不同种类的任务只需略作修改便能适应。
现实世界中,无标签的文本语料库非常巨大,而带有标签的数据则显得十分匮乏,如何有效利用无标签的原始文本,对缓解自然语言处理相关任务对有监督学习方式的依赖显得至关重要。
有效的从无标签文本中利用超单词级信息有两个主要的难点:
①无法确定什么样的优化目标能学到最有效的文本表征,使之更好的用于迁移目的。
②对于学习到的文本表征,采用何种方式将其迁移到目标任务上,目前尚无共识。
论文中提出了半监督的方式来做语言理解,也就是无监督的pre-train,和有监督的fine-tune。该方法首先利用无监督的pre−train模型,学习到更加普遍、更适用的表征,然后模型以很小的微调迁移到众多特定的有监督学习任务上。在实验效果上,大幅超过了众多任务的state-of-art。不同于word Embedding、ELMo 以无监督的方式学习到一些特征,然后利用这些特征喂给一些特定的有监督模型,这里是先无监督的pre−train模型,然后直接fine-tune预训练后的模型,迁移到一些特定的有监督任务上。
ELMo方法中,训练基于LSTM的双向语言模型能结合上下文内容学习到语义更丰富的词表征,而本论文中预训练的语言模型中使用了transformer(Masked Multi-Head Attention,单向)结构,相对而言,transformer更加鲁棒,在长距离依赖上的效果更好,迁移效果也更好。
适用场景:无标签样本量(只有X)远大于有标签样本量的数据集(同时有X,y),如果只用这少量的带标签样本训练出的模型泛化能力肯定比较弱,这个时候我们可以先用无标签样本(也就是只用X)预训练好一个语言模型,然后在该语言模型基础上用少量带标签的样本(同时有X,y)进行fine-tune,有监督的训练。
GPT 训练过程分为两个阶段:第一个阶段是 Pre-training 阶段,主要利用大型语料库完成非监督学习;第二阶段是 Fine-tuning,针对特定任务在相应数据集中进行监督学习,通过 Fine-tuning 技术来适配具体任务。下图为 GPT 的架构图:
上图中,每一层的所有Trm属于一个自左向右的单向transformer,故在embedding输入和上一层的输出到下一层的输入时,都会做self attention操作,而这个self attention操作相当于当前位置cell会结合上一层所有位置的状态信息,这样就相当于双向连接了,因此需要乘以一个mask矩阵,用来屏蔽当前位置后面的位置的隐藏层状态信息。这是transformer decoder的一个关键。可看代码:
def mask_attn_weights(w):
n = shape_list(w)[-1]
b = tf.matrix_band_part(tf.ones([n, n]), -1, 0)## 下三角
b = tf.reshape(b, [1, 1, n, n])
w = w*b + -1e9*(1-b)
return w
如果不做这样的一个屏蔽操作,那么就变成双向的了。
分两步走,第一步:利用海量无标签的样本集预训练一个语言模型;第二步:利用预训练后的模型通过fine-tuning迁移到有监督的任务上。
从上图我们可以看出,GPT 采用 Transformer 来代替 LSTM 作为特征提取器,并基于语言模型进行训练。这里只使用了 Transformer 的 Decoder 部分,并且每个子层只有一个 Masked Multi Self-Attention(768 维向量和 12 个 Attention Head)和一个 Feed Forward,共叠加使用了 12 层的 Decoder。
这里简单解释下为什么只用 Decoder 部分:语言模型是利用上文预测下一个单词的,因为 Decoder 使用了 Masked Multi Self-Attention 屏蔽了单词的后面内容,所以 Decoder 是现成的语言模型。又因为没有使用 Encoder,所以也就不需要 encoder-decoder attention 了。
对于给定的非监督语料库的 Token 序列 ,基于语言模型的目标函数: m a x L 1 ( U ) = ∑ i log P ( u i ∣ u i − k , ⋯ , u i − 1 ; Θ ) max\space L_1(U)=\sum_i\log P(u_i|u_{i-k},\cdots,u_{i-1};\Theta) max L1(U)=i∑logP(ui∣ui−k,⋯,ui−1;Θ)其中, k k k 是上下文窗口的大小, P P P 为条件概率, Θ \Theta Θ 为条件概率的参数,参数更新采用 SGD。
GPT 输入文本和位置 Embedding(采用使用 one-hot 编码),经过 12 层的 Transformer 的 Decoder 后通过 Softmax 得到输出: h 0 = U W e + W p h_0=UW_e+W_p h0=UWe+Wp h l = t r a n s f o r m e r _ b l o c k ( h l − 1 ) ∀ l ∈ [ 1 , n ] h_l=transformer\_block(h_{l-1})\space\forall l\in[1,n] hl=transformer_block(hl−1) ∀l∈[1,n] P ( u ) = s o f t m a x ( h n W e T ) P(u)=softmax(h_nW_e^T) P(u)=softmax(hnWeT)其中, U = { u − k , ⋯ , u − 1 } U=\{u_{-k},\cdots,u_{-1}\} U={u−k,⋯,u−1} 是当前单词的前面 k k k 个 Token, n n n 为神经网络的层数, W e W_e We是 Token 的 Embedding 矩阵, W p W_p Wp 是位置编码的 Embedding 矩阵。
完成预训练后,我们会得到一个训练好的 Transformer 模型,接下来我们要用这个训练好的模型来完成特定的监督学习的任务。
假设我们有个带标签的数据集 C C C,即每一个 Token 序列 x 1 , x 2 , ⋯ , x m x^1,x^2,\cdots,x^m x1,x2,⋯,xm 都有一个标签 y y y。我们将 Token 序列输入,并通过 Transformer 模型得到输出的状态 ,然后将这个加到线性层进行输出并预测标签 y: P ( y ∣ x 1 , x 2 , ⋯ , x m ) = s o f t m a x ( h l m W y ) P(y|x^1,x^2,\cdots,x^m)=softmax(h_l^mW_y) P(y∣x1,x2,⋯,xm)=softmax(hlmWy)其中, W y W_y Wy 是线性层的权重。
所以针对该监督学习,我们也有新的目标函数: L 2 ( C ) = ∑ ( x , y ) log P ( y ∣ x 1 , ⋯ , x m ) L_2(C)=\sum_{(x,y)}\log P(y|x^1,\cdots,x^m) L2(C)=(x,y)∑logP(y∣x1,⋯,xm)另外,将预训练好的语言模型作为辅助目标进行 Fine-tuning 不仅可以使监督模型更具泛化性,还可以加速收敛。于是我们有: 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)其中, λ \lambda λ 为权重。
对于某些任务如文本分类等 word-level 的任务,我们可以像上述描述的方式来 Fine-tuning 模型;但是有些任务如问题回答等句子、句子对等结构化输入的任务需要稍作修改才能应用。
针对这种情况,作者提出了一种遍历式的方法(traversal-style),将结构化输入转换成预训练模型可以处理得到有序序列。
对输入转换避免了兼容不同任务,防止对模型进行大量更改,所有的转换包括添加随机初始化的开始标记( < s > <s>)和结束标记( < e >
上图是对不同任务进行微调的输入转换。将所有的结构化输入转换为 Token 序列,然后使用预训练模型(Transformer)进行处理,最后使用线性和 Softmax 层完成特定的监督学习任务。
对于文本蕴涵(Text Entailment)来说,作者将前提 p 和假设 h 令牌序列连接起来,并使用分隔符($)分开。
文本蕴含是指两个文本片段有指向关系。当认为一个文本片段真实时,可以推断出另一个文本片断的真实性。也就是说一个文本片段蕴涵了另一个文本片段的知识,可以分别称蕴涵的文本为前提,被蕴涵的文本为假设。
对于句子相似(Similarity)来说,为了消除两个句子之间的内在的顺序,作者以不同顺序合并了两个句子并以分隔符进行分割,然后独立地处理每一种顺序并得到两个句子的表征,对两个句子进行元素求和后送给 Linear 层。
对于问答和常识推理(Question Answering and Commonsense Reasoning)来说,有上下文文档 z z z 、问题 q q q 和可能答案的集合 { a k } \{a_k\} {ak},作者将上下文和问题与每个可能的答案连接起来并在中间添加分隔符令牌$[z;q;$;a_k]$ 。每个序列都将由模型独立处理,然后通过 Linear 层和 Softmax 层,从而在可能的答案上产生一个输出分布。
下图展示了推理任务的实验结果:
下图展示了问题回答和常识推理的实验结果:
下图展示了语义相似度和分类的实验结果:
下图左边展示的预训练语言模型中 Transformer 层数对结果的影响;右图展示了预训练不用 Fine-tuning 而直接使用预训练网络来解决多种类型任务的结果,横坐标为更新次数,纵坐标为模型相对表现:
def clf(x, ny, w_init=tf.random_normal_initializer(stddev=0.02), b_init=tf.constant_initializer(0), train=False):
with tf.variable_scope('clf'):
nx = shape_list(x)[-1]
w = tf.get_variable("w", [nx, ny], initializer=w_init)
b = tf.get_variable("b", [ny], initializer=b_init)
return tf.matmul(x, w)+b
def model(X, M, Y, train=False, reuse=False):
with tf.variable_scope('model', reuse=reuse):
we = tf.get_variable("we", [n_vocab+n_special+n_ctx, n_embd], initializer=tf.random_normal_initializer(stddev=0.02))
we = dropout(we, embd_pdrop, train)
X = tf.reshape(X, [-1, n_ctx, 2])
M = tf.reshape(M, [-1, n_ctx])
h = embed(X, we)
for layer in range(n_layer):
h = block(h, 'h%d'%layer, train=train, scale=True)
lm_h = tf.reshape(h[:, :-1], [-1, n_embd]) ##得到最后一个block的输出, 也就是上面所说的$h_l^m$
lm_logits = tf.matmul(lm_h, we, transpose_b=True)
lm_losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=lm_logits, labels=tf.reshape(X[:, 1:, 0], [-1])) ## 注意看预训练的语言模型的label只是将x向后移了一步
lm_losses = tf.reshape(lm_losses, [shape_list(X)[0], shape_list(X)[1]-1])
lm_losses = tf.reduce_sum(lm_losses*M[:, 1:], 1)/tf.reduce_sum(M[:, 1:], 1) ## 得到预训练模型的损失函数
clf_h = tf.reshape(h, [-1, n_embd]) // h 为transformer中最后一个block的输出
pool_idx = tf.cast(tf.argmax(tf.cast(tf.equal(X[:, :, 0], clf_token), tf.float32), 1), tf.int32)
clf_h = tf.gather(clf_h, tf.range(shape_list(X)[0], dtype=tf.int32)*n_ctx+pool_idx)
clf_h = tf.reshape(clf_h, [-1, 2, n_embd])
if train and clf_pdrop > 0:
shape = shape_list(clf_h)
shape[1] = 1
clf_h = tf.nn.dropout(clf_h, 1-clf_pdrop, shape)
clf_h = tf.reshape(clf_h, [-1, n_embd])
clf_logits = clf(clf_h, 1, train=train) ## 执行 上面公式中$softmax(h_l^m *W_y)$
clf_logits = tf.reshape(clf_logits, [-1, 2])
clf_losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=clf_logits, labels=Y) ## 得到监督学习的损失函数
return clf_logits, clf_losses, lm_losses
最终的loss函数为:
train_loss = tf.reduce_mean(clf_losses) + lm_coef*tf.reduce_mean(lm_losses)
GPT 是一种半监督学习,采用两阶段任务模型,通过使用无监督的 Pre-training 和有监督的 Fine-tuning 来实现强大的自然语言理解。在 Pre-training 中采用了 12 层的修改过的 Transformer Decoder 结构,在 Fine-tuning 中会根据不同任务提出不同的微调方式,从而达到适配各类 NLP 任务的目的。
GPT 与 ELMo 有很多相似的地方,比如说都采用了预训练的方式,但是 ELMo 是针对某一任务定制了一个架构,而 GPT 的目的在于适配多种任务;此外 ELMo 使用了 2 层的双向的 LSTM 结构而 GPT 使用了 12 层单向的 Transformer Dncoder 结构,更大的深度也加强了模型的学习能力(ELMo 不是不想用更深的,而是再深的话就学不动了)。