En este tutorial vamos a explorar una de las arquitecturas más revolucionarias en el campo de la Inteligencia Artificial de los últimos años: la Red Transformer.
Específicamente, nos enfocaremos en una versión de BERT (Bidirectional Encoder Representations from Transformers) entrenada íntegramente en Español y veremos cómo utilizarla para una tarea específica de Procesamiento de Lenguaje Natural (NLP): el Question Answering o sistemas de pregunta-respuesta.
Aprenderemos a presentarle un texto de referencia a este modelo, formular preguntas sobre dicho contenido y verificar la capacidad de BERT para extraer las respuestas correctas de forma precisa.
Como siempre, al final del tutorial se encuentra el enlace de descarga del código fuente.
Introducción a BERT
El procesamiento del lenguaje natural ha experimentado una transformación gigantesca gracias a la arquitectura Transformer. BERT es una categoría especializada de esta arquitectura, capaz de interpretar y realizar múltiples tareas de NLP con una eficacia impresionante.
Para este tutorial, utilizaremos un modelo que no solo entiende el idioma español, sino que también ha pasado por un proceso de afinación (o fine-tuning). Esto significa que, partiendo de un modelo pre-entrenado, se ha agregado una red neuronal adicional y se ha re-entrenado con un set de datos específico (SQuAD2 en Español) que contiene miles de pares de preguntas y respuestas. De este modo, el modelo ya está optimizado para identificar fragmentos de texto que responden a interrogantes específicos dentro de un contexto dado.
Requerimientos e instalación
Para trabajar con estos modelos utilizaremos la librería Hugging Face Transformers, la cual centraliza una enorme variedad de modelos pre-entrenados basados precisamente en la arquitectura Transformer.
El primer paso es instalar la librería:
!pip install transformers
Este comando instalará todas las dependencias necesarias, incluyendo tokenizers, numpy y requests, esenciales para descargar y procesar los modelos desde los servidores de Hugging Face.
Carga del modelo y el Tokenizer
Para implementar el sistema, necesitamos importar tres componentes clave de la librería:
- AutoTokenizer: para procesar el texto y convertirlo en un formato numérico. Es decir para generar los tokens, que es lo que en últimas procesará BERT internamente.
- AutoModelForQuestionAnswering: para cargar la arquitectura del modelo afinada para una tarea de pregunta-respuesta.
- pipeline: para reducir la cantidad de código requerida para inferencia (es decir para presentar al modelo el texto y la pregunta y para que este genere el texto con la respuesta correspondiente)
Este sería el código requerido:
# Importar librerías necesarias
from transformers import AutoTokenizer, AutoModelForQuestionAnswering, pipeline
# Definir el modelo pre-entrenado (DistilBERT en Español y optimizado para SQuAD)
the_model = 'mrm8488/distill-bert-base-spanish-wwm-cased-finetuned-spa-squad2-es'
# Cargar el tokenizer y el modelo
tokenizer = AutoTokenizer.from_pretrained(the_model, do_lower_case=False)
model = AutoModelForQuestionAnswering.from_pretrained(the_model)
Explicación del código:
- Estamos utilizando una versión destilada (más ligera y rápida) de BERT en Español, específicamente
distill-bert-base-spanish-wwm-cased-finetuned-spa-squad2-es(línea 5) do_lower_case=False(línea 8) es crucial aquí para que el modelo sea capaz de diferenciar adecuadamente entre mayúsculas y minúsculas, lo cual es vital para identificar nombres propios.- La función
from_pretrained(líneas 8 y 9) descarga automáticamente los archivos de configuración y los pesos del modelo desde Internet.
Arquitectura del modelo y embeddings
Si inspeccionamos el modelo usando «print(model)», veremos que posee múltiples capas de codificación. Y en los bloques iniciales encontramos los embeddings.
En particular, BERT utiliza un vocabulario de aproximadamente 31,000 símbolos o palabras del Español. Cada una de estas palabras se codifica en un vector de 768 elementos. Esta representación numérica son precisamente los embeddings, y esta representación vectorial es la que ingresa a los siguientes bloques de la Red Transformer para ser procesada.
A la salida, el modelo cuenta con una capa lineal que toma el vector de 768 elementos y genera dos valores específicos: la posición de inicio y la posición de finalización de la respuesta dentro del texto original. Así que cuando el modelo se afina para esta tarea de pregunta-respuesta, en realidad lo que aprende es a predecir dichas posiciones de inicio y finalización.
Comprendiendo la tokenización
Antes de que un modelo como BERT pueda «leer», el texto debe convertirse en números. El tokenizer realiza esta tarea separando el texto en tokens (que son palabras o sub-palabras) y agregando estos tokens especiales:
- [CLS]: Token de inicio de secuencia.
- [SEP]: Token separador que permite al modelo diferenciar entre la pregunta y el contexto de referencia.
Veamos un ejemplo práctico de cómo se ve esta codificación:
# Ejemplo de tokenización
contexto = 'Yo soy Miguel'
pregunta = '¿cómo me llamo?'
# Codificación plus para agregar tokens especiales
encode = tokenizer.encode_plus(pregunta, contexto, return_tensors='pt')
input_ids = encode['input_ids'].tolist()
tokens = tokenizer.convert_ids_to_tokens(input_ids[0])
# Visualizar la relación entre tokens e IDs
for id, token in zip(input_ids[0], tokens):
print('{:<12} {:>6}'.format(token, id))
Al ejecutar el código anterior obtendremos un resultado similar a este:
[CLS] 4
¿ 1067
cómo 2078
me 1129
llamo 6861
? 1064
[SEP] 5
Yo 1584
soy 2020
Miguel 7793
[SEP] 5
En el anterior código en Python podemos ver que la función encode_plus (línea 6) ha estructurado la entrada. Así, en el resultado obtenido podemos ver qué:
- Primero aparece el token de inicio [CLS] (línea 1)
- Luego aparece la pregunta introducida por el usuario (líneas 2 a 6)
- Luego el token de separación [SEP] (línea 7)
- Luego la respuesta (líneas 8 a 10)
- Y finalmente otro token de separación (línea 11)
Además, podemos ver que cada palabra tiene una representación numérica única. Dicha representación numérica es lo que realmente procesa BERT.
Inferencia con pipelines
Para ejecutar el modelo de forma sencilla, y sin demasiadas líneas de código, la librería Transformers de Hugging Face nos ofrece las «pipelines» a las cuales podemos acceder a través del módulo pipeline importado anteriormente.
Este módulo el pre-procesamiento, la ejecución del modelo y el post-procesamiento de la salida. ¡Y todo en pocas líneas de código!
Veamos cómo implementar una «pipeline» aplicada al ejemplo anterior:
# Crear el pipeline de Question Answering
nlp = pipeline('question-answering', model=model, tokenizer=tokenizer)
# Ejecutar una prueba rápida
salida = nlp({'question': pregunta, 'context': contexto})
print(salida)
En esencia, creamos la instancia de la pipeline (línea 2), especificando la tarea a realizar («question-answering»), la instancia del modelo definida anteriormente y el tokenizer.
Y luego ejecutamos la «pipeline» (línea 5) presentándole la pregunta y el contexto definidos anteriormente.
Al ejecutar el código anterior obtenemos este resultado:
{'score': 0.636, 'start': 7, 'end': 12, 'answer': 'Miguel'}
El resultado nos entrega un score (confianza del modelo), un valor entre 0 y 1 que nos indica qué tan seguro está el modelo de haber encontrado una respuesta a la pregunta. Luego nos entrega las posiciones de los caracteres donde se encuentra la respuesta (campos «start» y «end») y, lo más importante, la respuesta extraída del texto: «Miguel».
Creación de un sistema interactivo de question answering
Así que ya tenemos todo lo necesario para crear un sencillo sistema interactivo de pregunta-respuesta usando BERT.
Para hacer esto, definiremos una función que nos permita interactuar con el modelo mediante un bucle de preguntas y respuestas:
from textwrap import wrap
def pregunta_respuesta(model, contexto, nlp):
# Imprimir contexto formateado para lectura
print('Contexto:')
print('-----------------')
print('\n'.join(wrap(contexto)))
# Bucle de interacción
continuar = True
while continuar:
print('\nPregunta:')
print('-----------------')
pregunta = str(input())
# El bucle se detiene si la pregunta está vacía
continuar = pregunta != ''
if continuar:
salida = nlp({'question': pregunta, 'context': contexto})
print('\nRespuesta:')
print('-----------------')
print(salida['answer'])
Esta función:
- Toma como entrada el modelo BERT, el contexto y la pipeline (línea 3)
- En primer lugar se imprime el contexto usando el módulo «wrap» para facilitar la lectura (líneas 4 a 7)
- Y luego ejecuta un bucle que se repite indefinidamente hasta que el usuario no introduzca ninguna pregunta (líneas 11 a 23). En particular, en este bucle:
- El usuario introduce la pregunta desde el teclado usando la función «input()» de Python (líneas 12 a 14)
- Esta pregunta es llevada a la pipeline (línea 20) y al final se imprime únicamente el campo «answer» que contiene el texto con la respuesta a la pregunta (línea 23).
Prueba del sistema pregunta-respuesta: Terminator 2
Probemos este sistema de pregunta respuesta con un párrafo complejo extraído de Wikipedia sobre la película Terminator 2. Este texto contiene múltiples nombres, fechas y acciones entrelazadas:
contexto = "La película empieza con una escena de la guerra contra las máquinas tras el 'Día del Juicio Final' en el año 2029, donde Sarah Connor (Linda Hamilton) narra los sucesos relatados en la película anterior y de como Skynet, al fracasar su plan, ha decidido enfrentar a John siendo este todavía un niño. A continuación, hay un salto a la actualidad de 1995. Un Terminator T-800 (Schwarzenegger) llega desnudo a través de un portal en el tiempo y entra a una cafetería en las afueras de Los Ángeles para robar la ropa, el arma y la moto a un motociclista. En otra parte de la ciudad, otro individuo desnudo (Robert Patrick), se materializa y asesina a un policía para robar su uniforme y su auto patrulla. Ambos comienzan a buscar a John Connor (Edward Furlong), ahora con 10 años que vive en Los Ángeles con sus padres adoptivos. John es muy rebelde y constantemente desobedece a sus tutores, luego que su madre Sarah fuera arrestada después de intentar hacer explotar una fábrica de computadoras siendo internada en el hospital psiquiátrico de Pescadero, donde es atendida por el Doctor Silberman (Earl Boen)."
pregunta_respuesta(model, contexto, nlp)
Y veamos algunas preguntas presentadas a BERT junto con las correspondientes respuestas y un breve análisis del resultado:
| Pregunta | Respuesta de BERT | Análisis |
| ¿Contra quiénes se enfrentan los humanos? | máquinas | Impresionante, ya que la palabra «humanos» no aparece literalmente en el texto; el modelo infiere el contexto de guerra. |
| ¿Cuándo fue la guerra contra las máquinas? | 2029, | Identifica correctamente la fecha asociada al evento. |
| ¿Qué cosas robó el terminator T-800? | ropa, el arma y la moto | Realiza una extracción precisa de una lista de elementos. |
| ¿Por qué fue arrestada Sarah Connor? | intentar hacer explotar una fábrica de computadoras | Logra conectar la causa y el efecto a través de la estructura gramatical del párrafo. |
Enlace de descarga del código fuente
▼ El código fuente de este tutorial
Conclusiones
Como vimos en este tutorial, BERT demuestra una capacidad asombrosa para comprender la estructura del lenguaje humano en Español.
Aunque el núcleo del modelo es idéntico al BERT original en Inglés, el pre-entrenamiento con datos en Español y la afinación para tareas de pregunta respuesta le permiten interactuar de una forma extremadamente natural.
Esto es solo un ejemplo del potencial de este tipo de modelos, que abre las puertas a la creación de asistentes inteligentes, buscadores semánticos y herramientas de análisis documental automatizado que pueden «entender» y extraer información relevante de manera similar a como lo haría un ser humano.
