Generación de texto estilo GPT de 5 líneas en Python con TensorFlow/Keras

En esta guía, aprenda a escribir una red de aprendizaje profundo NLP estilo GPT (transformador) personalizada en solo 5 líneas de código, utilizando TensorFlow y Keras/KerasNLP y Python, y genere texto con un modelo de lenguaje natural autorregresivo.

Los transformadores, aunque se lanzaron en 2017, solo comenzaron a ganar una tracción significativa en los últimos años. Con la proliferación de la tecnología a través de plataformas como HuggingFace, NLP y Large Language Models (LLM) se han vuelto más accesibles que nunca.

Sin embargo, incluso con toda la exageración que los rodea y con muchas guías orientadas a la teoría, no hay muchas implementaciones personalizadas en línea, y los recursos no están tan disponibles como con algunos otros tipos de redes, que han existido. a lo largo. Si bien podría simplificar su ciclo de trabajo usando un Transformador preconstruido de HuggingFace (el tema de otra guía), puede llegar a sentir cómo funciona al construir uno usted mismo, antes de abstraerlo a través de una biblioteca. Nos centraremos en la construcción, en lugar de la teoría y la optimización aquí.

En esta guía, construiremos un Modelo de lenguaje autorregresivo para generar texto. Nos centraremos en los aspectos prácticos y minimalistas/concisos de cargar datos, dividirlos, vectorizarlos, construir un modelo, escribir una devolución de llamada personalizada y capacitación/inferencia. Cada una de estas tareas se puede dividir en guías más detalladas, por lo que mantendremos la implementación como genérica, dejando espacio para la personalización y optimización según su propio conjunto de datos.

Tipos de LLM y GPT-Fyodor

Si bien la categorización puede volverse mucho más compleja, puede ampliamente categorizar los modelos de lenguaje basados ​​en Transformer en tres categorías:

  • Modelos basados ​​en codificador - ALBERT, BERT, DistilBERT, RoBERTa
  • Basado en decodificador - GPT, GPT-2, GPT-3, TransformerXL
  • Modelos Seq2Seq - BART, mBART, T5

Los modelos basados ​​en codificadores solo usan un codificador Transformer en su arquitectura (generalmente, apilados) y son excelentes para comprender oraciones (clasificación, reconocimiento de entidades nombradas, respuesta a preguntas).

Los modelos basados ​​en decodificadores solo usan un decodificador Transformer en su arquitectura (también generalmente apilado) y son excelentes para la predicción futura, lo que los hace adecuados para la generación de texto.

Los modelos Seq2Seq combinan codificadores y decodificadores y son excelentes para generar texto, resumir y, lo que es más importante, traducir.

La familia de modelos GPT, que ganó mucha tracción en los últimos años, son modelos de transformadores basados ​​en decodificadores y son excelentes para producir texto similar al humano, entrenados en grandes corpus de datos y con un aviso como un nuevo semilla inicial para la generación. Por ejemplo:

1
generate_text('the truth ultimately is')

Que debajo del capó alimenta este mensaje en un modelo similar a GPT y produce:

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

¡Este es, de hecho, un pequeño spoiler del final de la guía! Otro pequeño spoiler es la arquitectura que produjo ese texto:

1
2
3
4
5
6
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 líneas es todo lo que se necesita para construir un modelo de transformador de solo decodificador, simulando un GPT pequeño. Dado que entrenaremos el modelo en las novelas de Fyodor Dostoyevsky (que puede sustituir con cualquier otra cosa, desde Wikipedia hasta los comentarios de Reddit), tentativamente llamaremos al modelo GPT-Fyodor.

PNL dura

El truco para un GPT-Fyodor de 5 líneas radica en Keras PNL, que está desarrollado por el equipo oficial de Keras, como una extensión horizontal de Keras, que, al más puro estilo de Keras, tiene como objetivo poner al alcance de su mano NLP de la fuerza de la industria, con nuevas capas (codificadores, decodificadores, incrustaciones de tokens, incrustaciones de posición, métricas, tokenizadores, etc.).

