🏭 Caso de Uso

T5 (MT5) — Traducción ES→EN y mapas de atención

Estudio de la arquitectura encoder-decoder T5 aplicada a traducción: extracción y visualización de encoder self-attention, decoder self-attention y cross-attention.

🐍 Python 📓 Jupyter Notebook

Traducción Español ↔ Inglés con un Transformer tipo T5: estudio de mapas de atención

Este notebook es una guía práctica y didáctica para entender cómo un Transformer encoder-decoder (familia T5) realiza traducción automática y, sobre todo, para inspeccionar e interpretar sus mapas de atención.

Objetivos de aprendizaje

Al finalizar, deberías poder:

  1. Entender el flujo completo de un modelo tipo T5 en traducción: tokenización → encoder → decoder → generación.
  2. Relacionar la teoría del submódulo de Transformers con un caso real:
    • self-attention en encoder,
    • masked self-attention en decoder,
    • cross-attention (decoder mirando al encoder).
  3. Leer mapas de atención por capas/cabezas y extraer conclusiones lingüísticas (alineación aproximada entre palabras).
  4. Diseñar pequeñas pruebas para validar comportamiento y limitaciones.

Modelo y datasets que vamos a usar

  • Modelo principal: google/mt5-small (familia T5, arquitectura encoder-decoder, enfoque text-to-text).
  • Tokenizer: AutoTokenizer de Hugging Face.
  • Datos de ejemplo (didácticos):
    • Un mini-conjunto de frases en español creado en el propio notebook para analizar fenómenos concretos (negación, orden de palabras, expresiones temporales, etc.).
    • No reentrenaremos el modelo: usaremos pesos preentrenados.

Nota pedagógica: T5/MT5 trata todas las tareas como transformación de texto a texto. En traducción, esto se expresa como un prompt del tipo: translate Spanish to English: ....


Fundamento matemático (visión compacta y rigurosa)

En cada bloque de atención, dados embeddings de dimensión $d_{model}$, se proyecta a:

$$Q = XW_Q,\quad K = XW_K,\quad V = XW_V$$

La atención escalada se calcula como:

$$\text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

En multi-head attention, esto se repite en varias cabezas y luego se concatena:

$$\text{MHA}(X) = \text{Concat}(head_1,\dots,head_h)W_O$$

En un Transformer encoder-decoder como T5:

  • Encoder self-attention: cada token fuente atiende a todos los tokens fuente.
  • Decoder self-attention (enmascarada): cada token generado atiende solo al pasado.
  • Cross-attention: cada token del decoder consulta representaciones del encoder para “mirar” la frase original.

La distribución de atención de cada cabeza es una matriz (aprox. estocástica por filas) que podemos visualizar como un heatmap. Aunque atención no equivale exactamente a explicación causal, sí da pistas muy valiosas sobre alineación y dependencias contextuales.


Plan del notebook

  1. Preparación del entorno y utilidades.
  2. Carga del modelo MT5.
  3. Traducción de frases de ejemplo.
  4. Extracción de atenciones (encoder, decoder, cross).
  5. Visualización de mapas de atención por capa/cabeza.
  6. Mini-experimentos y tests didácticos.
  7. Conclusiones y siguientes pasos.
[1]

# Si trabajas en un entorno limpio, descomenta la línea siguiente:
# !pip install -q transformers sentencepiece torch seaborn matplotlib pandas

import numpy as np
import pandas as pd
import torch
import seaborn as sns
import matplotlib.pyplot as plt

from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, set_seed

# Configuración visual y de reproducibilidad
sns.set_theme(style="whitegrid", context="notebook")
set_seed(42)
np.random.seed(42)
torch.manual_seed(42)

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Usando dispositivo: {DEVICE}")
/home/nuberu/xuan/naux/.venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
Usando dispositivo: cuda

1) Cargar modelo tipo T5 (MT5 pequeño)

Elegimos google/mt5-small porque:

  • mantiene la lógica T5 (text-to-text),
  • es razonablemente ligero para un notebook,
  • es multilingüe, por lo que podemos probar español → inglés.

Usaremos AutoModelForSeq2SeqLM, que incluye encoder y decoder para generación.

[2]

MODEL_NAME = "google/mt5-small"

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME).to(DEVICE)
model.eval()

