tutoriales.com

Clasificación de Intenciones y Detección de Entidades en Diálogos Conversacionales con BERT

Este tutorial práctico te guiará paso a paso en el desarrollo de un módulo de Comprensión del Lenguaje Natural (NLU) utilizando arquitecturas basadas en Transformers (BERT). Descubrirás cómo entrenar un modelo para clasificar las intenciones del usuario y, simultáneamente, extraer información clave mediante etiquetado de secuencias.

Avanzado12 min de lectura6 views
Reportar error

🎯 Introducción a la Comprensión del Lenguaje Natural (NLU)

La Comprensión del Lenguaje Natural (NLU, por sus siglas en inglés) es el motor invisible detrás de cualquier asistente virtual, chatbot inteligente o sistema conversacional moderno. Cuando un usuario escribe o dice una frase como: Resérvame un vuelo a París para el próximo viernes, el sistema no solo debe entender qué quiere hacer el usuario, sino también cuáles son los detalles específicos de esa solicitud.

Para lograr esto con precisión, los motores NLU modernos descomponen la tarea en dos subtareas fundamentales que ocurren de forma conjunta o secuencial:

  1. Clasificación de Intenciones (Intent Classification): Determinar el objetivo o propósito principal del usuario (ej. reservar_vuelo, consultar_clima, cancelar_suscripción).
  2. Detección de Entidades (Named Entity Recognition - NER): Extraer fragmentos específicos de texto que aportan contexto vital (ej. destino: París, fecha: próximo viernes).

En este tutorial, aprovecharemos la potencia de los modelos basados en Transformers, específicamente BERT (Bidirectional Encoder Representations from Transformers), para construir un clasificador de doble tarea robusto utilizando Python y la biblioteca Hugging Face Transformers.

📌 Nota: Este tutorial asume que tienes conocimientos básicos de Python, aprendizaje automático y conceptos fundamentales de redes neuronales y procesamiento de texto.

🛠️ Requisitos Previos y Entorno de Trabajo

Antes de escribir nuestro código para el procesamiento de lenguaje natural, necesitamos configurar nuestro entorno de desarrollo e instalar las dependencias necesarias. Utilizaremos PyTorch como backend de aprendizaje profundo y Hugging Face para acceder a los modelos preentrenados.

Abre tu terminal e instala las siguientes bibliotecas ejecutando el comando correspondiente:

pip install torch torchvision torchaudio
pip install transformers datasets scikit-learn pandas

Verifiquemos que todo esté instalado correctamente creando un script de prueba o ejecutando un entorno interactivo de Python para comprobar la versión de PyTorch y Transformers:

import torch
import transformers

print(f"Versión de PyTorch: {torch.__version__}")
print(f"Versión de Transformers: {transformers.__version__}")
print(f"¿GPU disponible?: {torch.cuda.is_available()}")
💡 Consejo: Si dispones de una tarjeta gráfica NVIDIA compatible con CUDA, el entrenamiento del modelo BERT será significativamente más rápido. Si no, puedes utilizar entornos gratuitos en la nube como Google Colab.

📊 Arquitectura del Sistema Dual: Intenciones y Entidades

Para entender cómo abordaremos el problema, visualicemos el flujo de datos a través de nuestro modelo basado en BERT. A diferencia de las tareas aisladas, un sistema NLU avanzado procesa el texto de entrada token por token para generar ambas predicciones.

Clasificador de Intención Detección de Entidades (NER por Token) Codificador BERT [CLS] T1 T2 T3 "Reservar un vuelo a Madrid" [CLS] Reservar un vuelo...

En la arquitectura anterior, observamos dos salidas principales:

  • La salida asociada al token especial [CLS] se utiliza para la clasificación de la intención global de la oración.
  • Las salidas asociadas a cada token individual de la palabra se utilizan para asignar etiquetas de entidades utilizando el esquema de etiquetado BIO (Beginning, Inside, Outside).

📝 Preparación y Formato del Dataset

Para entrenar nuestro modelo de NLU de doble tarea, necesitamos un conjunto de datos etiquetado adecuadamente. Crearemos un pequeño dataset simulado en formato tabular que contenga la oración del usuario, la intención global y las etiquetas de entidades para cada palabra.

Vamos a escribir un script en Python para cargar y estructurar nuestros datos utilizando la biblioteca Pandas:

import pandas as pd