KerasNLP no es un zoológico modelo. Es una parte de Keras (como un paquete separado), que reduce la barrera de entrada para el desarrollo del modelo NLP, al igual que reduce la barrera de entrada para el desarrollo general de aprendizaje profundo con el paquete principal.

{.icon aria-hidden=“true”}

Nota: Al momento de escribir, KerasNLP todavía se está produciendo y en las primeras etapas. Las diferencias sutiles pueden estar presentes en futuras versiones. El informe está utilizando la versión 0.3.0.

Para poder usar KerasNLP, deberá instalarlo a través de pip:

1
$ pip install keras_nlp

Y puedes verificar la versión con:

1
2
keras_nlp.__version__
# 0.3.0

Implementación de un modelo de estilo GPT con Keras

Comencemos importando las bibliotecas que usaremos: TensorFlow, Keras, KerasNLP y NumPy:

1
2
3
4
import tensorflow as tf
from tensorflow import keras
import keras_nlp
import numpy as np

Cargando datos

Carguemos algunas de las novelas de Dostoyevsky: una sería demasiado corta para que se ajuste a un modelo, sin un poco de sobreajuste desde las primeras etapas en adelante. Usaremos con gracia los archivos de texto sin formato de Proyecto Gutenberg, debido a la simplicidad de trabajar con dichos datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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()
        # First 50 lines are the Gutenberg intro and preface
        # Skipping first 10k characters for each book should be approximately
        # removing the intros and prefaces.
        texts += text[10000:]

Simplemente descargamos todos los archivos, los revisamos y los concatenamos uno encima del otro. ¡Esto incluye cierta diversidad en el lenguaje utilizado, sin dejar de mantenerlo claramente Fyodor! Para cada archivo, hemos saltado los primeros 10k caracteres, que es aproximadamente la longitud promedio del prefacio y la introducción de Gutenberg, por lo que nos queda un cuerpo del libro prácticamente intacto para cada iteración. Echemos un vistazo a algunos 500 caracteres aleatorios en la cadena texts ahora:

1
2
# 500 characters
texts[25000:25500]
1
'nd that was why\nI addressed you at once. For in unfolding to you the story of my life, I\ndo 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 man\nof feeling and education. Know then that my wife was educated in a\nhigh-class school for the daughters of noblemen, and on leaving she\ndanced the shawl dance before the governor and other personages for\nwhich she was presented with a gold medal and a certificate of merit.\n'

Separemos la cadena en oraciones antes de realizar cualquier otro procesamiento:

1
2
text_list = texts.split('.')
len(text_list) # 69181

Tenemos 69k oraciones. Cuando reemplaza los caracteres \n con espacios en blanco y cuenta las palabras:

1
len(texts.replace('\n', ' ').split(' ')) # 1077574

{.icon aria-hidden=“true”}

Nota: Por lo general, querrá tener al menos un millón de palabras en un conjunto de datos e, idealmente, mucho más que eso. Estamos trabajando con unos pocos megabytes de datos (~5 MB), mientras que los modelos de lenguaje se entrenan más comúnmente en decenas de gigabytes de texto. Naturalmente, esto hará que sea muy fácil sobreajustar la entrada de texto y difícil de generalizar (alta perplejidad sin sobreajuste, o baja perplejidad con mucho sobreajuste). Toma los resultados con pinzas.

Sin embargo, dividámoslos en un conjunto de entrenamiento, prueba y validación. Primero, eliminemos las cadenas vacías y mezclemos las oraciones:

1
2
3
4
5
# Filter out empty strings ('') that are to be found commonly due to the book's format
text_list = list(filter(None, text_list))

import random
random.shuffle(text_list)

Luego, haremos una división 70/15/15:

1
2
3
4
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):]

Esta es una forma simple pero efectiva de realizar una división de validación de prueba de entrenamiento. Echemos un vistazo a text_train:

1
2
[' It was a dull morning, but the snow had ceased',
 '\n\n"Pierre, you who know so much of what goes on here, can you really have\nknown nothing of this business and have heard nothing about it?"\n\n"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 he\nsaid?"\n\nThere was a general outcry; but then suddenly an incident took place\nwhich no one could have anticipated', ...

¡Es hora de la estandarización y la vectorización!

Vectorización de texto {#vectorización de texto}

Las redes no entienden las palabras, entienden los números. Querremos tokenizar las palabras:

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

Además, dado que las oraciones difieren en longitud, el relleno generalmente se agrega a la izquierda o a la derecha para garantizar la misma forma en todas las oraciones que se ingresan. Digamos que nuestra oración más larga tiene 5 palabras (tokens) de largo. En ese caso, la oración de Wall-E se rellenaría con dos ceros, por lo que aseguramos la misma forma de entrada:

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

Tradicionalmente, esto se hacía usando los métodos TensorFlow Tokenizer y Keras' pad_sequences(); sin embargo, se puede usar una capa mucho más práctica, TextVectorization, que tokeniza y rellena su entrada, lo que le permite extraer el vocabulario y su tamaño, sin saber el vocabulario por adelantado!

Vamos a adaptar y ajustar una capa TextVectorization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
# You can also set calculate the longest sentence in the data - 25 in this case
# maxlen = len(max(text_list).split(' ')) 

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

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

El método custom_standardization() puede ser mucho más largo que esto. Simplemente hemos puesto en minúsculas todas las entradas y hemos reemplazado \n con " ". Aquí es donde realmente puede poner la mayor parte de su preprocesamiento de texto y suministrarlo a la capa de vectorización a través del argumento opcional “estandarizar”. Una vez que adapta() la capa al texto (arreglo NumPy o lista de textos), puede obtener el vocabulario, así como su tamaño desde allí:

1
2
vocab_size = len(vocab)
vocab_size # 49703

Finalmente, para destokenizar palabras, crearemos un diccionario index_lookup:

1
2
index_lookup = dict(zip(range(len(vocab)), vocab))    
index_lookup[5] # of

Asigna todos los tokens ([1, 2, 3, 4, ...]) a palabras en el vocabulario (['a', 'the', 'i', ...]). Al pasar una clave (índice de token), podemos recuperar fácilmente la palabra. Ahora puede ejecutar vectorize_layer() en cualquier entrada y observar las oraciones vectorizadas:

1
vectorize_layer(['hello world!'])

Lo que resulta en:

1
2
3
4
5
6
<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)>

¡Hello tiene el índice de 1 mientras que el mundo tiene el índice de 7509! El resto es el relleno del maxlen que hemos calculado.

Tenemos los medios para vectorizar texto. Ahora, vamos a crear conjuntos de datos a partir de text_train, text_test y text_valid, utilizando nuestra capa de vectorización como medio de conversión entre palabras y vectores que se pueden introducir en GPT-Fyodor.

Creación de conjuntos de datos

Crearemos un tf.data.Dataset para cada uno de nuestros conjuntos, usando from_tensor_slices() y proporcionando una lista de, bueno, segmentos de tensor (frases):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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)

Una vez creado y barajado (nuevamente, por si acaso), podemos aplicar una función de preprocesamiento (vectorización y división de secuencias):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)

La función preprocess_text() simplemente se expande por la última dimensión, vectoriza el texto usando nuestra vectorize_layer y crea las entradas y los objetivos, compensados ​​por un solo token. El modelo usará [0..n] para inferir n+1, generando una predicción para cada palabra, teniendo en cuenta todas las palabras anteriores. Echemos un vistazo a una sola entrada en cualquiera de los conjuntos de datos:

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

Al investigar las entradas y los objetivos devueltos, en lotes de 64 (con una longitud de 30 cada uno), podemos ver claramente cómo se compensan con uno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(<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)>)

Finalmente, ¡es hora de construir el modelo!