print("Modelo y tokenizer cargados correctamente.")
print(f"Vocabulario tokenizer: {tokenizer.vocab_size} tokens")
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
Loading weights: 100%|██████████| 192/192 [00:00<00:00, 61586.60it/s]
The tied weights mapping and config for this model specifies to tie shared.weight to encoder.embed_tokens.weight, but both are present in the checkpoints, so we will NOT tie them. You should update the config with `tie_word_embeddings=False` to silence this warning
The tied weights mapping and config for this model specifies to tie shared.weight to decoder.embed_tokens.weight, but both are present in the checkpoints, so we will NOT tie them. You should update the config with `tie_word_embeddings=False` to silence this warning
The tied weights mapping and config for this model specifies to tie shared.weight to lm_head.weight, but both are present in the checkpoints, so we will NOT tie them. You should update the config with `tie_word_embeddings=False` to silence this warning
Modelo y tokenizer cargados correctamente.
Vocabulario tokenizer: 250100 tokens

2) Frases de ejemplo y función de traducción

Definimos un pequeño banco de frases para cubrir casos variados:

  • frases simples,
  • tiempo pasado/futuro,
  • negación,
  • estructuras con subordinadas.

La plantilla del prompt seguirá el formato de traducción del paradigma text-to-text.

[3]

# Banco de frases en español para análisis didáctico
sample_sentences_es = [
    "La inteligencia artificial está transformando la medicina.",
    "Ayer no pude ir al trabajo porque llovía mucho.",
    "Si estudias con constancia, aprenderás más rápido.",
    "El gato duerme debajo de la mesa mientras yo leo.",
    "¿Podrías explicarme cómo funciona la atención en Transformers?",
]


def build_translation_prompt(spanish_text: str) -> str:
    """Construye el prompt text-to-text para traducción es->en."""
    return f"translate Spanish to English: {spanish_text}"


@torch.no_grad()
def translate_es_en(spanish_text: str, max_new_tokens: int = 64, num_beams: int = 4):
    """Genera traducción con beam search y devuelve texto + tensores de entrada/salida."""
    prompt = build_translation_prompt(spanish_text)
    inputs = tokenizer(prompt, return_tensors="pt").to(DEVICE)

    generated_ids = model.generate(
        **inputs,
        max_new_tokens=max_new_tokens,
        num_beams=num_beams,
        early_stopping=True
    )

    translation = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
    return translation, inputs, generated_ids


rows = []
for s in sample_sentences_es:
    tr, _, _ = translate_es_en(s)
    rows.append({"es": s, "en_pred": tr})

pd.DataFrame(rows)
es en_pred
0 La inteligencia artificial está transformando ... <extra_id_0>
1 Ayer no pude ir al trabajo porque llovía mucho. <extra_id_0>.
2 Si estudias con constancia, aprenderás más ráp... <extra_id_0>
3 El gato duerme debajo de la mesa mientras yo leo. <extra_id_0>
4 ¿Podrías explicarme cómo funciona la atención ... <extra_id_0>.

3) Obtener atenciones del modelo

Para estudiar mapas de atención necesitamos ejecutar model(...) con:

  • output_attentions=True
  • return_dict=True

y proporcionar también decoder_input_ids para que el modelo calcule:

  • encoder_attentions
  • decoder_attentions
  • cross_attentions

Trabajaremos con una frase concreta para no saturar visualmente.

[4]

analysis_sentence = "La inteligencia artificial está transformando la medicina."
prompt = build_translation_prompt(analysis_sentence)

# Tokenizamos la entrada (fuente)
enc_inputs = tokenizer(prompt, return_tensors="pt").to(DEVICE)

# Generamos traducción para obtener la secuencia objetivo predicha
with torch.no_grad():
    generated_ids = model.generate(**enc_inputs, max_new_tokens=64, num_beams=4)

translation_text = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
print("Entrada (ES):", analysis_sentence)
print("Traducción predicha:", translation_text)

# Para inspección de atención, usamos decoder_input_ids como la salida generada sin el último token
# (teacher forcing parcial para obtener distribuciones por cada paso del decoder)
decoder_input_ids = generated_ids[:, :-1]

with torch.no_grad():
    outputs = model(
        input_ids=enc_inputs["input_ids"],
        attention_mask=enc_inputs["attention_mask"],
        decoder_input_ids=decoder_input_ids,
        output_attentions=True,
        return_dict=True
    )

