1. 背景
GPT 的全名: Generative Pre-Training, 其论文标题是 Improving Language Understanding by Generative Pre-Training.
相信大家都在有看到 GPT 系列在文本生成任务类等任务又刷新记录取得成绩如下图, 这篇文章主要来看看 GPT 与 GPT 的结构与任务是怎么样的.
2. GPT
GPT 的底层架构是 transformer, 是由 pre-training 和 fine-tuning 两部分构成的
预训练数据 GPT 使用 BooksCorpus 数据集, 它包含了 7000 本书, 共计 5GB 文字. 这样超大的数据规模, 是 GPT 成功的关键之一. Elmo 所使用的 1B Word Benchmark 数据集与之体量相当, 但被重新整理成单句, 因而丢失了长序列的样本, 是它没有被 GPT 选用的原因.
2.1 pre-training
pre-training 是采用 transformer 框架进行的, 不过对 transformer 改动了一下. transformer 在之前的博客中讲过, 详见
我们知道 transformer 里有 encoder 层和 decoder 层, 而 GPT 里主要用的是 decoder 层, 不过做了一点改变, 去掉了中间的 Encoder-Decoder Attention 层 (因为没有 encoder 层, 所以也不需要 Encoder-Decoder Attention 层) 如下图
整个过程如上图所示, 词向量 (token embedding) 和位置向量 (position embedding) 的和作为输入, 经过 12 层的 Masked Multi-Head Attention 和 Feed Forward(当然中间也包括 Layer Norm), 得到预测的向量和最后一个词的向量, 最后一个词的词向量会作为后续 fine-tuning 的输入.
GPT 本质上是自回归模型, 自回归的意思是指, 每次产生新单词后, 将新单词加到原输入句后面, 作为新的输入句.
模型会将语句输入上图所示的结构中, 预测下一个词, 然后再将新单词加入, 作为新的输入, 继续预测. 损失函数会计算预测值与实际值之间的偏差.
训练目标:
在预训练阶段, 只以语言模型为训练目标, 即图中的 Text Prediction.
公式 1 描述了这个典型的从左到右的语言模型: 在给定上文的条件下预测下一个词.
L_1(u)=\sum_ilogP(u_i|u_{i-k},...,u_{i-1};\Theta)
在公式 2 中, h0 表示网络第一个隐层, U 表示输入序列, We 表示 token embedding 矩阵, Wp 表示 position embedding.h1 到 hn 是各层 Transformer block 的输出隐层. 最终以 sotfmax 计算下一个词的概率.
- h_0=UW_e+W_p
- h_1=transformer\_block(h_{
- l-1
- }) \forall i \in [1,n]
- P(u)=softmax(h_nW^T_e)
问题: 无监督训练的终止条件是什么呢? 训练到什么时候可以停止呢? 像聚类是训练到分类比较稳定的情况下就停止了
我们可以通过准确率来评价训练何时停止. 训练的时候生成的文本和原文本进行比对, 得到准确率, 通过准确率是否达到预期值或是准确率是否一直上下波动等来确定是否该停止训练.
2.2 有监督 fine-tuning
在 Fine-Tune 阶段, 输入的样本是(序列 x1...xm, 标签 y). 给定一个序列, 预测标签的概率如下所示.
P(y|x^1,...,x^m)=softmax(h_l^mW_y)
Task Classifier 的训练目标是最大化全体有监督训练样本出现的概率, 即图 1 中的 Task Classifier.
L_2(C)=\sum_{(x,y)}logP(y|x^1,...,x^m)
在这个阶段, 语言模型的训练目标也被一并启用.
L_3(C)=L_2(C)+\lambda*L_1(C)
重新看一遍网络结构和 Fine-Tune 阶段的公式, 就会发现, 引入的参数, 只有 Wy.
先将大部分的参数通过无监督预训练训练好, 然后通过微调确定最后一个参数 w 的值, 以适应不同的任务. 利用无监督最后一个词的向量作为微调的输入(个人认为其实可以整句话的词向量作为输入, 但是没必要).
上图展示了对于不同 NLP 任务的微调过程:
分类任务: 输入就是文本, 最后一个词的向量直接作为微调的输入, 得到最后的分类结果(可以多分类)
推理任务: 输入是 先验 + 分隔符 + 假设, 最后一个词的向量直接作为微调的输入, 得到最后的分类结果, 即: 是否成立
句子相似性: 输入是 两个句子相互颠倒, 得到的最后一个词的向量再相加, 然后进行 Linear, 得到最后分类结果, 即: 是否相似
问答任务: 输入是上下文和问题放在一起与多个回答, 中间也是分隔符分隔, 对于每个回答构成的句子的最后一个词的向量作为微调的输入, 然后进行 Linear, 将多个 Linear 的结果进行 softmax, 得到最后概率最大的
问题: 对于问答任务, 最后多个 Linear 的结果如何进行 softmax?
对于问答任务来说, 一个问题对应多个回答, 而最后我要取最准确的回答 (分值最高) 作为结果, 我通过对多对问题答案做 transformer 后, 再分别做 linear, 可以将维度统一, 然后对多个 linear 进行 softmax~ 之前都是对一个 linear 做 softmax, 直接取概率值最大的即可, 但是现在多个 linear 如何进行 softmax 呢?
以上就是 GPT 的大致描述, 采用无监督的预训练和有监督的微调可以实现大部分的 NLP 任务, 而且效果显著, 但是还是不如 Bert 的效果好. 不过 GPT 采用单向 transformer 可以解决 Bert 无法解决的生成文本任务, 具体原因可见后续总结.
2.3 效果
3. GPT-2
GPT-2 依然沿用 GPT 单向 transformer 的模式, 只不过做了一些改进与改变. 那 GPT-2 相对于 GPT 有哪些不同呢? 看看下面几方面:
GPT-2 去掉了 fine-tuning 层: 不再针对不同任务分别进行微调建模, 而是不定义这个模型应该做什么任务, 模型会自动识别出来需要做什么任务. 这就好比一个人博览群书, 你问他什么类型的问题, 他都可以顺手拈来, GPT-2 就是这样一个博览群书的模型.
增加数据集: 这是一个比更大还更大的数据集. GPT-2 构造了一个新数据集, webText. 这些无监督样本, 全部来自于 Reddit 的外链, 而且是那些获得至少三个赞的外链, 共计 4500 万链接. 使用 Dragnet 和 Newspaper 两种工具来抽取网页内容. 过滤 2017 年 12 月之后的网页. 删去来自 Wikipedia 的网页. 去重. 最后还做一些琐碎的没有明说的所谓启发式清洗工作. 如此得到 8 百万网页共计 40GB 文本数据. WebText 数据集的特点在于全面而干净. 全面性来源于 Reddit 本身是一个门类广泛的社交媒体网站. 干净来源于作者的有意筛选, 至少三赞意味着获取的外链是有意义的或者有趣的.
增加网络参数: GPT-2 将 Transformer 堆叠的层数增加到 48 层, 隐层的维度为 1600, 参数量更是达到了 15 亿.(Bert 的参数量也才只有 3 亿)
调整 transformer: 将 layer normalization 放到每个 sub-block 之前, 并在最后一个 Self-attention 后再增加一个 layer normalization.
GPT-2 的输入是完全的文本, 什么提示都不加吗?
它也会加入提示词, 比如:"TL;DR:",GPT-2 模型就会知道是做摘要工作了. 输入的格式就是 文本 + TL;DR:,
模型 | Layers | d_size | ff_size | Heads | Parameters |
---|---|---|---|---|---|
GPT2-base | 12 | 768 | 3072 | 12 | 117M |
GPT2-medium | 24 | 1024 | 4096 | 16 | 345M |
GPT2-large | 36 | 1280 | 5120 | 20 | 774M |
GPT2-xl | 48 | 1600 | 6400 | 25 | 1558M |
3.1 效果
注意到, 标准模型 1542M 在 8 个测试集上斩获 7 个记录, 而最小的模型 117M 也能在其中 4 个测试集上挑起大梁. 而且 GPT-2 没有 Fine-Tune
4. GPT-2 应用 - 文本生成
4.1 字节对编码
GPT-2 模型在数据预处理时使用了字节对编码 (Byte Pair Encoding, 简称 BPE) 方法, BPE 是一种能够解决未登录词问题, 并减小词典大小的方法. 它综合利用了单词层面编码和字符层面编码的优势, 举例来说, 我们要对下面的字符串编码,
aaabdaaabac
字节对 aa 出现的次数最多, 所以我们将它替换成一个没在字符串中被用过的字符 Z ,
ZabdZabac
Z=aa
然后我们重复这个过程, 用 Y 替换 ab ,
- ZYdZYac
- Y=ab
- Z=aa
继续, 用 X 替换 ZY ,
- XdXac
- X=ZY
- Y=ab
- Z=aa
这个过程重复进行, 直到没有字节对出现超过一次. 当需要解码时, 就将上述替换过程反向进行.
PT-2 就是一个语言模型, 能够根据上文预测下一个单词, 所以它就可以利用预训练已经学到的知识来生成文本, 如生成新闻. 也可以使用另一些数据进行微调, 生成有特定格式或者主题的文本, 如诗歌, 戏剧. 所以接下来, 我们会用 GPT-2 模型进行一个文本生成.
4.2 预训练模型生成新闻
想要直接运行一个预训练好的 GPT-2 模型, 最简单的方法是让它自由工作, 即随机生成文本. 换句话说, 在开始时, 我们给它一点提示, 即一个预定好的起始单词, 然后让它自行地随机生成后续的文本.
但这样有时可能会出现问题, 例如模型陷入一个循环, 不断生成同一个单词. 为了避免这种情况, GPT-2 设置了一个 top-k 参数, 这样模型就会从概率前 k 大的单词中随机选取一个单词, 作为下一个单词. 下面是选择 top-k 的函数的实现,
- import random
- def select_top_k(predictions, k=10):
- predicted_index = random.choice(
- predictions[0, -1, :].sort(descending=True)[1][:10]).item()
- return predicted_index
下面引入 GPT-2 模型, 我们将使用在 PyTorch-Transformers 模型库中封装好的 GPT2Tokenizer() 和 GPT2LMHeadModel() 类来实际看一下 GPT-2 在预训练后的对下一个词预测的能力. 首先, 需要安装 PyTorch-Transformers.
!pip install pytorch_transformers==1.0 # 安装 PyTorch-Transformers
使用 PyTorch-Transformers 模型库, 先设置好准备输入模型的例子, 使用 GPT2Tokenizer() 建立分词器对象对原句编码.
- import torch
- from pytorch_transformers import GPT2Tokenizer
- import logging
- logging.basicConfig(level=logging.INFO)
- # 载入预训练模型的分词器
- tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
- # 使用 GPT2Tokenizer 对输入进行编码
- text = "Yesterday, a man named Jack said he saw an alien,"
- indexed_tokens = tokenizer.encode(text)
- tokens_tensor = torch.tensor([indexed_tokens])
- tokens_tensor.shape
接下来使用 GPT2LMHeadModel() 建立模型, 并将模型模式设为验证模式. 由于预训练模型参数体积很大, 且托管在外网, 所以本次先从网盘下载预训练模型, 本地无需此步骤.
- from pytorch_transformers import GPT2LMHeadModel
- # 读取 GPT-2 预训练模型
- model = GPT2LMHeadModel.from_pretrained("./")
- model.eval()
- total_predicted_text = text
- n = 100 # 预测过程的循环次数
- for _ in range(n):
- with torch.no_grad():
- outputs = model(tokens_tensor)
- predictions = outputs[0]
- predicted_index = select_top_k(predictions, k=10)
- predicted_text = tokenizer.decode(indexed_tokens + [predicted_index])
- total_predicted_text += tokenizer.decode(predicted_index)
- if '<|endoftext|>' in total_predicted_text:
- # 如果出现文本结束标志, 就结束文本生成
- break
- indexed_tokens += [predicted_index]
- tokens_tensor = torch.tensor([indexed_tokens])
- print(total_predicted_text)
运行结束后, 我们观察一下模型生成的文本, 可以看到, 大致感觉上这好像是一段正常的文本, 不过, 仔细看就会发现语句中的逻辑问题, 这也是之后研究人员会继续攻克的问题.
除了直接利用预训练模型生成文本, 我们还可以使用微调的方法使 GPT-2 模型生成有特定风格和格式的文本.
4.3 微调生成戏剧文本
接下来, 我们将使用一些戏剧剧本对 GPT-2 进行微调. 由于 OpenAI 团队开源的 GPT-2 模型预训练参数为使用英文数据集预训练后得到的, 虽然可以在微调时使用中文数据集, 但需要大量数据和时间才会有好的效果, 所以这里我们使用了英文数据集进行微调, 从而更好地展现 GPT-2 模型的能力.
首先, 下载训练数据集, 这里使用了莎士比亚的戏剧作品《罗密欧与朱丽叶》 http://shakespeare.mit.edu/romeo_juliet/full.html 作为训练样本. 数据集已经提前下载好并放在云盘中, 链接: https://pan.baidu.com/s/1LiTgiake1KC8qptjRncJ5w 提取码: km06
- with open('./romeo_and_juliet.txt', 'r') as f:
- dataset = f.read()
- len(dataset)
预处理训练集, 将训练集编码, 分段.
- indexed_text = tokenizer.encode(dataset)
- del(dataset)
- dataset_cut = []
- for i in range(len(indexed_text)//512):
- # 将字符串分段成长度为 512
- dataset_cut.append(indexed_text[i*512:i*512+512])
- del(indexed_text)
- dataset_tensor = torch.tensor(dataset_cut)
- dataset_tensor.shape
这里使用 PyTorch 提供的 DataLoader() 构建训练集数据集表示, 使用 TensorDataset() 构建训练集数据迭代器.
- from torch.utils.data import DataLoader, TensorDataset
- # 构建数据集和数据迭代器, 设定 batch_size 大小为 2
- train_set = TensorDataset(dataset_tensor,
- dataset_tensor) # 标签与样本数据相同
- train_loader = DataLoader(dataset=train_set,
- batch_size=2,
- shuffle=False)
- train_loader
检查是否机器有 GPU, 如果有就在 GPU 运行, 否则就在 CPU 运行.
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
- device
开始训练.
- from torch import nn
- from torch.autograd import Variable
- import time
- pre = time.time()
- epoch = 30 # 循环学习 30 次
- model.to(device)
- model.train()
- optimizer = torch.optim.Adam(model.parameters(), lr=1e-5) # 定义优化器
- for i in range(epoch):
- total_loss = 0
- for batch_idx, (data, target) in enumerate(train_loader):
- data, target = Variable(data).to(device), Variable(
- target).to(device)
- optimizer.zero_grad()
- loss, logits, _ = model(data, labels=target)
- total_loss += loss
- loss.backward()
- optimizer.step()
- if batch_idx == len(train_loader)-1:
- # 在每个 Epoch 的最后输出一下结果
- print('average loss:', total_loss/len(train_loader))
- print('训练时间:', time.time()-pre)
训练结束后, 可以使模型生成文本, 观察输出.
- text = "From fairest creatures we desire" # 这里也可以输入不同的英文文本
- indexed_tokens = tokenizer.encode(text)
- tokens_tensor = torch.tensor([indexed_tokens])
- model.eval()
- total_predicted_text = text
- # 使训练后的模型进行 500 次预测
- for _ in range(500):
- tokens_tensor = tokens_tensor.to('cuda')
- with torch.no_grad():
- outputs = model(tokens_tensor)
- predictions = outputs[0]
- predicted_index = select_top_k(predictions, k=10)
- predicted_text = tokenizer.decode(indexed_tokens + [predicted_index])
- total_predicted_text += tokenizer.decode(predicted_index)
- if '<|endoftext|>' in total_predicted_text:
- # 如果出现文本结束标志, 就结束文本生成
- break
- indexed_tokens += [predicted_index]
- if len(indexed_tokens)> 1023:
- # 模型最长输入长度为 1024, 如果长度过长则截断
- indexed_tokens = indexed_tokens[-1023:]
- tokens_tensor = torch.tensor([indexed_tokens])
- print(total_predicted_text)
从生成结果可以看到, 模型已经学习到了戏剧剧本的文本结构. 但是仔细读起来会发现缺少逻辑和关联, 这是因为由于时间和设备的限制, 对模型的训练比较有限. 如果有条件可以用更多的数据, 训练更长的时间, 这样模型也会有更好的表现.
4.4 评价指标
https://zhuanlan.zhihu.com/p/88502676
模型 A:
融合模型, 主要目标有两个: 生成长文本, 增加生成的故事和提示语的相关性. 使用了门限自注意力机制和融合机制((Sriram et al., 2018), 融合机制的含义: 第一个 seq2seq 模型进行训练, 第二个 seq2seq 在训练时使用第一个模型的隐层.
模型 B:
GPT-2 模型, 采用 117M 的预训练模型. 作者说明为啥要采用 117M 的, 而不是 340M 的呢. 在做实验的时候, 更大的预训练模型还没有放出来呢. gpt-2 详细介绍, 从简. 网上的资料很多, 比如:
解码算法:
起源于机器翻译的发明, 大部分生成任务都是采用 beam search 算法.(Shang et al., 2015; Serbanet al., 2016). 但是研究表明, beam search 算法在开放领域的文本生成中, 容易出现重复, 一般性词汇. 见(Holtzman et al., 2019).
top-k 采样, 在解码的每一步, 先在整个词汇表的概率分布上取 k 个 token, 然后重新规范化, 然后从中选择一个作为解码的 token.
top-k 采样中, k 值的影响:
在无条件生成长文本的深度模型中, 大的 k 值代表跟高的熵. 更小的 k 值, 生成的文本, 往往更简单, 重复度高. 同时发现, 更小的 k 值, GPT2-117 更易于从引导语中复制, 包含更多的动词和代词, 更少的名词和形容词, 名词更具体, 更小范围的句式.
如果将 k 值设为很大, 比如词汇表的大小. 在大多数指标上和人类的指标相似, 但是在常识推理, 世界知识, 句子之间的连贯性等表现不佳.
4.4.1 困惑度
ppl 表示词级别的困惑度, 是在 WritingPrompts-1024(引导语不超过 1024 个词)数据子集上.
困惑度 (perplexity) 的基本思想是: 给测试集的句子赋予较高概率值的语言模型较好, 当语言模型训练完之后, 测试集中的句子都是正常的句子, 那么训练好的模型就是在测试集上的概率越高越好, 公式如下:
由公式可知, 句子概率越大, 语言模型越好, 迷惑度越小. 困惑度 p 可以理解为, 如果每个时间步都根据语言模型计算的概率分布随机挑词, 那么平均情况下, 挑多少个词才能挑到正确的那个
4.4.2 Prompt ranking accuracy
这个指标的定义和评价方法, 来自《Hierarchical Neural Story Generation》. 主要是关注引导语和生成的故事之间的相关性. 具体做法是: 在测试集中选择一对 (p,g),p 表示引导语, g 表示生成的故事, 在随机选取其他的引导语 p1-p9, 然后计算 p 和 g 的 likelihood. 条件一:(p,g) 的相似性比 (p1,g) 的相似性大. 那么就取 10000 个测试集中的(p,g), 满足条件一的部分占比, 就称为 Prompt ranking accuracy.
GPT2-117 的得分: 80.16%
Fusion Model 的 39.8%
4.4.3 句子嵌入的相似度
计算引导语和生成的故事的句子嵌入 (用 GloVe 取每个词的平均嵌入值) 的余弦相似度.
在 top-k 算法中, k 取不同值, 分别计算每个模型, 结果如下:
命名实体的使用: 主要看引导语中命名实体在生成的故事中出现的次数.
4.4.4 评价连贯性
连贯性的评价方法, 来自《Modeling local coherence: An entity-based approach》, 主要思想是, 在测试数据集中, 对于一个故事 s0, 选择前面 15 个句子, 打乱顺序, 生成 14 个乱序的故事 s1-s14. 然后用语言模型计算 s0-s14 的可能性. 对于 s1-s14, 如果可能性大于 s0, 就称为反例. 错误率定义为反例的占比.
GPT2-117 的得分: 2.17%
Fusion Model 的 3.44%
4.4.5 评价单词的重复性和 rareness
GPT2-117 比 Fusion Model 的重复性要小, rareness 要高点. 但是差距很小.
5. 对话任务
训练流程可以参加 https://cloud.tencent.com/developer/article/1877398 https://github.com/yangjianxin1/GPT2-chitchat
详解参见 https://blog.csdn.net/g534441921/article/details/104312983
总结
因 GPT 是一自回归语言模型, 实验的场景设定是 zero-shot 预测, 没有特别针对下游任务进行设计和微调, 所以在各种 NLP 任务如阅读理解, 问答系统, 文本摘要, 翻译系统等虽然也能取的一定的效果, 但是比 BERT 这类经过微调的监督模型会有差距.
为什么 GPT 模型相比 BERT 更适合文本生成?
目前主流的文本生成是从左到右进行的, 生成第 N 个 token 时, 只能用到第 1~N-1 个 token 的信息, 无法用到后面的 token 信息, 而 GPT/GPT-2 正是用相同的从左到右生成任务进行预训练(也就是所谓的单向), 预训练与下游任务一致;
而 BERT 使用 MLM 任务进行预训练, 即给出前后上下文, 预测文中被 mask 掉的一个 token, 这种预训练方式使 BERT 可以充分利用双向的上下文信息(前文和后文), 但在文本生成任务中, 模型天然只能使用前文信息, 与 BERT 的预训练任务存在较大的 gap, 因此 BERT 并不适合直接用于文本生成任务.
作者宣称, 即便 GPT-2 在参数上十倍于 GPT, 五倍于 Bert, 却依然在 WebText 数据集上是欠拟合的.
网络越叠越大, 也许没有尽头, 而在找到更高效的网络之前, 我们的最佳选择仍然是 Transformer.
Ref
https://zhuanlan.zhihu.com/p/96791725 GPT/GPT2 介绍
- https://github.com/Morizeyao/GPT2-Chinese
- GPT2
GPT2 聊天机器人
http://jalammar.github.io/illustrated-gpt2/ GPT2 详解
- http://blog.zhangcheng.ai/2019/04/28/汝果欲学诗,功夫在诗外-论gpt-2无监督预训练的/
- https://www.cnblogs.com/wwj99/p/12503545.html pytorch GPT2
https://zhuanlan.zhihu.com/p/88502676 GPT2 训练
https://cloud.tencent.com/developer/article/1877398 GPT2 聊天机器人训练
https://blog.csdn.net/g534441921/article/details/104312983 GPT2 对话任务综述
来源: https://www.qcloud.com/developer/article/1877406