作者: 陈茜
创建日期: 2023/04/17
最后修改: 2024/04/12
描述: 使用 KerasNLP 的 GPT2 模型和 samplers
进行文本生成。
在本教程中,您将学习如何使用KerasNLP加载预训练的大型语言模型(LLM) - GPT-2 模型(最初由 OpenAI 发明),对其进行微调以适应特定的文本风格,并根据用户输入(也称为提示)生成文本。您还将学习 GPT2 如何快速适应非英语语言,例如中文。
Colab 提供不同类型的运行环境。确保进入 Runtime -> Change runtime type 并选择 GPU 硬件加速器运行时(应具有 >12G 的主机 RAM 和 ~15G 的 GPU RAM),因为您将对 GPT-2 模型进行微调。在 CPU 运行时运行此教程将需要数小时。
该示例使用Keras 3在"tensorflow"
、"jax"
或 "torch"
中工作。Keras 3 的支持已集成到 KerasNLP 中,只需更改 "KERAS_BACKEND"
环境变量即可选择您选择的后端。我们在下面选择 JAX 后端。
!pip install git+https://github.com/keras-team/keras-nlp.git -q
import os
os.environ["KERAS_BACKEND"] = "jax" # 或 "tensorflow" 或 "torch"
import keras_nlp
import keras
import tensorflow as tf
import time
keras.mixed_precision.set_global_policy("mixed_float16")
大型语言模型(LLMs)是一种机器学习模型,经过大量文本数据的训练,以生成用于各种自然语言处理(NLP)任务的输出,例如文本生成、问答和机器翻译。
生成性 LLM 通常基于深度学习神经网络,如 Google 研究人员在 2017 年发明的Transformer 架构,并在大量文本数据上训练,通常涉及数十亿个单词。这些模型,如 Google 的LaMDA 和PaLM,是使用来自各种数据源的大型数据集进行训练,使它们能够为许多任务生成输出。生成性 LLM 的核心是预测句子中的下一个单词,通常称为 因果语言模型预训练。通过这种方式,LLMs 可以根据用户提示生成连贯的文本。有关语言模型的更多教学讨论,您可以参考斯坦福 CS324 LLM 课程。
大型语言模型构建复杂且从头训练成本高昂。幸运的是,有可以立即使用的预训练 LLM。 KerasNLP 提供了大量预训练的检查点,允许您实验 SOTA 模型而无需自己进行训练。
KerasNLP 是一个自然语言处理库,支持用户整个开发周期。KerasNLP 提供预训练模型和模块化构建块,使开发人员可以轻松重用预训练模型或堆叠他们自己的 LLM。
简而言之,对于生成性 LLM,KerasNLP 提供:
generate()
方法的预训练模型,例如 keras_nlp.models.GPT2CausalLM
和 keras_nlp.models.OPTCausalLM
。KerasNLP 提供了一些预训练的模型,例如Google Bert和GPT-2。您可以在KerasNLP 仓库中查看可用模型的列表。
加载 GPT-2 模型非常简单,如下所示:
# 为了加速训练和生成,我们使用长度为128的预处理器
# 而不是完整长度1024。
preprocessor = keras_nlp.models.GPT2CausalLMPreprocessor.from_preset(
"gpt2_base_en",
sequence_length=128,
)
gpt2_lm = keras_nlp.models.GPT2CausalLM.from_preset(
"gpt2_base_en", preprocessor=preprocessor
)
一旦加载了模型,您可以立即用它来生成一些文本。运行下面的单元格来试试。只需调用一个函数 generate():
start = time.time()
output = gpt2_lm.generate("My trip to Yosemite was", max_length=200)
print("\nGPT-2 output:")
print(output)
end = time.time()
print(f"TOTAL TIME ELAPSED: {end - start:.2f}s")
GPT-2 output:
My trip to Yosemite was pretty awesome. The first time I went I didn't know how to go and it was pretty hard to get around. It was a bit like going on an adventure with a friend. The only things I could do were hike and climb the mountain. It's really cool to know you're not alone in this world. It's a lot of fun. I'm a little worried that I might not get to the top of the mountain in time to see the sunrise and sunset of the day. I think the weather is going to get a little warmer in the coming years.
这篇文章更深入地介绍了如何走这条小径。它介绍了如何在内华达山脉远足,如何与内华达山脉一起远足,如何在内华达山脉远足,如何到达山顶,以及如何用自己的装备到达山顶。
内华达山脉是优胜美地公园中非常受欢迎的小径
TOTAL TIME ELAPSED: 25.36s
再试一个:
start = time.time()
output = gpt2_lm.generate("That Italian restaurant is", max_length=200)
print("\nGPT-2 output:")
print(output)
end = time.time()
print(f"TOTAL TIME ELAPSED: {end - start:.2f}s")
GPT-2 output:
That Italian restaurant is known for its delicious food, and the best part is that it has a full bar, with seating for a whole host of guests. And that's only because it's located at the heart of the neighborhood.
意大利餐厅的菜单相当简单:
菜单由三道主菜组成:
意大利香肠
博洛尼亚式肉酱
香肠
带奶酪的博洛尼亚肉酱
奶油酱
带奶酪的意大利香肠
带奶酪的博洛尼亚肉酱
主菜单还包括一些其他菜品。
有两个桌子:一个提供香肠和带奶酪的博洛尼亚式肉酱的菜单(提供香肠和带奶酪的博洛尼亚式肉酱的菜单)和一个提供香肠和带奶酪的博洛尼亚式肉酱的菜单。两个桌子每天24小时、每周7天开放。
TOTAL TIME ELAPSED: 1.55s
注意第二次调用的速度快了多少。这是因为计算图在第一次运行时被 XLA 编译,并在第二次运行时在后台重用。
生成文本的质量看起来可以,但我们可以通过微调来提高它。
接下来,我们将实际微调模型以更新其参数,但在此之前,让我们看看我们用于处理 GPT2 的全套工具。
GPT2 的代码可以在 这里 找到。从概念上讲,GPT2CausalLM
可以分层拆分为 KerasNLP 中的几个模块,所有这些模块都有一个 from_preset() 函数来加载一个预训练模型:
keras_nlp.models.GPT2Tokenizer
: GPT2 模型使用的分词器,它是一个 字节对编码器。keras_nlp.models.GPT2CausalLMPreprocessor
: GPT2 因果语言模型训练使用的预处理器。它执行分词以及其他预处理工作,例如创建标签和附加结束标记。keras_nlp.models.GPT2Backbone
: GPT2 模型,它是一堆 keras_nlp.layers.TransformerDecoder
。这通常简称为 GPT2
。keras_nlp.models.GPT2CausalLM
: 包装 GPT2Backbone
,它将 GPT2Backbone
的输出与嵌入矩阵相乘,以生成词汇标记的逻辑值。现在您已经掌握了 KerasNLP 中的 GPT-2 模型的知识,您可以进一步微调该模型,使其生成特定风格的文本,无论是简短或冗长,严格或随意。在本教程中,我们将使用 Reddit 数据集作为示例。
import tensorflow_datasets as tfds
reddit_ds = tfds.load("reddit_tifu", split="train", as_supervised=True)
让我们来看看来自reddit TensorFlow数据集的示例数据。它包含两个特征:
for document, title in reddit_ds:
print(document.numpy())
print(title.numpy())
break
b"我和一个朋友决定上个星期天去海滩。我们装好东西出发了。我们大约走了一半的路程时,我决定在没吃到海鲜之前不离开。\n\n现在我不是在说红龙虾。各位朋友,我是在说南方低煮海鲜。我找到了餐馆并问了路。我不知道你们中有没有人听说过Tybee岛上的蟹屋,但我告诉你,绝对值得。\n\n我们到了,很快就被安排了座位。我们决定点一个两人份的海鲜拼盘,分着吃。服务员把它分到了两个盘子里。食物的量令人震惊。两种螃蟹,虾,贻贝,小龙虾,安杜伊香肠,红薯,和玉米棒。我成功吃完了,还有我的一些朋友的龙虾和贻贝。今天真是个吃狂欢的日子。我们吃完后付了钱,去了海滩。\n\n关于海鲜的搞笑事。它通过我体内的速度比一个肯尼亚人还快。\n\n我们到了,四处走了一会儿。当我们到达海滩大约45分钟后,我感到肚子深处传来了隆隆声。我忽略了它,我不想让我的肚子破坏我们的乐趣。我压抑着这种感觉,继续前进。大约15分钟后,感觉又回来了,比之前更强烈。再次,我忽略了它,继续行进。5分钟后,我感觉就像我的肚子里刚刚发生了一起核反应。我开始跑了起来。我对我的朋友大喊快点儿。\n\n在沙滩上跑步是极其困难的,如果你不知道这一点。我们上了他的车,我大喊让他开快点。我的肚子在尖叫,如果他不快点,我就要在他车里生个婴儿了,绝对不美观。在经历了几次红灯和我像临产的女人一样尖叫后,我们终于到了商店。\n\n我几乎撕开了他的车门,冲了进去。我跑到洗手间,打开门,在几乎脱下裤子之前,闸门就开了,洪水般的粪便从我的屁股喷涌而出。\n\n我结束时感到屁股上有东西湿乎乎的。我用手擦了擦,以为是溅出的水。不,后果让我沐浴在我蹂躏马桶后的恶果中。我抓了一堆纸巾,给自己来了一次妓女澡。\n\n我用空气清新剂喷洒了洗手间,然后离开了。一位老年女士迅速走进来并关上了门。我正准备走开时,听到了窒息声。我并没有走,而是跑了起来。我到了车上,叫他赶紧离开。"
b'喜欢海鲜'
在我们的案例中,我们正在进行语言模型中的下一个单词预测,因此我们只需要“document”特征。
train_ds = (
reddit_ds.map(lambda document, _: document)
.batch(32)
.cache()
.prefetch(tf.data.AUTOTUNE)
)
现在你可以使用熟悉的fit()函数来微调模型。注意,由于GPT2CausalLM
是一个keras_nlp.models.Task
实例,因此preprocessor
将在fit
方法内部被自动调用。
如果要将其训练到完全训练状态,这一步需要相当多的GPU内存和时间。在这里,我们只是使用数据集的一部分进行演示。
train_ds = train_ds.take(500)
num_epochs = 1
# 线性衰减学习率。
learning_rate = keras.optimizers.schedules.PolynomialDecay(
5e-5,
decay_steps=train_ds.cardinality() * num_epochs,
end_learning_rate=0.0,
)
loss = keras.losses.SparseCategoricalCrossentropy(from_logits=True)
gpt2_lm.compile(
optimizer=keras.optimizers.Adam(learning_rate),
loss=loss,
weighted_metrics=["accuracy"],
)
gpt2_lm.fit(train_ds, epochs=num_epochs)
500/500 ━━━━━━━━━━━━━━━━━━━━ 75s 120ms/step - accuracy: 0.3189 - loss: 3.3653
<keras.src.callbacks.history.History at 0x7f2af3fda410>
在微调完成后,你可以再次使用相同的generate()函数生成文本。这一次,生成的文本将更接近Reddit的写作风格,生成的长度将接近于我们在训练集中的预设长度。
start = time.time()
output = gpt2_lm.generate("我喜欢篮球", max_length=200)
print("\nGPT-2输出:")
print(output)
end = time.time()
print(f"总耗时: {end - start:.2f}s")
GPT-2输出:
我喜欢篮球。它有史上最伟大的投篮和最好的投篮。我需要多练习几次,争取一些练习时间。
今天我有机会在一个离我学校非常近的城市参加一个比赛,所以我很兴奋地想看看会怎么样。我刚才和几个其他人一起玩过,所以我想我会去和他们一起打几场。
经过几场比赛,我对自己非常自信,感到相当有信心。我刚获得了这个机会,需要一些练习时间。
所以我去到
总时间已过:21.13秒
在KerasNLP中,我们提供了一些采样方法,例如对比搜索、Top-K和束搜索。默认情况下,我们的GPT2CausalLM
使用Top-k搜索,但您可以选择自己的采样方法。
就像优化器和激活函数一样,有两种方法可以指定自定义采样器:
keras_nlp.samplers.Sampler
实例,通过这种方式可以使用自定义配置。# 使用字符串标识符。
gpt2_lm.compile(sampler="top_k")
output = gpt2_lm.generate("我喜欢篮球", max_length=200)
print("\nGPT-2输出:")
print(output)
# 使用`Sampler`实例。`GreedySampler`往往会重复自身,
greedy_sampler = keras_nlp.samplers.GreedySampler()
gpt2_lm.compile(sampler=greedy_sampler)
output = gpt2_lm.generate("我喜欢篮球", max_length=200)
print("\nGPT-2输出:")
print(output)
GPT-2输出:
我喜欢篮球,这是一个相当不错的。
首先,我的妻子非常优秀,她是一位非常优秀的篮球运动员,真的非常擅长打篮球。
她有一个叫篮球的惊人游戏,这是一款非常有趣的游戏。
我坐在沙发上玩。 我坐在那里,看沙发上的比赛。 我妻子正在玩她的手机。 她正在和许多人在手机上玩。
我的妻子坐在那里看篮球。 她坐在那里看
GPT-2输出:
我喜欢篮球,但我不喜欢打它。
所以我在当地的高中打篮球,和我的朋友们一起打。
我和我的朋友们一起打,我和我的兄弟一起打,他和他的兄弟一起打篮球。
所以我和我的兄弟一起打,他和他兄弟的兄弟一起打。
所以我和我的兄弟一起打,他和他兄弟的兄弟一起打。
所以我和我的兄弟一起打,他和他兄弟的兄弟一起打。
所以我和我的兄弟一起打,他和他兄弟的兄弟一起打。
所以我和我的兄弟一起打,他和他兄弟的兄弟一起打。
所以我和我的兄弟一起打,他和他兄弟
有关KerasNLP Sampler
类的更多详情,您可以查看代码
这里。
我们还可以在非英语数据集上微调GPT2。对于了解中文的读者,这部分说明如何在中文诗歌数据集上微调GPT2,以教我们的模型成为一位诗人!
由于GPT2使用字节对编码,并且原始预训练数据集中包含一些汉字,我们可以使用原始词汇在中文数据集上微调。
!# 加载中文诗歌数据集。
!git clone https://github.com/chinese-poetry/chinese-poetry.git
克隆到'chinese-poetry'...
从json文件加载文本。我们只使用《全唐诗》作为演示。
import os
import json
poem_collection = []
for file in os.listdir("chinese-poetry/全唐诗"):
if ".json" not in file or "poet" not in file:
continue
full_filename = "%s/%s" % ("chinese-poetry/全唐诗", file)
with open(full_filename, "r") as f:
content = json.load(f)
poem_collection.extend(content)
paragraphs = ["".join(data["paragraphs"]) for data in poem_collection]
让我们看看示例数据。
print(paragraphs[0])
毋謂支山險,此山能幾何。崎嶔十年夢,知歷幾蹉跎。
类似于Reddit的例子,我们转换为TF数据集,并仅使用部分数据进行训练。
train_ds = (
tf.data.Dataset.from_tensor_slices(paragraphs)
.batch(16)
.cache()
.prefetch(tf.data.AUTOTUNE)
)
# 运行整个数据集需要很长时间,只取 `500` 并运行 1
# 纪元用于演示目的。
train_ds = train_ds.take(500)
num_epochs = 1
learning_rate = keras.optimizers.schedules.PolynomialDecay(
5e-4,
decay_steps=train_ds.cardinality() * num_epochs,
end_learning_rate=0.0,
)
loss = keras.losses.SparseCategoricalCrossentropy(from_logits=True)
gpt2_lm.compile(
optimizer=keras.optimizers.Adam(learning_rate),
loss=loss,
weighted_metrics=["accuracy"],
)
gpt2_lm.fit(train_ds, epochs=num_epochs)
500/500 ━━━━━━━━━━━━━━━━━━━━ 49s 71ms/step - accuracy: 0.2357 - loss: 2.8196
<keras.src.callbacks.history.History at 0x7f2b2c192bc0>
让我们检查结果!
output = gpt2_lm.generate("昨夜雨疏风骤", max_length=200)
print(output) # 输出生成的文本
昨夜雨疏风骤,爲臨江山院短靜。石淡山陵長爲羣,臨石山非處臨羣。美陪河埃聲爲羣,漏漏漏邊陵塘
不错😀