# Inspección rápida de estructuras
print("# capas encoder attn:", len(outputs.encoder_attentions))
print("# capas decoder self-attn:", len(outputs.decoder_attentions))
print("# capas cross-attn:", len(outputs.cross_attentions))
print("Shape ejemplo encoder_attn capa 0:", tuple(outputs.encoder_attentions[0].shape))
print("Shape ejemplo cross_attn capa 0:", tuple(outputs.cross_attentions[0].shape))
Entrada (ES): La inteligencia artificial está transformando la medicina.
Traducción predicha: <extra_id_0>
# capas encoder attn: 8
# capas decoder self-attn: 8
# capas cross-attn: 8
Shape ejemplo encoder_attn capa 0: (1, 6, 17, 17)
Shape ejemplo cross_attn capa 0: (1, 6, 2, 17)

Test rápido de consistencia de formas

Comprobamos que las dimensiones siguen el patrón esperado:

  • Encoder self-attention: (batch, heads, src_len, src_len)
  • Decoder self-attention: (batch, heads, tgt_len, tgt_len)
  • Cross-attention: (batch, heads, tgt_len, src_len)
[5]

# Extraemos longitudes útiles
src_len = enc_inputs["input_ids"].shape[1]
tgt_len = decoder_input_ids.shape[1]

enc_shape = outputs.encoder_attentions[0].shape
dec_shape = outputs.decoder_attentions[0].shape
cross_shape = outputs.cross_attentions[0].shape

assert enc_shape[2] == src_len and enc_shape[3] == src_len, "Forma inesperada en encoder attention"
assert dec_shape[2] == tgt_len and dec_shape[3] == tgt_len, "Forma inesperada en decoder self-attention"
assert cross_shape[2] == tgt_len and cross_shape[3] == src_len, "Forma inesperada en cross-attention"

print("✅ Shapes de atención validadas correctamente.")
✅ Shapes de atención validadas correctamente.

4) Funciones de visualización de heatmaps

Crearemos utilidades para:

  • seleccionar una capa y una cabeza,
  • convertir tokens ids a tokens legibles,
  • pintar la matriz de atención con seaborn.heatmap.
[6]

def ids_to_tokens(ids_tensor):
    """Convierte ids (1D tensor) a lista de tokens."""
    return tokenizer.convert_ids_to_tokens(ids_tensor.tolist())


def plot_attention_map(attn_matrix, x_tokens, y_tokens, title, figsize=(11, 7), cmap="magma"):
    """Dibuja heatmap de una matriz de atención 2D."""
    plt.figure(figsize=figsize)
    sns.heatmap(attn_matrix, xticklabels=x_tokens, yticklabels=y_tokens, cmap=cmap)
    plt.xticks(rotation=75, ha="right", fontsize=9)
    plt.yticks(fontsize=9)
    plt.title(title)
    plt.xlabel("Tokens clave/valor")
    plt.ylabel("Tokens consulta")
    plt.tight_layout()
    plt.show()


# Tokens fuente y objetivo
src_tokens = ids_to_tokens(enc_inputs["input_ids"][0].cpu())
tgt_tokens = ids_to_tokens(decoder_input_ids[0].cpu())

print("Tokens fuente (encoder):")
print(src_tokens)
print("\nTokens objetivo parciales (decoder):")
print(tgt_tokens)
Tokens fuente (encoder):
['▁translate', '▁', 'Spanish', '▁to', '▁English', ':', '▁La', '▁inteligenc', 'ia', '▁artificial', '▁está', '▁transform', 'ando', '▁la', '▁medicina', '.', '</s>']

Tokens objetivo parciales (decoder):
['<pad>', '▁<extra_id_0>']

5) Visualizar encoder self-attention

[7]

layer_idx = 0
head_idx = 0

# Tensor: (batch, heads, src_len, src_len)
enc_attn = outputs.encoder_attentions[layer_idx][0, head_idx].detach().cpu().numpy()

plot_attention_map(
    enc_attn,
    x_tokens=src_tokens,
    y_tokens=src_tokens,
    title=f"Encoder self-attention | capa={layer_idx}, cabeza={head_idx}"
)
Output

6) Visualizar decoder self-attention (enmascarada)

[8]

layer_idx = 0
head_idx = 0

# Tensor: (batch, heads, tgt_len, tgt_len)
dec_attn = outputs.decoder_attentions[layer_idx][0, head_idx].detach().cpu().numpy()

plot_attention_map(
    dec_attn,
    x_tokens=tgt_tokens,
    y_tokens=tgt_tokens,
    title=f"Decoder self-attention (masked) | capa={layer_idx}, cabeza={head_idx}",
    cmap="viridis"
)
Output

