一 背景介绍

GPT2模型是OpenAI组织在2018年于GPT模型的基础上发布的新预训练模型,其论文原文为 language_models_are_unsupervised_multitask_learners
GPT2模型的预训练语料库为超过40G的近8000万的网页文本数据,GPT2的预训练语料库相较于GPT而言增大了将近10倍。

二 GPT2与GPT 模型的区别

3 GPT2模型结构

GPT-2 模型由多层单向 Transformer 的解码器部分构成,本质上是自回归模型,自回归的意思是指,每次产生新单词后,将新单词加到原输入句后面,作为新的输入句。其中 Transformer 解码器结构如下图:

GPT-2 模型中只使用了多个 Masked Self-Attention 和 Feed Forward Neural Network 两个模块。

GPT-2 模型会将语句输入上图所示的结构中,预测下一个词,然后再将新单词加入,作为新的输入,继续预测。损失函数会计算预测值与实际值之间的偏差。

从上一节我们了解到 BERT 是基于双向 Transformer 结构构建,而 GPT-2 是基于单向 Transformer,这里的双向与单向,是指在进行注意力计算时,BERT会同时考虑被遮蔽词左右的词对其的影响,而 GPT-2 只会考虑在待预测词位置左侧的词对待预测词的影响。

通过上述数据预处理方法和模型结构,以及大量的数据训练出了 GPT-2 模型。OpenAI 团队由于安全考虑,没有开源全部训练参数,而是提供了小型的预训练模型,接下来我们将在 GPT-2 预训练模型的基础上进行。

4 使用GPT生成文本

预训练模型生成新闻
直接运行一个预训练好的 GPT-2 模型:给定一个预定好的起始单词,然后让它自行地随机生成后续的文本。
问题1:重复性问题。重复生成同一个单词。
解决方式1:从概率前 k 大的单词中随机选取一个单词,作为下一个单词。
问题2:逻辑问题
解决方式:未知。知识??

import randomimport logginglogging.basicConfig(level=logging.INFO)import torchfrom pytorch_transformers import GPT2Tokenizerfrom pytorch_transformers import GPT2LMHeadModel# 选择 top-k 的函数的实现,def select_top_k(predictions, k=10):    predicted_index = random.choice(        predictions[0, -1, :].sort(descending=True)[1][:10]).item()    return predicted_index# 载入预训练模型的分词器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# 读取 GPT-2 预训练模型model = GPT2LMHeadModel.from_pretrained("./")model.eval()total_predicted_text = textn = 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 '' in total_predicted_text:        # 如果出现文本结束标志,就结束文本生成        break    indexed_tokens += [predicted_index]    tokens_tensor = torch.tensor([indexed_tokens])print(total_predicted_text)#运行结束后,我们观察一下模型生成的文本,可以看到,大致感觉上这好像是一段正常的文本,不过,仔细看就会发现语句中的逻辑问题,这也是之后研究人员会继续攻克的问题。

微调生成风格文本
接下来,我们将使用一些戏剧剧本对 GPT-2 进行微调。由于 OpenAI 团队开源的 GPT-2 模型预训练参数为使用英文数据集预训练后得到的,虽然可以在微调时使用中文数据集,但需要大量数据和时间才会有好的效果,所以这里我们使用了英文数据集进行微调,从而更好地展现 GPT-2 模型的能力。

首先,下载训练数据集,这里使用了莎士比亚的戏剧作品《罗密欧与朱丽叶》作为训练样本。
数据集已经提前下载好并放在云盘中,
链接:https://pan.baidu.com/s/1LiTgiake1KC8qptjRncJ5w 提取码:km06

with open('./romeo_and_juliet.txt', 'r') as f:    dataset = f.read()#预处理训练集,将训练集编码、分段。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 大小为 2train_set = TensorDataset(dataset_tensor,                          dataset_tensor)  # 标签与样本数据相同train_loader = DataLoader(dataset=train_set,                          batch_size=2,                          shuffle=False)#检查是否机器有 GPU,如果有就在 GPU 运行,否则就在 CPU 运行。device = torch.device("cuda" if torch.cuda.is_available() else "cpu")#开始训练。from torch import nnfrom torch.autograd import Variableimport timepre = 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 '' 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)#从生成结果可以看到,模型已经学习到了戏剧剧本的文本结构。但是仔细读起来会发现缺少逻辑和关联,这是因为由于时间和设备的限制,对模型的训练比较有限。如果有条件可以用更多的数据,训练更长的时间,这样模型也会有更好的表现。

5 GPT-2从finetune 到部署

用到的训练数据是我从网上爬下来的老友记十季的剧本:
https://pan.baidu.com/share/init?surl=blbeVCro1nErh34KUGrPIA
提取码: 40bn

如何用nshepperd 的 gpt-2 库来 finetune 模型
步骤一:下载项目:git clone https://github.com/nshepperd/gpt-2
步骤二:安装所需环境:pip install -r requirements.txt
步骤三:准备模型:python download_model.py 345M
步骤四:准备数据。放到/data目录下
步骤五: finetune【根据机器训练速度会不同,但基本上两三千步就能看到些还算不错的结果了】

如想要 finetune 时更快些的话,可以预编码数据成训练格式。
PYTHONPATH=src ./encode.py data/friends.txt data/friends.txt.npz
PYTHONPATH=src ./train.py –dataset data/friends.txt.npz –model_name 345M
其他值得关注参数:
learning_rate: 学习率,默认2e-5,可根据数据集大小适当调整,数据集大的话可以调大些,小的话可以调小些。
sample_every: 每多少步生成一个样本看看效果,默认 100。
run_name: 当前训练命名,分别在samples和checkpoint文件夹下创建当前命名的子文件夹,之后生成的样本和保存的模型分别保存在这两个子文件夹。训练中断想继续训练就可以用同样的run_name,如想跑不同任务请指定不同run_name.

