代码示例 / 生成式深度学习 / 使用微型GPT生成文本

使用微型GPT生成文本

作者: Apoorv Nandan
创建日期: 2020/05/29
最后修改: 2020/05/29
描述: 实现一个微型版本的GPT并训练它生成文本。

在Colab中查看 GitHub源文件


介绍

此示例演示如何使用微型版本的GPT模型实现自回归语言模型。 该模型由一个具有因果掩码的单个Transformer块构成 在其注意力层中。 我们使用IMDB情感分类数据集的文本进行训练 并为给定的提示生成新的电影评论。 使用此脚本时,请确保数据集至少包含 100万个单词。

此示例应与tf-nightly>=2.3.0-dev20200531或 TensorFlow 2.3或更高版本一起运行。

参考文献:


设置

# 我们将后端设置为TensorFlow。该代码适用于
# `tensorflow`和`torch`。它不适用于JAX
# 由于`jax.numpy.tile`在jit范围内的行为
# (在`causal_attention_mask()`中使用:`tile`在JAX中
# 不支持动态`reps`参数。
# 您可以通过将代码包裹在
# `causal_attention_mask`函数内部的装饰器中使代码在JAX中工作
# 以防止jit编译:
# `with jax.ensure_compile_time_eval():`。
import os

os.environ["KERAS_BACKEND"] = "tensorflow"

import keras
from keras import layers
from keras import ops
from keras.layers import TextVectorization
import numpy as np
import os
import string
import random
import tensorflow
import tensorflow.data as tf_data
import tensorflow.strings as tf_strings

将Transformer块实现为层

def causal_attention_mask(batch_size, n_dest, n_src, dtype):
    """
    在自注意力中掩盖点积矩阵的上半部分。
    这防止了未来标记对当前标记的信息流动。
    在下三角中,1从右下角开始计数。
    """
    i = ops.arange(n_dest)[:, None]
    j = ops.arange(n_src)
    m = i >= j - n_src + n_dest
    mask = ops.cast(m, dtype)
    mask = ops.reshape(mask, [1, n_dest, n_src])
    mult = ops.concatenate(
        [ops.expand_dims(batch_size, -1), ops.convert_to_tensor([1, 1])], 0
    )
    return ops.tile(mask, mult)


class TransformerBlock(layers.Layer):
    def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
        super().__init__()
        self.att = layers.MultiHeadAttention(num_heads, embed_dim)
        self.ffn = keras.Sequential(
            [
                layers.Dense(ff_dim, activation="relu"),
                layers.Dense(embed_dim),
            ]
        )
        self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)
        self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)
        self.dropout1 = layers.Dropout(rate)
        self.dropout2 = layers.Dropout(rate)

    def call(self, inputs):
        input_shape = ops.shape(inputs)
        batch_size = input_shape[0]
        seq_len = input_shape[1]
        causal_mask = causal_attention_mask(batch_size, seq_len, seq_len, "bool")
        attention_output = self.att(inputs, inputs, attention_mask=causal_mask)
        attention_output = self.dropout1(attention_output)
        out1 = self.layernorm1(inputs + attention_output)
        ffn_output = self.ffn(out1)
        ffn_output = self.dropout2(ffn_output)
        return self.layernorm2(out1 + ffn_output)

实现嵌入层

创建两个单独的嵌入层:一个用于标记,一个用于标记索引 (位置)。

class TokenAndPositionEmbedding(layers.Layer):
    def __init__(self, maxlen, vocab_size, embed_dim):
        super().__init__()
        self.token_emb = layers.Embedding(input_dim=vocab_size, output_dim=embed_dim)
        self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=embed_dim)

    def call(self, x):
        maxlen = ops.shape(x)[-1]
        positions = ops.arange(0, maxlen, 1)
        positions = self.pos_emb(positions)
        x = self.token_emb(x)
        return x + positions

实现微型GPT模型

vocab_size = 20000  # 仅考虑前20k个单词
maxlen = 80  # 最大序列长度
embed_dim = 256  # 每个标记的嵌入大小
num_heads = 2  # 注意力头的数量
feed_forward_dim = 256  # 变换器中前馈网络的隐藏层大小


def create_model():
    inputs = layers.Input(shape=(maxlen,), dtype="int32")
    embedding_layer = TokenAndPositionEmbedding(maxlen, vocab_size, embed_dim)
    x = embedding_layer(inputs)
    transformer_block = TransformerBlock(embed_dim, num_heads, feed_forward_dim)
    x = transformer_block(x)
    outputs = layers.Dense(vocab_size)(x)
    model = keras.Model(inputs=inputs, outputs=[outputs, x])
    loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)
    model.compile(
        "adam",
        loss=[loss_fn, None],
    )  # 不根据变换器块的词嵌入进行损失和优化
    return model

准备数据以进行单词级语言建模

下载IMDB数据集并将训练集和验证集结合用于文本生成任务。

!curl -O https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
!tar -xf aclImdb_v1.tar.gz
batch_size = 128