Definición del modelo {#definición del modelo}

Haremos uso de las capas de KerasNLP aquí. Después de una Entrada, codificaremos la entrada a través de una capa TokenAndPositionEmbedding, pasando nuestro vocab_size, maxlen y embed_dim. El mismo embed_dim que esta capa genera e ingresa en el TransformerDecoder se retendrá en el Decodificador. Al momento de escribir, el decodificador mantiene automáticamente la dimensionalidad de entrada y no le permite proyectarla en una salida diferente, pero sí le permite definir las dimensiones latentes a través del argumento intermediate_dim.

Multiplicaremos las dimensiones de incrustación por dos para la representación latente, pero puede mantenerlo igual o usar un número separado de las dimensiones de incrustación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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()

En la parte superior del decodificador, tenemos una capa Densa para elegir la siguiente palabra en la secuencia, con una activación softmax (que produce la distribución de probabilidad para cada token siguiente). Echemos un vistazo al resumen del modelo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 apila muchos decodificadores: GPT-2 Small tiene 12 decodificadores apilados (117M de parámetros), mientras que GPT-2 Extra Large tiene 48 decodificadores apilados (1.5B de parámetros). Nuestro modelo de decodificador único con unos humildes parámetros de 13M debería funcionar lo suficientemente bien para fines educativos. Con LLM, la ampliación ha demostrado ser una estrategia extremadamente buena, y los transformadores permiten una buena ampliación, lo que hace factible entrenar modelos extremadamente grandes.

GPT-3 tiene unos "escasos" parámetros 175B. El equipo de Google Brain entrenó un modelo de parámetros de 1,6 T para realizar investigaciones de escasez y mantener los cálculos al mismo nivel que los modelos mucho más pequeños.

De hecho, si aumentamos el número de decodificadores de 1 a 3:

1
2
3
4
5
6
7
8
9
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)

Nuestro recuento de parámetros se incrementaría en 400k:

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

¡La mayoría de los parámetros en nuestra red provienen de las capas TokenAndPositionEmbedding y Dense!

Pruebe diferentes profundidades del decodificador, desde 1 hasta todo lo que su máquina puede manejar y observe los resultados. En cualquier caso, ¡estamos casi listos para entrenar al modelo! Vamos a crear una devolución de llamada personalizada que producirá una muestra de texto en cada época, para que podamos ver cómo el modelo aprende a formar oraciones a través del entrenamiento.

Devolución de llamada personalizada

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class TextSampler(keras.callbacks.Callback):
    def __init__(self, start_prompt, max_tokens):
        self.start_prompt = start_prompt
        self.max_tokens = max_tokens
        
    # Helper method to choose a word from the top K probable words with respect to their probabilities
    # in a sequence
    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)
            # To find the index of the next word in the prediction array.
            # The tokenized prompt is already shorter than the original decoded sample
            # by one, len(decoded_sample.split()) is two words ahead - so we remove 1 to get
            # the next word in the sequence
            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")

# First 5 words of a random sentence to be used as a seed
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')

