使用TensorFlow实现微型GPT模型完成文本生成任务

释放双眼,带上耳机,听听看~!
学习如何使用TensorFlow实现微型的GPT模型完成文本生成任务,包括语言模型训练和数据准备。

前文

本文主要展示了如何实现微型的 GPT 模型完成文本生成任务,该模型只由 1 个 Transformer 块组成。

Data

这部分代码主要用于准备文本数据集进行语言模型训练,这里需要事先下载好 aclImdb 数据,并且解压到当前目录。

首先,定义了一个批量大小 batch_size 和一个存储文件名的列表 filenames,其中包含了要处理的文本文件的路径。接下来,通过随机打乱 filenames 列表的顺序,来增加数据集的随机性。接着使用 TensorFlow 的 TextLineDataset 创建一个文本数据集 text_ds,并通过 shuffle 方法对数据集进行洗牌。

定义了一个自定义的标准化函数 custom_standardization,用于对输入字符串进行标准化处理。函数将字符串转换为小写,并使用正则表达式去除 HTML 标签和标点符号。然后创建了一个 TextVectorization 层,通过自定义函数对文本数据进行处理,对 text_ds 中的文本进行矢量化处理,也就是将文本转换为整数序列。并从数据集中自动构建词汇表。

通过 map 方法将处理后的文本转换为模型的输入和标签,即将每个序列中的一句话去掉最后一个字作为输入,然后将对应的同样一个序列从第二个字开始到最后的序列作为标签。从局部来看也就是前一个字是输入,预测输出后一个字。

最后,使用 prefetch 方法对数据集进行预取操作,以便在模型训练过程中能够高效地加载数据。

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.layers import TextVectorization
import numpy as np
import os
import string
import random

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))

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):
    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):
    text = tf.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)
text_ds = text_ds.prefetch(tf.data.AUTOTUNE)

Miniature GPT

Transformer Block

这里主要是定义了一个 TransformerBlock 类,该类实现了 Transformer 模型中的一个 Transformer Block 。整个 TransformerBlock 类的作用是将输入序列经过自注意力计算和前馈网络变换,得到一个更丰富的表示。

TransformerBlock 类的构造函数 __init__ 接受四个参数:embed_dim 表示嵌入维度,num_heads 表示注意力头数,ff_dim 表示前馈网络的维度,rate 表示 Dropout 的比例。

call 方法中,首先获取输入的形状信息,包括批大小和序列长度。然后调用 causal_attention_mask 函数生成一个注意力掩码,用于遮蔽 Transformer 中的未来信息,确保模型只能看到当前位置以及之前的输入信息。这个掩码是一个二维矩阵,维度为 (seq_len, seq_len)。

接下来,使用 MultiHeadAttentionself.att 对输入进行自注意力计算,并传入注意力掩码。然后应用第一个 Dropout 层 self.dropout1 对注意力输出进行随机失活。将输入和注意力输出相加,并通过 LayerNormalization 层 self.layernorm1 进行归一化处理,得到第一个子层的输出 out1

接着,将第一个子层的输出 out1 传入前馈神经网络 self.ffn 进行非线性变换。再次应用 Dropout 层 self.dropout2 对前馈网络的输出进行随机失活。将第一个子层的输出 out1 和前馈网络的输出相加,并通过 LayerNormalization 层 self.layernorm2 进行归一化处理,得到 Transformer Block 的最终输出。

def causal_attention_mask(batch_size, n_dest, n_src, dtype):
    i = tf.range(n_dest)[:, None]
    j = tf.range(n_src)
    m = i >= j - n_src + n_dest
    mask = tf.cast(m, dtype)
    mask = tf.reshape(mask, [1, n_dest, n_src])
    mult = tf.concat( [tf.expand_dims(batch_size, -1), tf.constant([1, 1], dtype=tf.int32)], 0 )
    return tf.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 = tf.shape(inputs)
        batch_size = input_shape[0]
        seq_len = input_shape[1]
        causal_mask = causal_attention_mask(batch_size, seq_len, seq_len, tf.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)

Token And Position Embedding

这里定义了一个 TokenAndPositionEmbedding 类,用于得到输入序列的 token 和位置信息的嵌入。

TokenAndPositionEmbedding 类的构造函数 __init__ 接受三个参数:maxlen 表示序列的最大长度,vocab_size 表示词汇表的大小,embed_dim 表示嵌入维度。

call 方法中,首先获取输入序列 x 的长度 maxlen。然后使用 tf.range 函数生成一个从 0 到 maxlen-1 的位置向量 positions。接着将位置向量 positions 传入位置嵌入层 self.pos_emb 进行嵌入,得到位置嵌入张量。同时,将输入序列 x 传入标记嵌入层 self.token_emb 进行嵌入,得到 token 的嵌入张量。最后,将 token 嵌入张量和位置嵌入张量相加,得到融合了标记和位置信息的嵌入张量,并将其作为输出返回。

整个 TokenAndPositionEmbedding 类的作用是将输入序列的 token 和位置信息进行嵌入计算,为后续的 Transformer 模型提供丰富的输入表示。在 Transformer 模型中,token 嵌入用于表示每个输入 token 的语义信息,而位置嵌入用于表示每个输入 token 在序列中的位置信息。

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 = tf.shape(x)[-1]
        positions = tf.range(start=0, limit=maxlen, delta=1)
        positions = self.pos_emb(positions)
        x = self.token_emb(x)
        return x + positions
        

