作者: Apoorv Nandan
创建日期: 2020/05/29
最后修改: 2020/05/29
描述: 实现一个微型版本的GPT并训练它生成文本。
此示例演示如何使用微型版本的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
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
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
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()` 函数。
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">
</div>
<div class="k-default-codeblock">