Observa el patrón aproximadamente triangular: cada posición del decoder atiende más a tokens ya disponibles (pasado) que a futuros, coherente con la máscara causal.

7) Visualizar cross-attention (decoder → encoder)

[9]

layer_idx = 0
head_idx = 0

# Tensor: (batch, heads, tgt_len, src_len)
cross_attn = outputs.cross_attentions[layer_idx][0, head_idx].detach().cpu().numpy()

plot_attention_map(
    cross_attn,
    x_tokens=src_tokens,
    y_tokens=tgt_tokens,
    title=f"Cross-attention | capa={layer_idx}, cabeza={head_idx}",
    cmap="rocket_r"
)
Output

Interpretación guiada

En cross-attention, cada fila (token objetivo) puede verse como una distribución de foco sobre la frase fuente.

  • Si una fila concentra masa en uno o pocos tokens fuente, sugiere alineación léxica relativamente directa.
  • Si reparte masa, puede reflejar fenómenos sintácticos o semánticos de más largo alcance.

Recuerda: la atención es una señal interna útil, no una prueba causal completa de explicación.

8) Comparar capas y cabezas de forma agregada

[10]

def attention_entropy(prob_vector, eps=1e-12):
    """Entropía de Shannon de una distribución discreta."""
    p = np.clip(prob_vector, eps, 1.0)
    return -(p * np.log(p)).sum()


records = []

# Recorremos cross-attention para medir concentración media por cabeza
for layer_i, layer_tensor in enumerate(outputs.cross_attentions):
    # layer_tensor: (batch, heads, tgt_len, src_len)
    mat = layer_tensor[0].detach().cpu().numpy()
    n_heads = mat.shape[0]

    for head_i in range(n_heads):
        # Promedio de entropía por filas (tokens objetivo)
        row_entropies = [attention_entropy(row / row.sum()) for row in mat[head_i]]
        records.append({
            "layer": layer_i,
            "head": head_i,
            "mean_row_entropy": float(np.mean(row_entropies)),
        })

entropy_df = pd.DataFrame(records)
entropy_pivot = entropy_df.pivot(index="layer", columns="head", values="mean_row_entropy")

plt.figure(figsize=(10, 6))
sns.heatmap(entropy_pivot, annot=True, fmt=".2f", cmap="YlGnBu")
plt.title("Entropía media de cross-attention por capa/cabeza\n(menor = atención más concentrada)")
plt.xlabel("Cabeza")
plt.ylabel("Capa")
plt.tight_layout()
plt.show()

entropy_df.head()
Output
layer head mean_row_entropy
0 0 0 1.393663
1 0 1 1.253802
2 0 2 0.013377
3 0 3 1.393034
4 0 4 1.326360

9) Mini-experimento: varias frases y tokens alineados más probables

Para cada token generado, localizamos el token fuente con mayor peso medio de cross-attention (promediando capas y cabezas). Es una aproximación sencilla de alineación.

[11]

@torch.no_grad()
def average_cross_attention_alignment(spanish_text: str, max_new_tokens: int = 64):
    """Devuelve traducción y alineaciones argmax usando cross-attention promediada."""
    prompt = build_translation_prompt(spanish_text)
    inputs = tokenizer(prompt, return_tensors="pt").to(DEVICE)

    gen_ids = model.generate(**inputs, max_new_tokens=max_new_tokens, num_beams=4)
    dec_ids = gen_ids[:, :-1]

    out = model(
        input_ids=inputs["input_ids"],
        attention_mask=inputs["attention_mask"],
        decoder_input_ids=dec_ids,
        output_attentions=True,
        return_dict=True,
    )

    src_toks = ids_to_tokens(inputs["input_ids"][0].cpu())
    tgt_toks = ids_to_tokens(dec_ids[0].cpu())

    # Promedio sobre capas y cabezas: (tgt_len, src_len)
    cross_stack = torch.stack(out.cross_attentions, dim=0)  # (layers, batch, heads, tgt, src)
    avg_cross = cross_stack[:, 0].mean(dim=(0, 1)).cpu().numpy()  # media en layers y heads

    best_src_idx = avg_cross.argmax(axis=1)
    alignments = [(tgt_toks[i], src_toks[j], float(avg_cross[i, j])) for i, j in enumerate(best_src_idx)]

    translation = tokenizer.decode(gen_ids[0], skip_special_tokens=True)
    return translation, src_toks, tgt_toks, avg_cross, alignments