# 数据集包含每个评论在一个单独的文本文件中
# 文本文件位于四个不同的文件夹中
# 创建所有文件的列表
filenames = []
directories = [
    "aclImdb/train/pos",
    "aclImdb/train/neg",
    "aclImdb/test/pos",
    "aclImdb/test/neg",
]
for dir in directories:
    for f in os.listdir(dir):
        filenames.append(os.path.join(dir, f))

print(f"{len(filenames)} files")

# 从文本文件创建数据集
random.shuffle(filenames)
text_ds = tf_data.TextLineDataset(filenames)
text_ds = text_ds.shuffle(buffer_size=256)
text_ds = text_ds.batch(batch_size)


def custom_standardization(input_string):
    """移除html换行标签并处理标点符号"""
    lowercased = tf_strings.lower(input_string)
    stripped_html = tf_strings.regex_replace(lowercased, "<br />", " ")
    return tf_strings.regex_replace(stripped_html, f"([{string.punctuation}])", r" \1")


# 创建一个向量化层,并对文本进行适应
vectorize_layer = TextVectorization(
    standardize=custom_standardization,
    max_tokens=vocab_size - 1,
    output_mode="int",
    output_sequence_length=maxlen + 1,
)
vectorize_layer.adapt(text_ds)
vocab = vectorize_layer.get_vocabulary()  # 通过标记索引获取单词


def prepare_lm_inputs_labels(text):
    """
    将词序列向右移动1个位置,使得位置(i)的目标为
    位置(i+1)的词。模型将使用位置(i)及之前的所有词
    来预测下一个词。
    """
    text = tensorflow.expand_dims(text, -1)
    tokenized_sentences = vectorize_layer(text)
    x = tokenized_sentences[:, :-1]
    y = tokenized_sentences[:, 1:]
    return x, y


text_ds = text_ds.map(prepare_lm_inputs_labels, num_parallel_calls=tf_data.AUTOTUNE)
text_ds = text_ds.prefetch(tf_data.AUTOTUNE)
  % 总计    % 已接收 % 传输  平均速度   时间    时间     时间  当前
                                 下载  上传   总计   已花费   剩余  速度
100 80.2M  100 80.2M    0     0  7926k      0  0:00:10  0:00:10 --:--:-- 7661k

50000 files

实现Keras回调以生成文本

class TextGenerator(keras.callbacks.Callback):
    """一个回调函数,用于从训练好的模型生成文本。
    1. 向模型输入一些起始提示
    2. 预测下一个标记的概率
    3. 从中采样下一个标记,并将其添加到下一个输入中

    参数:
        max_tokens: 整数,提示后生成的标记数量。
        start_tokens: 整数列表,起始提示的标记索引。
        index_to_word: 字符串列表,通过 TextVectorization 层获得。
        top_k: 整数,从 `top_k` 个标记预测中采样。
        print_every: 整数,每经过这么多轮输出一次。
    """

    def __init__(
        self, max_tokens, start_tokens, index_to_word, top_k=10, print_every=1
    ):
        self.max_tokens = max_tokens
        self.start_tokens = start_tokens
        self.index_to_word = index_to_word
        self.print_every = print_every
        self.k = top_k

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

    def detokenize(self, number):
        return self.index_to_word[number]

    def on_epoch_end(self, epoch, logs=None):
        start_tokens = [_ for _ in self.start_tokens]
        if (epoch + 1) % self.print_every != 0:
            return
        num_tokens_generated = 0
        tokens_generated = []
        while num_tokens_generated <= self.max_tokens:
            pad_len = maxlen - len(start_tokens)
            sample_index = len(start_tokens) - 1
            if pad_len < 0:
                x = start_tokens[:maxlen]
                sample_index = maxlen - 1
            elif pad_len > 0:
                x = start_tokens + [0] * pad_len
            else:
                x = start_tokens
            x = np.array([x])
            y, _ = self.model.predict(x, verbose=0)
            sample_token = self.sample_from(y[0][sample_index])
            tokens_generated.append(sample_token)
            start_tokens.append(sample_token)
            num_tokens_generated = len(tokens_generated)
        txt = " ".join(
            [self.detokenize(_) for _ in self.start_tokens + tokens_generated]
        )
        print(f"生成的文本:\n{txt}\n")


# 词标记化起始提示
word_to_index = {}
for index, word in enumerate(vocab):
    word_to_index[word] = index

start_prompt = "this movie is"
start_tokens = [word_to_index.get(_, 1) for _ in start_prompt.split()]
num_tokens_generated = 40
text_gen_callback = TextGenerator(num_tokens_generated, start_tokens, vocab)

训练模型

注意:此代码最好在 GPU 上运行。

model = create_model()

model.fit(text_ds, verbose=2, epochs=25, callbacks=[text_gen_callback])
第 1 轮/25