data = {
    "text": [
        "Quiero reservar un vuelo de Madrid a Barcelona para mañana",
        "¿Cuál es el pronóstico del tiempo en Buenos Aires hoy?",
        "Cancela mi suscripción al plan premium por favor"
    ],
    "intent": [
        "reservar_vuelo",
        "consultar_clima",
        "cancelar_suscripción"
    ],
    "entities": [
        ["O", "O", "O", "O", "O", "B-origen", "O", "B-destino", "O", "B-fecha"],
        ["O", "O", "O", "O", "O", "O", "O", "B-ubicacion", "B-ubicacion", "B-fecha"],
        ["O", "O", "B-servicio", "O", "B-plan", "O", "O"]
    ]
}

df = pd.DataFrame(data)
print(df.head())

Convención de Etiquetas BIO

El formato BIO es un estándar en la extracción de información:

  • B- (Beginning): Indica el comienzo de una entidad específica.
  • I- (Inside): Indica que el token continúa dentro de la misma entidad actual.
  • O (Outside): Indica que el token no pertenece a ninguna entidad de interés.

🔠 Tokenización y Alineación de Etiquetas con Hugging Face

Uno de los mayores desafíos al implementar NER con BERT es que el tokenizador de BERT (WordPiece) divide las palabras en subpalabras (subwords). Esto significa que una sola palabra de nuestro dataset original puede convertirse en múltiples tokens, desalineando nuestras etiquetas originales.

Vamos a escribir una función robusta para tokenizar el texto y alinear correctamente las etiquetas de las entidades con los nuevos tokens generados:

from transformers import AutoTokenizer

MODEL_NAME = "dccuchile/bert-base-spanish-wwm-cased"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)

def tokenize_and_align_labels(tokenizer, texts, entities_list, label2id):
    tokenized_inputs = tokenizer(texts, padding=True, truncation=True, return_offsets_mapping=True)
    
    all_labels = []
    for i, entities in enumerate(entities_list):
        word_ids = tokenized_inputs.word_ids(batch_index=i)
        previous_word_idx = None
        label_ids = []
        
        for word_idx in word_ids:
            if word_idx is None:
                label_ids. Além de esto, ignoramos tokens especiales como [CLS] o [SEP]
                label_ids.append(-100)
            elif word_idx != previous_word_idx:
                # Asignamos la etiqueta correspondiente al primer subtoken de la palabra
                label_ids.append(label2id.get(entities[word_idx], 0))
            else:
                # Para subtokens subsiguientes, podemos asignar -100 para ignorarlos en la pérdida
                label_ids.append(-100)
            previous_word_idx = word_idx
        all_labels.append(label_ids)
        
    tokenized_inputs["labels"] = all_labels
    return tokenized_inputs
⚠️ Advertencia: En PyTorch, el valor especial -100 se ignora automáticamente al calcular la función de pérdida (*Cross Entropy Loss*), lo cual es ideal para evitar penalizar al modelo por los tokens especiales o subtokens añadidos por el tokenizador.

🏗️ Construcción del Modelo de Doble Tarea con PyTorch

Para resolver ambas tareas de manera simultánea, crearemos una clase personalizada utilizando torch.nn.Module que tome como base el codificador preentrenado de BERT y añada dos cabezales (heads) independientes: uno lineal para la clasificación de intenciones y otro para el etiquetado de tokens (NER).

import torch.nn as nn
from transformers import BertModel, BertPreTrainedModel

class BertNLUModel(BertPreTrainedModel):
    def __init__(self, config, num_intents, num_entities):
        super().__init__(config)
        self.num_intents = num_intents
        self.num_entities = num_entities
        
        # Codificador base BERT
        self.bert = BertModel(config)
        
        # Cabezal para Clasificación de Intenciones
        self.intent_classifier = nn.Linear(config.hidden_size, num_intents)
        
        # Cabezal para Extracción de Entidades (NER)
        self.entity_classifier = nn.Linear(config.hidden_size, num_entities)
        
        self.init_weights()
        
    def forward(self, input_ids, attention_mask=None, token_type_ids=None, intent_labels=None, entity_labels=None):
        outputs = self.bert(
            input_ids,
            attention_mask=attention_mask,
            token_type_ids=token_type_ids
        )
        
        sequence_output = outputs[0]  # Representación de todos los tokens [batch_size, seq_len, hidden_size]
        pooled_output = outputs[1]    # Representación del token [CLS] [batch_size, hidden_size]
        
        # Predicciones
        intent_logits = self.intent_classifier(pooled_output)
        entity_logits = self.entity_classifier(sequence_output)
        
        total_loss = None
        if intent_labels is not None and entity_labels is not None:
            loss_fct = nn.CrossEntropyLoss()
            
            # Pérdida de la intención
            intent_loss = loss_fct(intent_logits, intent_labels)
            
            # Pérdida de las entidades (ignorando etiquetas -100)
            active_loss = attention_mask.view(-1) == 1
            active_entity_logits = entity_logits.view(-1, self.num_entities)
            active_entity_labels = entity_labels.view(-1)
            
            entity_loss = loss_fct(active_entity_logits, active_entity_labels)
            
            # Pérdida total combinada
            total_loss = intent_loss + entity_loss
            
        return {
            "loss": total_loss,
            "intent_logits": intent_logits,
            "entity_logits": entity_logits
        }