Entrenando al modelo {#entrenando al modelo}

¡Por fin, hora de entrenar! Ingresemos nuestro train_dataset y validation_dataset con las devoluciones de llamada en su lugar:

1
2
3
4
5
model = create_model()
history = model.fit(train_dataset, 
                    validation_data=valid_dataset,
                    epochs=10, 
                    callbacks=[sampler, reducelr])

La muestra eligió una oración desafortunada que comienza con la cita final y la cita inicial, pero aún produce resultados interesantes durante el entrenamiento:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 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

Empieza con:

"Qué no hubieras sido yo hubiera sido el mismo"...

Lo cual realmente no tiene mucho sentido. Al final de las diez épocas cortas, produce algo similar a:

"¿Qué quieres decir con que es el hombre más común de un hombre por supuesto"...

Si bien la segunda oración todavía no tiene demasiado sentido, es mucho más sensata que la primera. Un entrenamiento más prolongado con más datos (con pasos de preprocesamiento más complejos) produciría mejores resultados. Solo lo hemos entrenado en 10 épocas con un alto abandono para combatir el tamaño pequeño del conjunto de datos. Si se dejara entrenando durante mucho más tiempo, produciría un texto muy parecido al de Fyodor, porque habría memorizado grandes partes del mismo.

{.icon aria-hidden=“true”}

Nota: Dado que la salida es bastante detallada, puede modificar el argumento verbose mientras ajusta el modelo para reducir la cantidad de texto en la pantalla.

Inferencia del modelo

Para realizar la inferencia, querremos replicar la interfaz de TextSampler, un método que acepta una semilla y una response_length (max_tokens). Usaremos los mismos métodos que dentro de la muestra:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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

Ahora, puede ejecutar el método en muestras nuevas:

1
2
3
4
5
generate_text('the truth ultimately is')
# 'the truth ultimately is really a joy in history, this state of life through which is almost invisible, superfluous  teleological'

generate_text('the truth ultimately is')
# 'the truth ultimately is not to make it a little   thing to go into your own  life for some'

¿Mejorar los resultados?

Entonces, ¿cómo se pueden mejorar los resultados? Hay algunas cosas bastante procesables que podrías hacer:

  • Limpieza de datos (limpie los datos de entrada más meticulosamente, simplemente recortamos un número aproximado desde el principio y eliminamos los caracteres de nueva línea)
  • Obtener más datos (solo trabajamos con unos pocos megas de datos de texto)
  • Escale el modelo junto con los datos (¡apilar decodificadores no es difícil!)

Yendo más lejos: proyecto de extremo a extremo portátil

¿Tu naturaleza inquisitiva te hace querer ir más allá? Recomendamos consultar nuestro Proyecto guiado: ["Subtítulos de imágenes con CNN y Transformers con Keras"](https://wikihtp.com/courses/image-captioning-with-cnns-and -transformadores-con-keras/#cta){target="_blank"}.

En este proyecto guiado, aprenderá a crear un modelo de subtítulos de imágenes, que acepta una imagen como entrada y produce un subtítulo de texto como salida.

Aprenderás a:

  • Preprocesar texto
  • Vectorizar la entrada de texto fácilmente
  • Trabaje con la API tf.data y cree conjuntos de datos de alto rendimiento
  • Cree Transformers desde cero con TensorFlow/Keras y KerasNLP: la adición horizontal oficial a Keras para crear modelos NLP de última generación
  • Cree arquitecturas híbridas donde la salida de una red se codifica para otra

¿Cómo enmarcamos los subtítulos de las imágenes? La mayoría lo considera un ejemplo de aprendizaje profundo generativo, porque estamos enseñando a una red a generar descripciones. Sin embargo, me gusta verlo como una instancia de traducción automática neuronal: estamos traduciendo las características visuales de una imagen en palabras. A través de la traducción, estamos generando una nueva representación de esa imagen, en lugar de simplemente generar un nuevo significado. Verlo como traducción, y solo por generación de extensión, enfoca la tarea bajo una luz diferente y la hace un poco más intuitiva.

Enmarcar el problema como uno de traducción hace que sea más fácil averiguar qué arquitectura querremos usar. Los transformadores solo de codificador son excelentes para comprender el texto (análisis de opinión, clasificación, etc.) porque los codificadores codifican representaciones significativas. Los modelos de solo decodificador son excelentes para la generación (como GPT-3), ya que los decodificadores pueden inferir representaciones significativas en otra secuencia con el mismo significado. La traducción generalmente se realiza mediante una arquitectura de codificador-decodificador, donde los codificadores codifican una representación significativa de una oración (o imagen, en nuestro caso) y los decodificadores aprenden a convertir esta secuencia en otra representación significativa que es más interpretable para nosotros (como una oración).

Conclusión

Si bien la canalización de preprocesamiento es minimalista y se puede mejorar, la canalización descrita en esta guía produjo un modelo de estilo GPT decente, con solo 5 líneas de código requeridas para construir un transformador personalizado solo de decodificador, usando Keras.

Los transformadores son populares y ampliamente aplicables para el modelado de secuencias genéricas (y muchas cosas se pueden expresar como secuencias). Hasta ahora, la principal barrera de entrada era una implementación engorrosa, pero con KerasNLP, los profesionales del aprendizaje profundo pueden aprovechar las implementaciones para crear modelos de forma rápida y sencilla.