警告:在调用 absl::InitializeLog() 之前的所有日志消息都写入 STDERR
I0000 00:00:1699499022.078758  633491 device_compiler.h:187] 使用 XLA 编译集群!此行在进程生命周期内最多记录一次。
/home/mattdangerw/miniconda3/envs/keras-tensorflow/lib/python3.10/contextlib.py:153: 用户警告:您的输入数据已耗尽;中断训练。请确保您的数据集或生成器可以生成至少 `steps_per_epoch * epochs` 批次。您可能需要在构建数据集时使用 `.repeat()` 函数。
generated text: this movie is so bad that it's hard to take it seriously . i can 't believe that such a great cast is wasted on such a terrible script . the direction is bad and the dialogue is even worse . . . i
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.6267 Epoch 15/25 generated text: this movie is completely forgettable . there 's nothing that makes it stand out in any way . the performances are bland and the plot is predictable . i honestly do not understand why this was made
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.5945 Epoch 16/25 generated text: this movie is simply awful . from start to finish , it 's painful to watch . the editing is choppy and the pacing is non-existent . i wish i had never wasted my time on it
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.5696 Epoch 17/25 generated text: this movie is exemplary of everything that is wrong with modern cinema . it fails on nearly every level and leaves you feeling empty . i would not recommend it to anyone
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.5462 Epoch 18/25 generated text: this movie is an exercise in frustration . the plot is flimsy and the characters are one-dimensional . it is a shame to see such potential wasted on such a lackluster film
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.5261 Epoch 19/25 generated text: this movie is a clear example of how not to make a film . it 's not coherent and leaves too many questions unanswered . i 'm honestly baffled as to how it ever got made
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.5081 Epoch 20/25 generated text: this movie is painfully dull . i can 't stress enough how boring it is . the performances are lifeless and the narrative drags on far too long . avoid it at all costs
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.4924 Epoch 21/25 generated text: this movie is still just as boring as i remember . it 's another one of those films that tries too hard to be profound but ends up being pretentious instead . skip it
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.4786 Epoch 22/25 generated text: this movie is a miserable experience . i could barely sit through it without checking my watch repeatedly . it lacks everything that makes a film enjoyable
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.4694 Epoch 23/25 generated text: this movie is outright terrible . i honestly feel like my time was wasted in watching it . i do not recommend it to anyone , it 's just not worth it
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.4604 Epoch 24/25 generated text: this movie is a disgrace to filmmaking . it is blatantly bad , and there is no redeeming quality to be found anywhere in it . even the most forgiving viewer would have a hard time enjoying it
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.4531 Epoch 25/25 generated text: this movie is honestly one of the worst films i 've ever seen in my life . it should be avoided at all costs . i regret ever watching it
</div>
391/391 - 17秒 - 42毫秒/步 - 损失: 3.6546 第14/25个周期 生成的文本: 这部电影是一个非常有趣的故事,充满动作和浪漫。如果你喜欢动作,故事实在太糟糕了。它没有抓住要点,但你拥有你黑暗的内心。那
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.6147 第 15/25 轮 生成的文本: 这部电影不仅仅是一部恐怖片。老实说,这部电影讲述了一群四处奔波的青少年。但这仍然是一部有趣的电影。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.5769 第 16/25 轮 生成的文本: 这部电影讲的是一个应该是女孩的男孩,在一部不合逻辑的电影中。幽默感并不是要从头到尾观看这部电影。你不能知道。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.5425 第 17/25 轮 生成的文本: 这部电影是我见过的最好的电影之一。我在租这部电影时感到非常惊讶,结果却没有更好,它甚至不好笑,我真的不知道我在想什么。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.5099 第 18/25 轮 生成的文本: 这部电影太糟糕了。我认为它有点被高估了。我看过很多糟糕的电影。我必须说这部电影就是糟糕。我本希望[UNK]。这个[UNK]很好。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 43ms/step - loss: 3.4800 第 19/25 轮 生成的文本: 这部电影是我见过的最佳功夫电影之一。这是一部很棒的电影,音乐也很好。画面真的很酷。动作场面比其他场面多得多。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.4520 第 20/25 轮 生成的文本: 这部电影简直糟糕透顶且愚蠢。我无法理解这部电影。我简直不敢相信人们居然为这部[UNK]花钱。我发誓,我如此尴尬,甚至有几句台词都是。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.4260 第 21/25 轮 生成的文本: 这部电影是我见过的那些电影之一,你必须知道我对这部电影并不满意。我觉得它有趣。故事的开始。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.4014 第 22/25 轮 生成的文本: 这部电影是一位男性生活的故事,是一部非常好的电影,探讨了某种电影。这部电影是你们曾经经历过的最惊人的电影之一。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.3783 第 23/25 轮 生成的文本: 这部电影是个伟大的好东西,即使是我见过的最糟糕的电影!这并不意味着一切都很糟糕,表演和导演都很糟糕。剧本很差,情节和。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.3564 第 24/25 轮 生成的文本: 这部电影是有史以来最好的电影之一。[UNK] [UNK]讲的是主角和一个名叫法伦的贵族;在一个古怪的岛屿上被困,爱上了她逃离的地方。与此同时,逃离的。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.3362 第 25/25 轮 生成的文本: 这部电影非常好。表演,尤其是整部电影——简直是最糟糕的。这部电影有很多值得推荐给任何人的地方。它并不好笑。故事太无聊了!这个。
</div>

<div class="k-default-codeblock">
391/391 - 17s - 42ms/step - loss: 3.3170 ```