Create Model

这里定义了一个 create_model 函数,用于创建一个 Transformer 模型。这个模型使用 Transformer 架构来处理输入序列,并在最后通过全连接层进行分类预测。它能够学习输入序列中的语义和上下文关系,用于生成预测的单词概率分布。

函数中首先创建了一个输入层 inputs,其形状为 (maxlen,),数据类型为 tf.int32,用于接收输入序列。接下来定义了一个 TokenAndPositionEmbedding 层,传入参数 maxlenvocab_sizeembed_dim,用于将输入序列的标记和位置信息进行嵌入。将输入层 inputs 作为输入传递给嵌入层,得到嵌入后的输出张量 x

然后创建了一个 TransformerBlock 层,传入参数 embed_dimnum_headsfeed_forward_dim,用于对嵌入后的序列进行 Transformer 操作。将嵌入后的张量 x 传递给 TransformerBlock 层,得到处理后的输出张量 x

接下来通过一个全连接层 layers.Dense 对输出张量 x 进行预测,输出一个形状为 (vocab_size,) 的张量 outputs,也就是计算出来下一个预测的单词的概率分布。

最后,定义了损失函数 loss_fn 为稀疏分类交叉熵损失函数,并使用 "adam" 优化器进行模型的编译。

vocab_size = 20000  
maxlen = 80  
embed_dim = 256  
num_heads = 2  
feed_forward_dim = 256 

def create_model():
    inputs = layers.Input(shape=(maxlen,), dtype=tf.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 = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
    model.compile( "adam", loss=[loss_fn, None], )
    return model

Text Generator

这里定义了一个 TextGenerator 类,用于在训练过程作为回调函数来生成文本,展示在不同训练 epoch 下面的文本生成效果。

构造函数 __init__ 接收参数 max_tokensstart_tokensindex_to_wordtop_kprint_every,用于配置文本生成的相关参数。

sample_from 方法用于从给定的 logits(对数概率)中进行采样,根据概率分布选择下一个预测的单词 。它首先使用 tf.math.top_k 选择概率最高的前 k 个单词,然后进行 softmax 归一化,得到概率分布。最后,使用 np.random.choice 方法根据概率分布进行采样,选择下一个预测的单词。

on_epoch_end 方法在每个训练周期结束时调用,用于生成文本。它通过循环生成文本的过程,从给定的起始文本开始,逐步生成下一个单词,直到达到指定的最大生成单词数。在每次生成单词后,将其添加到已生成的列表中,并更新起始文本。最后,将生成的文本转换为字符串,并打印输出。

接下来,定义了一个起始提示文本 start_promptthis movie is very good,并根据词汇表和起始提示文本生成了起始标记 start_tokens。然后,创建了一个 TextGenerator 对象 text_gen_callback,传入生成文本所需的参数。

class TextGenerator(keras.callbacks.Callback):
    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 = tf.math.top_k(logits, k=self.k, 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 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)
            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"generated text:n{txt}n")

word_to_index = {}
for index, word in enumerate(vocab):
    word_to_index[word] = index

start_prompt = "this movie is very good"
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)

Train

该部分就是创建了一个文本生成模型,训练 30 个 epoch ,并且调用 text_gen_callback 对象,在每次 epoch 结束的时候进行文本的生成。

model = create_model()
model.fit(text_ds, epochs=30, callbacks=[text_gen_callback])

下面对部分结果进行打印展示。可以看出来生成的文本效果一般,可能和数据集质量以及模型的复杂度有关。

Epoch 1/30
0s 169ms/stepse_5_loss: 5.59
generated text:
this movie is very good movie . the worst movie is about the plot . the story of course of course the story line is a great story about it . it is so well . the plot is a great plot of the
Epoch 2/30
0s 17ms/step- loss: 4.7109 - dense_5_loss: 4.71
generated text:
this movie is a great movie . a wonderful movie about it , it was just the characters that they were not a movie but the way the acting was not a bad script that is bad . but the script was bad ,
...
Epoch 12/30
0s 18ms/step- loss: 3.6976 - dense_5_loss: 3.69
generated text:
this movie is one of the best movies i have ever seen and i have seen it on vhs uncut and i 've seen the first time . i watched this film for the first and was all of it . it was great
Epoch 13/30
0s 19ms/step- loss: 3.6531 - dense_5_loss: 3.65
generated text:
this movie is one of the worst actors i have ever seen . it is the worst bollywood movie i have ever seen . i have no idea it . the acting was terrible and the directing is bad , but it was bad
...
Epoch 29/30
0s 18ms/step- loss: 3.2507 - dense_5_loss: 3.25
generated text:
this movie is so [UNK] and the acting is awful , but the script is poor . the plot is laughable and the ending is terrible . there isn 't anything about this movie that was so bad it doesn 't make any sense
Epoch 30/30
0s 17ms/step- loss: 3.2359 - dense_5_loss: 3.23
generated text:
this movie is not a great time , but this movie is one of those actors that are [UNK] and that you can not take up to the screen . the plot is simple . it doesn 't matter what 's going on and
本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

文本分类入门实战 - 掘金

2023-12-7 19:51:14

AI教程

V4.0阶段下的人工智能对传统AI公司的影响

2023-12-7 19:55:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索