🚀 Entrenamiento y Optimización del Modelo

Una vez definida nuestra arquitectura de doble tarea y preparado nuestro pipeline de datos, estamos listos para configurar el bucle de entrenamiento. Utilizaremos un optimizador estándar como AdamW y ajustaremos los hiperparámetros básicos.

Progreso del Tutorial: 60%

Aquí tienes un ejemplo de cómo configurar el proceso de entrenamiento en PyTorch:

from torch.utils.data import DataLoader, TensorDataset
import torch.optim as optim

# Supongamos que ya tenemos nuestros tensores preparados:
# input_ids, attention_mask, intent_labels, entity_labels

# Ejemplo de optimizador
# optimizer = AdamW(model.parameters(), lr=5e-5)

print("Configuración de entrenamiento lista para ejecutar iteraciones de optimización.")
🔥 Importante: Al entrenar modelos basados en Transformers, asegúrate de utilizar tasas de aprendizaje pequeñas (típicamente entre 2e-5 y 5e-5) para evitar destruir el conocimiento previo almacenado en los pesos del modelo preentrenado.

🧪 Inferencia: Probando nuestro Sistema NLU

Una vez entrenado nuestro modelo, el verdadero valor reside en su capacidad para procesar nuevas oraciones en tiempo real y extraer tanto la intención como las entidades de forma unificada.

Veamos cómo estructurar una función de inferencia para un nuevo texto:

def predict_nlu(text, model, tokenizer, id2intent, id2entity):
    model.eval()
    inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
    
    with torch.no_grad():
        outputs = model(**inputs)
        
    # Obtener la intención predicha
    intent_preds = torch.argmax(outputs["intent_logits"], dim=1)
    predicted_intent = id2intent[intent_preds.item()]
    
    # Obtener las entidades predichas
    entity_preds = torch.argmax(outputs["entity_logits"], dim=2)
    tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
    
    extracted_entities = []
    for token, pred_id in zip(tokens, entity_preds[0]):
        if token not in ["[CLS]", "[SEP]", "[PAD]"]:
            entity_name = id2entity[pred_id.item()]
            extracted_entities.append((token, entity_name))
            
    return {
        "text": text,
        "intent": predicted_intent,
        "entities": extracted_entities
    }

💡 Buenas Prácticas y Consejos Avanzados

Para llevar tu sistema NLU al siguiente nivel de producción, ten en cuenta las siguientes recomendaciones:

  • Aumento de Datos (Data Augmentation): Utiliza técnicas de traducción cruzada (traducir a otro idioma y volver al español) para enriquecer tu dataset con variaciones sintácticas.
  • Congelamiento de Capas: Si tus recursos computacionales son limitados, puedes congelar los primeros bloques de BERT y entrenar únicamente las capas superiores y los cabezales de tarea.
  • Evaluación Continua: Monitoreá métricas separadas: Accuracy para la clasificación de intenciones y F1-Score ponderado para la detección de entidades.

❓ Preguntas Frecuentes (FAQ)

¿Puedo entrenar la intención y las entidades por separado en lugar de hacerlo en multitarea? Sí, es completamente posible y a veces más fácil de depurar. Sin embargo, el enfoque multitarea (*multi-task learning*) permite que BERT aproveche representaciones compartilhadas, mejorando la generalización general del sistema conversacional.
¿Qué tamaño de dataset necesito para obtener resultados aceptables? Para tareas acotadas de dominios específicos (como reservas de vuelos o consultas bancarias), puedes empezar a ver buenos resultados con tan solo 500 a 1000 ejemplos debidamente etiquetados por intención.

🎓 Conclusión

¡Felicidades! Has aprendido los fundamentos teóricos y prácticos para construir un sistema de Comprensión del Lenguaje Natural (NLU) avanzado utilizando BERT. Al combinar la clasificación de intenciones y la detección de entidades en una arquitectura unificada, tus asistentes virtuales podrán interpretar las peticiones de los usuarios con un nivel de precisión profesional.

Continúa experimentando con diferentes hiperparámetros, ajusta tus datasets y despliega tu propio modelo conversacional inteligente.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!