使用 TensorFlow/Keras 在 Python 中生成 5 行 GPT 样式的文本

变形金刚尽管于 2017 年发布,但在过去几年才开始获得显着的吸引力。 随着技术通过 HuggingFace、NLP 和 大型语言模型 (LLM) 变得比以往任何时候都更容易获得。

然而——即使在他们周围的所有炒作和 许多 面向理论的指南,在线自定义实现并不多,而且资源不像其他一些网络类型那样容易获得,这些网络类型已经存在了更长的时间。 虽然您可以通过使用 HuggingFace 的预构建 Transformer(另一个指南的主题)来简化您的工作周期,但您可以访问 感觉 它是如何通过自己构建的,然后通过库将其抽象出来的。 我们将在这里专注于构建,而不是理论和优化。

在本指南中,我们将构建一个 自回归语言模型生成文本. 我们将专注于加载数据、拆分、矢量化、构建模型、编写自定义回调和训练/推理的实用和简约/简洁方面。 这些任务中的每一项都可以拆分为更详细的指南,因此我们将实现作为通用任务,根据您自己的数据集为自定义和优化留出空间。

法学硕士和 GPT-Fyodor 的类型

虽然分类可以变得更加复杂——你可以 宽广地 将基于 Transformer 的语言模型分为三类:

  • 基于编码器的模型 – 阿尔伯特、伯特、迪斯蒂尔伯特、罗伯塔
  • 基于解码器 – GPT、GPT-2、GPT-3、TransformerXL
  • Seq2Seq 模型 – BART、mBART、T5

基于编码器 模型仅在其架构中使用 Transformer 编码器(通常是堆叠的),并且非常适合理解句子(分类、命名实体识别、问答)。

基于解码器 模型仅在其架构中使用 Transformer 解码器(通常也是堆叠的),并且非常适合未来的预测,这使得它们适用于文本生成。

序列2 模型结合了编码器和解码器,擅长文本生成、摘要和最重要的翻译。

GPT 系列模型在过去几年中获得了很大的关注,它是基于解码器的转换器模型,非常擅长生成类似人类的文本,对大量数据集进行训练,并作为新的一代的开始种子。 例如:

generate_text('the truth ultimately is')

它在引擎盖下将此提示输入到类似 GPT 的模型中,并产生:

'the truth ultimately is really a joy in history, this state of life through which is almost invisible, superfluous  teleological...'

事实上,这是指南末尾的一个小剧透! 另一个小剧透是产生该文本的架构:

inputs = layers.Input(shape=(maxlen,))
embedding_layer = keras_nlp.layers.TokenAndPositionEmbedding(vocab_size, maxlen, embed_dim)(inputs)
transformer_block = keras_nlp.layers.TransformerDecoder(embed_dim, num_heads)(embedding_layer)
outputs = layers.Dense(vocab_size, activation='softmax')(transformer_block)
    
model = keras.Model(inputs=inputs, outputs=outputs)

只需 5 行代码就可以构建一个仅解码器的变压器模型——模拟一个小型 GPT。 由于我们将在 Fyodor Dostoyevsky 的小说中训练模型(你可以用其他任何东西替换它,从 Wikipedia 到 Reddit 评论)——我们暂时将模型称为 GPT-费多尔.

Keras自然语言处理

5 行 GPT-Fyodor 的诀窍在于 Keras自然语言处理,由 Keras 官方团队开发,作为 Keras 的横向扩展,以真正的 Keras 方式,旨在通过新层(编码器、解码器、令牌嵌入、位置嵌入、指标、标记器等)。

KerasNLP 不是模型动物园. 它是 Keras 的一部分(作为一个单独的包),它降低了 NLP 模型开发的准入门槛,就像它降低了使用主包进行一般深度学习开发的准入门槛一样。

请注意: 在撰写本文时,KerasNLP 仍在生产中,并且处于早期阶段。 在未来的版本中可能会出现细微的差异。 文章正在使用版本 0.3.0.

