Modelos de lenguaje para completar oraciones
Modelos de lenguaje para completar oraciones
Una aplicación práctica de un modelo de idioma que elige la palabra candidata más probable que extiende una oración en inglés con una palabra
Co-escrito con Naresh Singh.
Tabla de contenidos
IntroducciónPlanteamiento del problemaGeneración de ideas para una solución
- Algoritmos y estructuras de datos
- Procesamiento del lenguaje natural (NLP)
- Aprendizaje profundo (Redes neuronales)
Un modelo LSTM
- Tokenización
- El modelo PyTorch
- Usando el modelo para eliminar sugerencias inválidas
- Calculando la probabilidad de la siguiente palabra
Un modelo Transformer
Conclusión
- PyCharm vs. Spyder Eligiendo el IDE correcto de Python
- Investigadores de la Universidad Heriot-Watt y Alana AI proponen Fu...
- Ajustar el Falcon 7B y otros LLMs en Amazon SageMaker con el decora...
Introducción
Los modelos de lenguaje como GPT se han vuelto muy populares recientemente y se utilizan para una variedad de tareas de generación de texto, como en ChatGPT u otros sistemas de IA conversacionales. Estos modelos de lenguaje son enormes, a menudo superan los miles de millones de parámetros, y requieren muchos recursos informáticos y dinero para funcionar.
En el contexto de los modelos de lenguaje en inglés, estos modelos masivos están sobreparametrizados, ya que utilizan los parámetros del modelo para memorizar y aprender aspectos de nuestro mundo en lugar de simplemente modelar el idioma inglés. Es probable que podamos usar un modelo mucho más pequeño si tenemos una aplicación que requiere que el modelo comprenda solo el lenguaje y sus construcciones.
El código completo para realizar inferencias en el modelo entrenado se puede encontrar en este notebook.
Planteamiento del problema
Supongamos que estamos construyendo un sistema de teclado deslizante que intenta predecir la palabra que escribirás a continuación en tu teléfono móvil. Según el patrón trazado por el deslizamiento, hay muchas posibilidades para la palabra que el usuario pretende usar. Sin embargo, muchas de estas posibles palabras no son palabras reales en inglés y se pueden eliminar. Incluso después de este paso inicial de eliminación y reducción, todavía quedan muchos candidatos, y necesitamos elegir uno como sugerencia para el usuario.
Para reducir aún más esta lista de candidatos, podemos usar un modelo de lenguaje basado en aprendizaje profundo que analice el contexto proporcionado y nos diga cuál es el candidato más probable para completar la oración.
Por ejemplo, si el usuario ha escrito la oración “He programado esto” y luego desliza un patrón como se muestra a continuación
Entonces, algunas posibles palabras en inglés que el usuario podría haber querido decir son:
- desordenando
- reunión
Sin embargo, si lo pensamos, es probable que el usuario haya querido decir “reunión” y no “desordenando” debido a la palabra “programado” en la parte anterior de la oración.
Dado todo lo que sabemos hasta ahora, ¿qué opciones tenemos para realizar esta reducción de forma programática? Analicemos algunas soluciones en la sección siguiente.
Generación de ideas para una solución
Algoritmos y estructuras de datos
Usando principios básicos, parece razonable comenzar con un corpus de datos, encontrar pares de palabras que se unan y entrenar un modelo de Markov que prediga la probabilidad de que el par ocurra en una oración. Notarás dos problemas significativos con este enfoque.
- Utilización del espacio: Hay entre 250 000 y 1 millón de palabras en el idioma inglés, sin incluir los numerosos nombres propios que constantemente están aumentando en volumen. Por lo tanto, cualquier solución de software tradicional que modele la probabilidad de un par de palabras que ocurren juntas debe mantener una tabla de búsqueda con 250 000 * 250 000 = 62.5 mil millones de pares de palabras, lo cual es excesivo. Parece probable que muchos pares no ocurran con mucha frecuencia y se puedan eliminar. Incluso después de la eliminación, aún hay muchos pares de los que preocuparse.
- Completitud: Codificar la probabilidad de un par de palabras no hace justicia al problema en cuestión. Por ejemplo, el contexto de la oración anterior se pierde por completo cuando solo se analiza el par de palabras más reciente. En la oración “Cómo va tu día”, si quieres verificar la palabra después de “día”, tendrías muchos pares que comienzan con “día”. Esto pasa por alto todo el contexto de la oración antes de esa palabra. Uno puede imaginar usar tripletas de palabras, etc… pero esto agrava el problema de la utilización del espacio mencionado anteriormente.
Centremos nuestra atención en una solución que aproveche la naturaleza del idioma inglés y veamos si eso puede ayudarnos aquí.
PNL (Procesamiento del Lenguaje Natural)
Históricamente, el área de PNL (procesamiento del lenguaje natural) involucraba entender las partes de la oración (POS) de una frase y utilizar esa información para tomar decisiones de poda y predicción. Uno puede imaginar el uso de una etiqueta POS asociada con cada palabra para determinar si la siguiente palabra en una frase es válida.
Sin embargo, el proceso de computar las partes de la oración para una frase es en sí mismo un proceso complejo y requiere un entendimiento especializado del lenguaje, como se evidencia en esta página sobre etiquetado de partes de la oración en NLTK.
A continuación, echemos un vistazo a un enfoque basado en aprendizaje profundo que requiere mucha más cantidad de datos etiquetados, pero no tanta experiencia en el lenguaje para construirlo.
Aprendizaje Profundo (Redes Neuronales)
El área de PNL ha sido revolucionada por la llegada del aprendizaje profundo. Con la invención de los modelos de lenguaje basados en LSTM y Transformer, la solución a menudo implica alimentar al modelo con datos de alta calidad y entrenarlo para predecir la siguiente palabra.
En esencia, esto es lo que hace el modelo GPT. Los modelos GPT (Generative Pre-Trained Transformer) están entrenados para predecir la siguiente palabra (token) dada una parte inicial de una frase.
Dada la parte inicial de la frase “Es tan maravilloso”, es probable que el modelo proporcione las siguientes predicciones de alta probabilidad para la palabra que sigue a la frase.
- día
- experiencia
- mundo
- vida
También es probable que las siguientes palabras tengan una menor probabilidad de completar la parte inicial de la frase.
- rojo
- ratón
- línea
La arquitectura del modelo Transformer es el corazón de sistemas como ChatGPT. Sin embargo, para el caso de uso más restringido de aprender la semántica del idioma inglés, podemos utilizar una arquitectura de modelo más económica de ejecutar, como un modelo LSTM (memoria a corto plazo de largo alcance).
Un modelo LSTM
Construyamos un modelo LSTM simple y entrenémoslo para predecir el siguiente token dado un prefijo de tokens. Ahora, podrías preguntar qué es un token.
Tokenización
Típicamente, para los modelos de lenguaje, un token puede significar
- Un solo carácter (o un solo byte)
- Toda una palabra en el idioma objetivo
- Algo entre 1 y 2. Esto se suele llamar sub-palabra
Mapear un solo carácter (o byte) a un token es muy restrictivo, ya que estamos sobrecargando ese token para mantener mucho contexto sobre dónde ocurre. Esto se debe a que el carácter “c”, por ejemplo, ocurre en muchas palabras diferentes, y para predecir el siguiente carácter después de ver el carácter “c” requiere que analicemos detenidamente el contexto previo.
Mapear una sola palabra a un token también es problemático, ya que el inglés en sí mismo tiene entre 250 mil y 1 millón de palabras. Además, ¿qué sucede cuando se agrega una nueva palabra al idioma? ¿Necesitamos volver y volver a entrenar todo el modelo para tener en cuenta esta nueva palabra?
La tokenización de sub-palabras se considera el estándar de la industria en el año 2023. Asigna subcadenas de bytes que ocurren con frecuencia juntas a tokens únicos. Típicamente, los modelos de lenguaje tienen desde unos pocos miles (digamos 4,000) hasta decenas de miles (digamos 60,000) de tokens únicos. El algoritmo para determinar qué constituye un token es determinado por el algoritmo BPE (Codificación de pares de bytes).
Para elegir el número de tokens únicos en nuestro vocabulario (llamado tamaño del vocabulario), debemos tener en cuenta algunas cosas:
- Si elegimos muy pocos tokens, volvemos al régimen de un token por carácter, y es difícil que el modelo aprenda algo útil.
- Si elegimos demasiados tokens, terminamos en una situación donde las tablas de inserción del modelo eclipsan el resto del peso del modelo y se vuelve difícil implementar el modelo en un entorno restringido. El tamaño de la tabla de inserción dependerá del número de dimensiones que usemos para cada token. No es raro usar un tamaño de inserción de 256, 512, 786, etc. Si usamos una dimensión de inserción de token de 512 y tenemos 100k tokens, terminamos con una tabla de inserción que usa 200MiB de memoria.
Por lo tanto, debemos encontrar un equilibrio al elegir el tamaño del vocabulario. En este ejemplo, seleccionamos 6600 tokens y entrenamos nuestro tokenizador con un tamaño de vocabulario de 6600. A continuación, veamos la definición del modelo en sí.
El modelo PyTorch
El modelo en sí es bastante sencillo. Tenemos las siguientes capas:
- Embedding de Tokens (tamaño del vocabulario = 6600, dimensión de embedding = 512), para un tamaño total de aproximadamente 15MiB (asumiendo float32 de 4 bytes como tipo de datos de la tabla de embedding)
- LSTM (número de capas = 1, dimensión oculta = 786) para un tamaño total de aproximadamente 16MiB
- Perceptrón Multicapa (dimensiones de 786 a 3144 a 6600) para un tamaño total de aproximadamente 93MiB
El modelo completo tiene aproximadamente 31M de parámetros entrenables para un tamaño total de aproximadamente 120MiB.
Aquí está el código de PyTorch para el modelo.
class WordPredictionLSTMModel(nn.Module): def __init__(self, num_embed, embed_dim, pad_idx, lstm_hidden_dim, lstm_num_layers, output_dim, dropout): super().__init__() self.vocab_size = num_embed self.embed = nn.Embedding(num_embed, embed_dim, pad_idx) self.lstm = nn.LSTM(embed_dim, lstm_hidden_dim, lstm_num_layers, batch_first=True, dropout=dropout) self.fc = nn.Sequential( nn.Linear(lstm_hidden_dim, lstm_hidden_dim * 4), nn.LayerNorm(lstm_hidden_dim * 4), nn.LeakyReLU(), nn.Dropout(p=dropout), nn.Linear(lstm_hidden_dim * 4, output_dim), ) # def forward(self, x): x = self.embed(x) x, _ = self.lstm(x) x = self.fc(x) x = x.permute(0, 2, 1) return x ##
Aquí está el resumen del modelo usando torchinfo.
Resumen del modelo LSTM
=================================================================Layer (type:depth-idx) Param #=================================================================WordPredictionLSTMModel - ├─Embedding: 1–1 3,379,200├─LSTM: 1–2 4,087,200├─Sequential: 1–3 - │ └─Linear: 2–1 2,474,328│ └─LayerNorm: 2–2 6,288│ └─LeakyReLU: 2–3 - │ └─Dropout: 2–4 - │ └─Linear: 2–5 20,757,000=================================================================Total params: 30,704,016Trainable params: 30,704,016Non-trainable params: 0=================================================================
Interpretando la precisión: Después de entrenar este modelo con 12M de oraciones en inglés durante aproximadamente 8 horas en una GPU P100, logramos una pérdida de 4.03, una precisión top-1 del 29% y una precisión top-5 del 49%. Esto significa que el modelo pudo predecir correctamente el siguiente token el 29% del tiempo, y el 49% del tiempo, el siguiente token en el conjunto de entrenamiento fue una de las 5 predicciones principales del modelo.
¿Cuál debería ser nuestra métrica de éxito? Si bien los números de precisión top-1 y top-5 de nuestro modelo no son impresionantes, no son tan importantes para nuestro problema. Nuestras palabras candidatas son un conjunto pequeño de posibles palabras que se ajustan al patrón de deslizamiento. Lo que queremos de nuestro modelo es poder seleccionar un candidato ideal para completar la oración de manera que sea sintáctica y semánticamente coherente. Dado que nuestro modelo aprende la naturaleza del lenguaje a través de los datos de entrenamiento, esperamos que asigne una probabilidad más alta a las oraciones coherentes. Por ejemplo, si tenemos la oración “El jugador de béisbol” y posibles candidatos de completado (“corrió”, “nadó”, “se escondió”), entonces la palabra “corrió” es una mejor palabra de seguimiento que las otras dos. Por lo tanto, si nuestro modelo predice la palabra “corrió” con una probabilidad más alta que el resto, funciona para nosotros.
Interpretando la pérdida: Una pérdida de 4.03 significa que el logaritmo negativo de la probabilidad de la predicción es 4.03, lo que significa que la probabilidad de predecir correctamente el siguiente token es e^-4.03 = 0.0178 o 1/56. Un modelo inicializado al azar típicamente tiene una pérdida de aproximadamente 8.8, que es -log_e(1/6600), ya que el modelo predice al azar 1/6600 tokens (6600 es el tamaño del vocabulario). Si bien una pérdida de 4.03 puede no parecer muy buena, es importante recordar que el modelo entrenado es aproximadamente 120 veces mejor que un modelo no entrenado (o inicializado al azar).
A continuación, veamos cómo podemos usar este modelo para mejorar las sugerencias de nuestro teclado deslizante.
Uso del modelo para eliminar sugerencias inválidas
Veamos un ejemplo real. Supongamos que tenemos una frase parcial “Creo que” y el usuario hace el patrón deslizante que se muestra en azul a continuación, comenzando en “o”, yendo entre las letras “c” y “v”, y terminando entre las letras “e” y “v”.
Algunas posibles palabras que podrían representar este patrón deslizante son:
- Sobre
- Oct (abreviatura de Octubre)
- Hielo
- Tengo (con el apóstrofe implícito)
De estas sugerencias, la más probable probablemente sea “Tengo”. Alimentemos estas sugerencias a nuestro modelo y veamos qué resulta.
[Creo que] [Tengo] = 0.00087[Creo que] [Sobre] = 0.00051[Creo que] [Hielo] = 0.00001[Creo que] [Oct] = 0.00000
El valor después del signo = es la probabilidad de que la palabra sea una finalización válida del prefijo de la frase. En este caso, vemos que la palabra “Tengo” ha sido asignada la probabilidad más alta. Por lo tanto, es la palabra más probable que siga al prefijo de la frase “Creo que”.
La siguiente pregunta que podrías tener es cómo podemos calcular estas probabilidades de la siguiente palabra. Veamos.
Cálculo de la probabilidad de la siguiente palabra
Para calcular la probabilidad de que una palabra sea una finalización válida de un prefijo de frase, ejecutamos el modelo en modo de evaluación (inferencia) y alimentamos el prefijo de la frase tokenizado. También tokenizamos la palabra después de agregar un espacio en blanco al principio de la palabra. Esto se hace porque el pre-tokenizador de HuggingFace divide las palabras con espacios al principio de la palabra, por lo que queremos asegurarnos de que nuestras entradas sean consistentes con la estrategia de tokenización utilizada por HuggingFace Tokenizers.
Supongamos que la palabra candidata está compuesta por 3 tokens T0, T1 y T2.
- Primero ejecutamos el modelo con el prefijo de frase tokenizado original. Para el último token, verificamos la probabilidad de predecir el token T0. Agregamos esto a la lista “probs”.
- A continuación, ejecutamos una predicción en el prefijo + T0 y verificamos la probabilidad del token T1. Agregamos esta probabilidad a la lista “probs”.
- A continuación, ejecutamos una predicción en el prefijo + T0 + T1 y verificamos la probabilidad del token T2. Agregamos esta probabilidad a la lista “probs”.
La lista “probs” contiene las probabilidades individuales de generar los tokens T0, T1 y T2 en secuencia. Dado que estos tokens corresponden a la tokenización de la palabra candidata, podemos multiplicar estas probabilidades para obtener la probabilidad combinada de que el candidato sea una finalización del prefijo de la frase.
A continuación se muestra el código para calcular las probabilidades de finalización.
def get_completion_probability(self, input, completion, tok): self.model.eval() ids = tok.encode(input).ids ids = torch.tensor(ids, device=self.device).unsqueeze(0) completion_ids = torch.tensor(tok.encode(completion).ids, device=self.device).unsqueeze(0) probs = [] for i in range(completion_ids.size(1)): y = self.model(ids) y = y[0,:,-1].softmax(dim=0) # prob is the probability of this completion. prob = y[completion_ids[0,i]] probs.append(prob) ids = torch.cat([ids, completion_ids[:,i:i+1]], dim=1) # return torch.tensor(probs) #
A continuación se muestran algunos ejemplos adicionales.
[Ese helado se ve] [realmente] = 0.00709[Ese helado se ve] [delicioso] = 0.00264[Ese helado se ve] [absolutamente] = 0.00122[Ese helado se ve] [real] = 0.00031[Ese helado se ve] [pez] = 0.00004[Ese helado se ve] [papel] = 0.00001[Ese helado se ve] [atroz] = 0.00000[Ya que nos dirigimos] [hacia] = 0.01052[Ya que nos dirigimos] [lejos] = 0.00344[Ya que nos dirigimos] [en contra] = 0.00035[Ya que nos dirigimos] [ambos] = 0.00009[Ya que nos dirigimos] [muerte] = 0.00000[Ya que nos dirigimos] [burbuja] = 0.00000[Ya que nos dirigimos] [nacimiento] = 0.00000[¿Hice] [un] = 0.22704[¿Hice] [el] = 0.06622[¿Hice] [bien] = 0.00190[¿Hice] [comida] = 0.00020[¿Hice] [color] = 0.00007[¿Hice] [casa] = 0.00006[¿Hice] [color] = 0.00002[¿Hice] [lápiz] = 0.00001[¿Hice] [flor] = 0.00000[Queremos un candidato] [con] = 0.03209[Queremos un candidato] [que] = 0.02145[Queremos un candidato] [experiencia] = 0.00097[Queremos un candidato] [que] = 0.00094[Queremos un candidato] [más] = 0.00010[Queremos un candidato] [menos] = 0.00007[Queremos un candidato] [escuela] = 0.00003[Esta es la guía definitiva para] [el] = 0.00089[Esta es la guía definitiva para] [completo] = 0.00047[Esta es la guía definitiva para] [frase] = 0.00006[Esta es la guía definitiva para] [rapero] = 0.00001[Esta es la guía definitiva para] [ilustrado] = 0.00001[Esta es la guía definitiva para] [extravagante] = 0.00000[Esta es la guía definitiva para] [envoltura] = 0.00000[Esta es la guía definitiva para] [minúsculo] = 0.00000[Por favor, ¿puedes] [verificar] = 0.00502[Por favor, ¿puedes] [confirmar] = 0.00488[Por favor, ¿puedes] [cesar] = 0.00002[Por favor, ¿puedes] [acunar] = 0.00000[Por favor, ¿puedes] [portátil] = 0.00000[Por favor, ¿puedes] [sobre] = 0.00000[Por favor, ¿puedes] [opciones] = 0.00000[Por favor, ¿puedes] [cordon] = 0.00000[Por favor, ¿puedes] [corola] = 0.00000[Creo que] [Tengo] = 0.00087[Creo que] [Sobre] = 0.00051[Creo que] [Hielo] = 0.00001[Creo que] [Oct] = 0.00000[Por favor] [puedes] = 0.00428[Por favor] [taxi] = 0.00000[He programado esta] [reunión] = 0.00077[He programado esta] [estropeando] = 0.00000
Estos ejemplos muestran la probabilidad de la palabra que completa la oración antes de ella. Los candidatos se ordenan en orden decreciente de probabilidad.
Dado que los Transformers están reemplazando lentamente los modelos LSTM y RNN para tareas basadas en secuencias, echemos un vistazo a cómo sería un modelo Transformer para el mismo objetivo.
Un modelo Transformer
Los modelos basados en Transformers son una arquitectura muy popular para entrenar modelos de lenguaje que predicen la siguiente palabra en una oración. La técnica específica que utilizaremos es el mecanismo de atención causal. Entrenaremos solo la capa codificadora del transformer en PyTorch utilizando atención causal. La atención causal significa que permitiremos que cada token en la secuencia solo mire los tokens anteriores a él. Esto se asemeja a la información que una capa LSTM unidireccional utiliza cuando se entrena solo en la dirección hacia adelante.
El modelo Transformer que veremos aquí se basa directamente en nn.TransformerEncoder y nn.TransformerEncoderLayer en PyTorch.
import mathdef generate_src_mask(sz, device): return torch.triu(torch.full((sz, sz), True, device=device), diagonal=1)#class PositionalEmbedding(nn.Module): def __init__(self, sequence_length, embed_dim): super().__init__() self.sqrt_embed_dim = math.sqrt(embed_dim) self.pos_embed = nn.Parameter(torch.empty((1, sequence_length, embed_dim))) nn.init.uniform_(self.pos_embed, -1.0, 1.0) # def forward(self, x): return x * self.sqrt_embed_dim + self.pos_embed[:,:x.size(1)] ##class WordPredictionTransformerModel(nn.Module): def __init__(self, sequence_length, num_embed, embed_dim, pad_idx, num_heads, num_layers, output_dim, dropout, norm_first, activation): super().__init__() self.vocab_size = num_embed self.sequence_length = sequence_length self.embed_dim = embed_dim self.sqrt_embed_dim = math.sqrt(embed_dim) self.embed = nn.Sequential( nn.Embedding(num_embed, embed_dim, pad_idx), PositionalEmbedding(sequence_length, embed_dim), nn.LayerNorm(embed_dim), nn.Dropout(p=0.1), ) encoder_layer = nn.TransformerEncoderLayer( d_model=embed_dim, nhead=num_heads, dropout=dropout, batch_first=True, norm_first=norm_first, activation=activation, ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.fc = nn.Sequential( nn.Linear(embed_dim, embed_dim * 4), nn.LayerNorm(embed_dim * 4), nn.LeakyReLU(), nn.Dropout(p=dropout), nn.Linear(embed_dim * 4, output_dim), ) # def forward(self, x): src_attention_mask = generate_src_mask(x.size(1), x.device) x = self.embed(x) x = self.encoder(x, is_causal=True, mask=src_attention_mask) x = self.fc(x) x = x.permute(0, 2, 1) return x ##
Podemos usar este modelo en lugar del modelo LSTM que usamos antes, ya que su API es compatible. Este modelo tarda más en entrenar para la misma cantidad de datos de entrenamiento y tiene un rendimiento comparable.
Los modelos Transformer son mejores para secuencias largas. En nuestro caso, tenemos secuencias de longitud 256. La mayor parte del contexto necesario para completar la siguiente palabra tiende a ser local, por lo que realmente no necesitamos el poder de los Transformers aquí.
Conclusión
Vimos cómo podemos resolver problemas de procesamiento de lenguaje natural muy prácticos utilizando técnicas de aprendizaje profundo basadas en modelos LSTM (RNN) y Transformers. No todas las tareas de lenguaje requieren el uso de modelos con miles de millones de parámetros. Las aplicaciones especializadas que requieren modelar el propio lenguaje, y no memorizar grandes volúmenes de información, pueden manejarse utilizando modelos mucho más pequeños que se pueden implementar fácilmente y de manera más eficiente que los enormes modelos de lenguaje que estamos acostumbrados a ver en estos días.
Todas las imágenes, excepto la primera, fueron creadas por el/los autor(es).