test_sentences = [
    "El profesor explicó el tema con ejemplos sencillos.",
    "Mañana viajaremos a Londres si el clima mejora.",
    "No me gusta llegar tarde a las reuniones.",
]

for sent in test_sentences:
    tr, src_toks, tgt_toks, avg_cross, aligns = average_cross_attention_alignment(sent)
    print("\n" + "=" * 100)
    print("ES:", sent)
    print("EN:", tr)
    print("Alineaciones aproximadas (token_target -> token_source):")
    for tgt_tok, src_tok, score in aligns[: min(len(aligns), 18)]:
        print(f"  {tgt_tok:>12} -> {src_tok:<18} (score={score:.3f})")
====================================================================================================
ES: El profesor explicó el tema con ejemplos sencillos.
EN: <extra_id_0>.
Alineaciones aproximadas (token_target -> token_source):
         <pad> -> ncillo             (score=0.109)
  ▁<extra_id_0> -> </s>               (score=0.719)
             . -> </s>               (score=0.853)

====================================================================================================
ES: Mañana viajaremos a Londres si el clima mejora.
EN: <extra_id_0> a Londres
Alineaciones aproximadas (token_target -> token_source):
         <pad> -> .                  (score=0.096)
  ▁<extra_id_0> -> </s>               (score=0.732)
             ▁ -> </s>               (score=0.753)
             a -> </s>               (score=0.757)
      ▁Londres -> </s>               (score=0.716)

====================================================================================================
ES: No me gusta llegar tarde a las reuniones.
EN: <extra_id_0>.
Alineaciones aproximadas (token_target -> token_source):
         <pad> -> </s>               (score=0.076)
  ▁<extra_id_0> -> </s>               (score=0.721)
             . -> </s>               (score=0.867)

Test numérico: las filas de atención suman ~1

Tras softmax, cada fila de una matriz de atención representa una distribución de probabilidad. En precisión finita puede haber pequeñas desviaciones.

[12]

# Verificamos en la cross-attention promedio del análisis principal
cross_stack = torch.stack(outputs.cross_attentions, dim=0)  # (layers, batch, heads, tgt, src)
avg_cross_main = cross_stack[:, 0].mean(dim=(0, 1)).cpu().numpy()  # (tgt, src)

row_sums = avg_cross_main.sum(axis=1)
max_dev = np.max(np.abs(row_sums - 1.0))

print("Suma por fila (primeros 10):", np.round(row_sums[:10], 6))
print("Desviación máxima respecto a 1.0:", float(max_dev))

assert max_dev < 1e-3, "Las filas no parecen distribuciones válidas (revisar extracción de atención)."
print("✅ Test de normalización de atención superado.")
Suma por fila (primeros 10): [1. 1.]
Desviación máxima respecto a 1.0: 1.1920928955078125e-07
✅ Test de normalización de atención superado.

10) Limitaciones y buenas prácticas de interpretación

  1. Atención ≠ explicación causal completa: útil para intuición, no definitiva.
  2. Una cabeza aislada puede parecer ruidosa; conviene mirar varias capas/cabezas y promedios.
  3. El resultado depende del tokenizador (subpalabras) y del prompt exacto.
  4. Los modelos preentrenados pueden cometer errores de traducción o sesgos contextuales.

Conclusiones y sugerencias para seguir explorando

Conclusiones

  • Hemos aplicado un Transformer tipo T5 (MT5) en traducción español→inglés sin reentrenar.
  • Hemos extraído y visualizado las tres atenciones clave del esquema encoder-decoder.
  • Los mapas de cross-attention permiten observar alineaciones aproximadas entre fuente y destino.
  • La comparación por capas/cabezas (entropía) ayuda a detectar patrones de atención más concentrados o más distribuidos.

Qué podrías probar después

  1. Repetir el análisis en sentido inverso (inglés→español) cambiando el prompt.
  2. Comparar con otro modelo encoder-decoder (por ejemplo, FLAN-T5 o modelos MarianMT).
  3. Medir calidad de traducción con métricas (BLEU/chrF) sobre un conjunto pequeño anotado.
  4. Analizar frases largas con ambigüedad léxica para ver cómo cambia la cross-attention.
  5. Estudiar estabilidad: ¿se repiten los patrones al variar decodificación (num_beams, temperature)?

Si quieres, en una segunda versión podemos convertir este notebook en una mini práctica evaluable con preguntas guiadas y rúbrica.