为了能够使用 KerasNLP,您必须通过以下方式安装它 pip:

$ pip install keras_nlp

您可以使用以下方法验证版本:

keras_nlp.__version__

使用 Keras 实现 GPT 样式的模型

让我们从导入我们将要使用的库开始——TensorFlow、Keras、KerasNLP 和 NumPy:

import tensorflow as tf
from tensorflow import keras
import keras_nlp
import numpy as np

加载数据中

让我们加载一些陀思妥耶夫斯基的小说——一个模型太短而无法适应,从早期阶段开始就没有一点过度拟合。 我们将优雅地使用来自 古登堡计划,由于使用此类数据的简单性:

crime_and_punishment_url = 'https://www.gutenberg.org/files/2554/2554-0.txt'
brothers_of_karamazov_url = 'https://www.gutenberg.org/files/28054/28054-0.txt'
the_idiot_url = 'https://www.gutenberg.org/files/2638/2638-0.txt'
the_possessed_url = 'https://www.gutenberg.org/files/8117/8117-0.txt'

paths = [crime_and_punishment_url, brothers_of_karamazov_url, the_idiot_url, the_possessed_url]
names = ['Crime and Punishment', 'Brothers of Karamazov', 'The Idiot', 'The Possessed']
texts = ''
for index, path in enumerate(paths):
    filepath = keras.utils.get_file(f'{names[index]}.txt', origin=path)
    text = ''
    with open(filepath, encoding='utf-8') as f:
        text = f.read()
        
        
        
        texts += text[10000:]

我们只是下载了所有文件,浏览它们并将它们一个接一个地连接起来。 这包括使用的语言的一些多样性,同时仍然保持它明显的 Fyodor! 对于每个文件,我们跳过了前 10k 个字符,这大约是序言和古腾堡介绍的平均长度,因此每次迭代我们都保留了大部分完整的书体。 让我们看一下随机的 500 个字符 texts 现在字符串:


texts[25000:25500]
'nd that was whynI addressed you at once. For in unfolding to you the story of my life, Indo not wish to make myself a laughing-stock before these idle listeners,nwho indeed know all about it already, but I am looking for a mannof feeling and education. Know then that my wife was educated in anhigh-class school for the daughters of noblemen, and on leaving shendanced the shawl dance before the governor and other personages fornwhich she was presented with a gold medal and a certificate of merit.n'

在进行任何其他处理之前,让我们将字符串分成句子:

text_list = texts.split('.')
len(text_list) 

我们有 69k 个句子。 当您更换 n 带空格的字符并计算单词:

len(texts.replace('n', ' ').split(' ')) 

请注意: 您通常希望数据集中至少有一百万个单词,理想情况下,远不止于此。 我们正在处理几兆字节的数据(约 5MB),而语言模型更常见的是在数十千兆字节的文本上进行训练。 这自然会使文本输入过拟合变得非常容易并且难以泛化(没有过拟合的高困惑度,或者有很多过拟合的低困惑度)。 对结果持保留态度。

尽管如此,让我们把它们分成一个 训练, test验证 放。 首先,让我们删除空字符串并打乱句子:


text_list = list(filter(None, text_list))

import random
random.shuffle(text_list)

然后,我们将进行 70/15/15 拆分:

length = len(text_list)
text_train = text_list[:int(0.7*length)]
text_test = text_list[int(0.7*length):int(0.85*length)]
text_valid = text_list[int(0.85*length):]

这是执行训练-测试-验证拆分的一种简单但有效的方法。 让我们来看看 text_train:

[' It was a dull morning, but the snow had ceased',
 'nn"Pierre, you who know so much of what goes on here, can you really havenknown nothing of this business and have heard nothing about it?"nn"What? What a set! So it's not enough to be a child in your old age,nyou must be a spiteful child too! Varvara Petrovna, did you hear what hensaid?"nnThere was a general outcry; but then suddenly an incident took placenwhich no one could have anticipated', ...

标准化和矢量化的时间!

文本矢量化

网络不理解文字——它们理解数字。 我们要对这些词进行标记:

...
sequence = ['I', 'am', 'Wall-E']
sequence = tokenize(sequence)
print(sequence) # [4, 26, 472]
...

此外,由于句子的长度不同 - 通常在左侧或右侧添加填充以确保输入的句子具有相同的形状。假设我们最长的句子是 5 个单词(标记)长。 在这种情况下,Wall-E 语句将被两个零填充,因此我们确保相同的输入形状:

sequence = pad_sequence(sequence)
print(sequence) # [4, 26, 472, 0, 0]

传统上,这是使用 TensorFlow 完成的 Tokenizer 和凯拉斯的 pad_sequences() 方法——然而,一个更方便的层, TextVectorization, 可以使用,它标记化 填充您的输入,允许您提取词汇及其大小,而无需预先知道词汇!

查看我们的 Git 学习实践指南,其中包含最佳实践、行业认可的标准以及随附的备忘单。 停止谷歌搜索 Git 命令,实际上 学习 它!

让我们适应并适应 TextVectorization 层:

from tensorflow.keras.layers import TextVectorization

def custom_standardization(input_string):
    sentence = tf.strings.lower(input_string)
    sentence = tf.strings.regex_replace(sentence, "n", " ")
    return sentence

maxlen = 50



vectorize_layer = TextVectorization(
    standardize = custom_standardization,
    output_mode="int",
    output_sequence_length=maxlen + 1,
)

vectorize_layer.adapt(text_list)
vocab = vectorize_layer.get_vocabulary()

custom_standardization() 方法可以得到比这更长的时间。 我们只是将所有输入小写并替换 n " ". 这是您真正可以对文本进行大部分预处理的地方 - 并通过可选的选项将其提供给矢量化层 standardize 争论。 一旦您 adapt() 文本层(NumPy 数组或文本列表)——您可以从那里获取词汇表及其大小:

vocab_size = len(vocab)
vocab_size 

最后,为了去分词,我们将创建一个 index_lookup 字典:

index_lookup = dict(zip(range(len(vocab)), vocab))    
index_lookup[5] 

它映射所有标记 ([1, 2, 3, 4, ...]) 到词汇表中的单词 (['a', 'the', 'i', ...])。 通过传入一个键(令牌索引),我们可以轻松地取回单词。 您现在可以运行 vectorize_layer() 在任何输入上并观察向量化的句子:

vectorize_layer(['hello world!'])

结果是:

<tf.Tensor: shape=(1, 51), dtype=int64, numpy=
array([[   1, 7509,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0]], dtype=int64)>

你好有索引 1 而世界有索引 7509! 剩下的就是填充 maxlen 我们已经计算过了。

我们有办法对文本进行矢量化——现在,让我们从 text_train, text_testtext_valid,使用我们的向量化层作为单词和向量之间的转换媒介,可以输入 GPT-Fyodor。

数据集创建

我们将创建一个 tf.data.Dataset 对于我们的每个集合,使用 from_tensor_slices() 并提供张量切片(句子)的列表:

batch_size = 64

train_dataset = tf.data.Dataset.from_tensor_slices(text_train)
train_dataset = train_dataset.shuffle(buffer_size=256)
train_dataset = train_dataset.batch(batch_size)

test_dataset = tf.data.Dataset.from_tensor_slices(text_test)
test_dataset = test_dataset.shuffle(buffer_size=256)
test_dataset = test_dataset.batch(batch_size)

valid_dataset = tf.data.Dataset.from_tensor_slices(text_valid)
valid_dataset = valid_dataset.shuffle(buffer_size=256)
valid_dataset = valid_dataset.batch(batch_size)

一旦创建和洗牌(再次,为了更好的衡量) - 我们可以应用预处理(矢量化和序列分割)功能:

def preprocess_text(text):
    text = tf.expand_dims(text, -1)
    tokenized_sentences = vectorize_layer(text)
    x = tokenized_sentences[:, :-1]
    y = tokenized_sentences[:, 1:]
    return x, y


train_dataset = train_dataset.map(preprocess_text)
train_dataset = train_dataset.prefetch(tf.data.AUTOTUNE)

test_dataset = test_dataset.map(preprocess_text)
test_dataset = test_dataset.prefetch(tf.data.AUTOTUNE)

valid_dataset = valid_dataset.map(preprocess_text)
valid_dataset = valid_dataset.prefetch(tf.data.AUTOTUNE)

preprocess_text() 函数简单地扩展最后一个维度,使用我们的向量化文本 vectorize_layer 并创建输入和目标,由单个标记偏移。 该模型将使用 [0..n] 推断 n+1,为每个单词产生一个预测,占之前的所有单词。 让我们看一下任何数据集中的单个条目:

for entry in train_dataset.take(1):
    print(entry)

调查返回的输入和目标,分批 64 个(每个长度为 30 个),我们可以清楚地看到它们是如何偏移 XNUMX 的:

(<tf.Tensor: shape=(64, 50), dtype=int64, numpy=
array([[17018,   851,     2, ...,     0,     0,     0],
       [  330,    74,     4, ...,     0,     0,     0],
       [   68,   752, 30273, ...,     0,     0,     0],
       ...,
       [    7,    73,  2004, ...,     0,     0,     0],
       [   44,    42,    67, ...,     0,     0,     0],
       [  195,   252,   102, ...,     0,     0,     0]], dtype=int64)>, <tf.Tensor: shape=(64, 50), dtype=int64, numpy=
array([[  851,     2,  8289, ...,     0,     0,     0],
       [   74,     4,    34, ...,     0,     0,     0],
       [  752, 30273,  7514, ...,     0,     0,     0],
       ...,
       [   73,  2004,    31, ...,     0,     0,     0],
       [   42,    67,    76, ...,     0,     0,     0],
       [  252,   102,  8596, ...,     0,     0,     0]], dtype=int64)>)

最后——是时候建立模型了!

模型定义

我们将在这里使用 KerasNLP 层。 经过一个 Input,我们将通过 a 对输入进行编码 TokenAndPositionEmbedding 层,传入我们的 vocab_size, maxlenembed_dim。 相同 embed_dim 该层输出和输入到 TransformerDecoder保留在解码器中. 在撰写本文时,解码器自动维护输入维度,并且不允许您将其投影到不同的输出中,但它允许您通过 intermediate_dim 论据。

对于潜在表示,我们将嵌入维度乘以 XNUMX,但您可以保持不变或使用与嵌入维度分离的数字:

embed_dim = 128
num_heads = 4

def create_model():
    inputs = keras.layers.Input(shape=(maxlen,), dtype=tf.int32)
    embedding_layer = keras_nlp.layers.TokenAndPositionEmbedding(vocab_size, maxlen, embed_dim)(inputs)
    decoder = keras_nlp.layers.TransformerDecoder(intermediate_dim=embed_dim, 
                                                            num_heads=num_heads, 
                                                            dropout=0.5)(embedding_layer)
    
    outputs = keras.layers.Dense(vocab_size, activation='softmax')(decoder)
    
    model = keras.Model(inputs=inputs, outputs=outputs)
    
    model.compile(
        optimizer="adam", 
        loss='sparse_categorical_crossentropy',
        metrics=[keras_nlp.metrics.Perplexity(), 'accuracy']
    )
    return model

model = create_model()
model.summary()

在解码器之上,我们有一个 Dense 层选择序列中的下一个单词,带有 softmax 激活(产生每个下一个标记的概率分布)。 让我们看一下模型的摘要:

Model: "model_5"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_6 (InputLayer)        [(None, 30)]              0         
                                                                 
 token_and_position_embeddin  (None, 30, 128)          6365824   
 g_5 (TokenAndPositionEmbedd                                     
 ing)                                                            
                                                                 
 transformer_decoder_5 (Tran  (None, 30, 128)          132480    
 sformerDecoder)                                                 
                                                                 
 dense_5 (Dense)             (None, 30, 49703)         6411687   
                                                                 
=================================================================
Total params: 13,234,315
Trainable params: 13,234,315
Non-trainable params: 0
_________________________________________________________________

GPT-2 堆叠了许多解码器——GPT-2 Small 有 12 个堆叠解码器(117M 参数),而 GPT-2 Extra Large 有 48 个堆叠解码器(1.5B 参数)。 我们的具有 13M 参数的单解码器模型应该可以很好地用于教育目的。 借助 LLM – 扩大规模已被证明是一种非常好的策略,而 Transformer 允许良好的规模扩大,使得训练超大型模型变得可行。

GPT-3 有一个 “微薄” 175B 参数。 Google Brain 的团队训练了一个 1.6T 参数模型来执行稀疏性研究,同时将计算保持在与更小模型相同的水平。

事实上,如果我们将解码器的数量从 1 个增加到 3 个:

def create_model():
    inputs = keras.layers.Input(shape=(maxlen,), dtype=tf.int32)
    x = keras_nlp.layers.TokenAndPositionEmbedding(vocab_size, maxlen, embed_dim)(inputs)
    for i in range(4):
        x = keras_nlp.layers.TransformerDecoder(intermediate_dim=embed_dim*2, num_heads=num_heads,                                                             dropout=0.5)(x)
    do = keras.layers.Dropout(0.4)(x)
    outputs = keras.layers.Dense(vocab_size, activation='softmax')(do)
    
    model = keras.Model(inputs=inputs, outputs=outputs)

我们的参数计数将增加 400k:

Total params: 13,631,755
Trainable params: 13,631,755
Non-trainable params: 0

我们网络中的大部分参数来自 TokenAndPositionEmbeddingDense 层!

尝试解码器的不同深度——从 1 到机器可以处理的所有深度,并记录结果。 无论如何——我们几乎已经准备好训练模型了! 让我们创建一个自定义回调,它将在每个 epoch 上生成一个文本样本,这样我们就可以看到模型如何通过训练学习形成句子。

自定义回调

class TextSampler(keras.callbacks.Callback):
    def __init__(self, start_prompt, max_tokens):
        self.start_prompt = start_prompt
        self.max_tokens = max_tokens
        
    
    
    def sample_token(self, logits):
        logits, indices = tf.math.top_k(logits, k=5, sorted=True)
        indices = np.asarray(indices).astype("int32")
        preds = keras.activations.softmax(tf.expand_dims(logits, 0))[0]
        preds = np.asarray(preds).astype("float32")
        return np.random.choice(indices, p=preds)

    def on_epoch_end(self, epoch, logs=None):
        decoded_sample = self.start_prompt
        
        for i in range(self.max_tokens-1):
            tokenized_prompt = vectorize_layer([decoded_sample])[:, :-1]
            predictions = self.model.predict([tokenized_prompt], verbose=0)
            
            
            
            
            sample_index = len(decoded_sample.strip().split())-1
            
            sampled_token = self.sample_token(predictions[0][sample_index])
            sampled_token = index_lookup[sampled_token]
            decoded_sample += " " + sampled_token
            
        print(f"nSample text:n{decoded_sample}...n")


random_sentence = ' '.join(random.choice(text_valid).replace('n', ' ').split(' ')[:4])
sampler = TextSampler(random_sentence, 30)
reducelr = keras.callbacks.ReduceLROnPlateau(patience=10, monitor='val_loss')

训练模型

最后,是时候训练了! 让我们加入我们的 train_datasetvalidation_dataset 回调到位:

model = create_model()
history = model.fit(train_dataset, 
                    validation_data=valid_dataset,
                    epochs=10, 
                    callbacks=[sampler, reducelr])

采样器选择了一个以结尾引号和开头引号开头的不幸句子,但它在训练时仍然产生了有趣的结果:

# Epoch training
Epoch 1/10
658/658 [==============================] - ETA: 0s - loss: 2.7480 - perplexity: 15.6119 - accuracy: 0.6711
# on_epoch_end() sample generation
Sample text:
”  “What do you had not been i had been the same man was not be the same eyes to been a whole man and he did a whole man to the own...
# Validation
658/658 [==============================] - 158s 236ms/step - loss: 2.7480 - perplexity: 15.6119 - accuracy: 0.6711 - val_loss: 2.2130 - val_perplexity: 9.1434 - val_accuracy: 0.6864 - lr: 0.0010
...
Sample text:
”  “What do you know it is it all this very much as i should not have a great impression  in the room to be  able of it in my heart...

658/658 [==============================] - 149s 227ms/step - loss: 1.7753 - perplexity: 5.9019 - accuracy: 0.7183 - val_loss: 2.0039 - val_perplexity: 7.4178 - val_accuracy: 0.7057 - lr: 0.0010

它始于:

“你以前不是我以前是什么”......

这真的没有多大意义。 在十个短时期结束时,它产生了以下内容:

“你什么意思,那当然是男人中最普通的男人”……

虽然第二句话仍然没有太多意义——它比第一句话更有意义。 对更多数据进行更长时间的训练(使用更复杂的预处理步骤)会产生更好的结果。 我们只在 10 个具有高 dropout 的 epoch 上对其进行了训练,以应对较小的数据集大小。 如果让它训练更长的时间,它会产生非常类似于 Fyodor 的文本,因为它会记住大部分内容。

请注意: 由于输出相当冗长,您可以调整 verbose 参数,同时拟合模型以减少屏幕上的文本量。

模型推理

为了执行推理,我们需要复制 TextSampler – 一种接受种子和 response_length (max_tokens)。 我们将使用与采样器中相同的方法:

def sample_token(logits):
        logits, indices = tf.math.top_k(logits, k=5, sorted=True)
        indices = np.asarray(indices).astype("int32")
        preds = keras.activations.softmax(tf.expand_dims(logits, 0))[0]
        preds = np.asarray(preds).astype("float32")
        return np.random.choice(indices, p=preds)

def generate_text(prompt, response_length=20):
    decoded_sample = prompt
    for i in range(response_length-1):
        tokenized_prompt = vectorize_layer([decoded_sample])[:, :-1]
        predictions = model.predict([tokenized_prompt], verbose=0)
        sample_index = len(decoded_sample.strip().split())-1

        sampled_token = sample_token(predictions[0][sample_index])
        sampled_token = index_lookup[sampled_token]
        decoded_sample += " " + sampled_token
    return decoded_sample

现在,您可以在新样本上运行该方法:

generate_text('the truth ultimately is')


generate_text('the truth ultimately is')

改善结果?

那么,如何提高结果呢? 您可以做一些非常可行的事情:

  • 数据清理(更细致地清理输入数据,我们只是从开头修剪了一个近似数字并删除了换行符)
  • 获取更多数据(我们只处理了几兆字节的文本数据)
  • 将模型与数据一起缩放(堆叠解码器并不难!)

结论

虽然预处理流水线极简并且可以改进——本指南中概述的流水线生成了一个不错的 GPT 样式模型,使用 Keras 构建一个自定义的仅解码器转换器只需要 5 行代码!

Transformer 很流行并且广泛适用于通用序列建模(许多东西可以表示为序列)。 到目前为止,进入的主要障碍是繁琐的实现,但有了 KerasNLP——深度学习从业者可以利用这些实现快速轻松地构建模型。

时间戳记:

更多来自 堆栈滥用