步骤六:存储模型:将生成的模型,更改名字,放入models文件夹里,替换掉原来的模型(一定要记得将之前的模型备份!)。
步骤七:生成样本
自由生成 python src/generate_unconditional_samples.py –top_k 40 –temperature 0.9 –model_name 345M
然后是命题作文,有条件互动生成环节。
python src/interactive_conditional_samples.py –top_k 40 –temperature 0.9 –model_name 345M
运行后会出现一个互动框,输入你想让模型续写的话,给定输入:Rachel loves Andy
在 Rachel loves Andy 两秒后,完美跑题,伤心,不过感觉后半段还是很有意思。
关于参数 –topk 还有 –temperature,会影响生成的效果,可自己尝试调节一下,上面例子使用的是两个推荐设定。
到此 finetune 一个基本 GPT-2 的过程就完了,是不是比想象中要简单很多。

GPT-2-simple更加简单地应用

# 步骤一:下载模型import gpt_2_simple as gpt2gpt2.download_gpt2(model_name="345M")# 步骤二:训练# 很直观,直接调用 gpt2.finetune 就可以了。sess = gpt2.start_tf_sess()gpt2.finetune(sess,              dataset="friends.txt",              model_name='345M',              steps=1000,              restore_from='fresh',              print_every=10,              sample_every=200,              save_every=500              )#gpt2.finetune 训练参数介绍:#  restore_from: fresh 是指从 GPT2 原模型开始, 而 latest是从之前 finetune 保存的模型继续训练#  sample_every: 每多少步输出样本,看看训练效果#  print_every: 每多少步打印训练的一些参数,从左到右,步数、时间,loss,平均loss#  learning_rate: 学习率 (默认 1e-4, 如果数据小于1MB的话可以调低到 1e-5)#  run_name: 运行的时候,保存模型到checkpoint下子文件夹,默认 run1#步骤三:生成环节,先把模型 load 进来。然后生成文本。# 要大量生成文本的话可以用gpt2.generate_to_file.sess = gpt2.start_tf_sess()gpt2.load_gpt2(sess)gpt2.generate(sess)# gpt2.generate 里面也有很多参数可以设置:#length: 生成文本长度 (默认 1023, 也是可设最大长度)#temperature: temperature 越高,生成越随意。 (默认 0.7,推荐 0.7 到 1.0之间)#top_k: 将输出限定在 top k 里面 (默认0,也就是不使用。推荐在生成效果差的时候使用,可以设top_k=40)#truncate: 从指定符号阶段生成文本 (比如设 truncate='', 那么就会取第一个''前的文本作为输出). 可以和一个比较小的length值搭配使用.#include_prefix: 如果用了 truncate 和 include_prefix=False, 那么在返回文本中就不会包含prefix里的文本。

flask 部署
步骤一:提供模型到models/ 下(Huggingface 的 pytorch 格式)。
命名为gpt2-pytorch_model.bin
也可以先用它提供的实例模型来做个实验:
mkdir models
curl –output models/gpt2-pytorch_model.bin https://s3.amazonaws.com/models.huggingface.co/bert/gpt2-pytorch_model.bin
步骤二:得到访问端口:python deployment/run_server.py.
步骤三:直接用浏览器访问就行了,如果是远程访问把 0.0.0.0 改成服务器IP就好了。

如何部署自己的模型
步骤一:将 tensorflow 的模型转换成 Pytorch 的模型。
这里可以用 Huggingface 的pytorch-pretrained-BERT 库里面的转换脚本,先根据指示安装库,之后运行以下脚本。

export GPT2_DIR=模型所在文件夹
pytorch_pretrained_bert convert_gpt2_checkpoint $GPT2_DIR/model_name output_dir/ path_to_config/config.json

上面命令 convert_gpt2_checkpoint 后三个参数分别是,输入的 tensorflow 模型路径,转换输出的 pytorch 模型路径,模型的配置参数文件。

需要注意的是,因为这几个库之间的不统一,所以下载下来 345M 模型的设置文件在转换时会出错,需要添加一些参数。前面有下载 345M 模型的话,会发现模型文件夹下有一个设置文件 hparams.json。
cp hparams.json hparams_convert.json #复制一份来修改
之后在 hparams_convert.json里添加几个参数,改成下面这样:
{
“n_vocab”: 50257,
“n_ctx”: 1024,
“n_embd”: 1024,
“n_head”: 16,
“n_layer”: 24,
“vocab_size”:50257,
“n_positions”:1024,
“layer_norm_epsilon”:1e-5,
“initializer_range”: 0.02
}
将这个设置文件指定到转换命令convert_gpt2_checkpoint后面相应参数去。

获得转换模型后,把它放入models/ 中去,并且重命名,之后把 deployment/GPT2/config.py 里面的参数设定改成再改成 345M 大模型的参数就好了。

class GPT2Config(object):
def init(
self,
vocab_size_or_config_json_file=50257,
n_positions=1024,
n_ctx=1024,
n_embd=1024,
n_layer=24,
n_head=16,
layer_norm_epsilon=1e-5,
initializer_range=0.02,
):
最后运行 run_server.py,成功载入模型,部署完成!之后测试一下,发现确实是已经 finetune 好的老友记模型。

参考:
GPT2 代码解释: GPT2LMHeadModel类、GPT2Model类、Block类、MLP类与Attention类
GPT-2 进行文本生成
AI界最危险武器 GPT-2 使用指南:从Finetune到部署