Neural Machine Translation with Subword Units: BPE y gestión de vocabulario
Análisis del algoritmo Byte Pair Encoding (BPE). Se examina la reducción del problema de palabras fuera de vocabulario (OOV), la optimización del tamaño de la matriz de embeddings y el balance entre eficiencia computacional y representatividad morfológica.
En el estado actual del arte de la Traducción Automática Neuronal (NMT), típicamente basado en arquitecturas Sequence-to-Sequence (Encoder-Decoder con mecanismos de atención), el manejo del vocabulario representa un cuello de botella crítico.
Los modelos tradicionales operan a nivel de palabra, limitando el vocabulario a los $K$ términos más frecuentes (usualmente 30,000 a 50,000). Esto obliga a mapear cualquier palabra no vista a un token genérico <UNK>, degradando severamente la calidad de la traducción y perdiendo información semántica. Por otro lado, los modelos a nivel de carácter (Character-level CNNs/RNNs) resuelven la cobertura pero incrementan drásticamente la longitud de las secuencias, elevando el costo computacional y dificultando el entrenamiento de LSTMs profundas debido al vanishing gradient problem.
La técnica de Byte Pair Encoding (BPE), adaptada recientemente por Sennrich et al. (ACL 2016), propone una solución intermedia: segmentar palabras en subunidades de frecuencia estadística. Esto permite a la red neuronal inferir el significado de palabras compuestas o declinaciones desconocidas a partir de sus morfemas constituyentes, manteniendo un tamaño de vocabulario denso y manejable.
Fundamentos matemáticos
BPE es un algoritmo iterativo de compresión de datos que reemplaza los pares de bytes más frecuentes por un byte no utilizado. En NLP, esto se abstrae a caracteres y n-gramas.
Sea $V$ el vocabulario inicial compuesto por el conjunto de caracteres presentes en el corpus $C$. Se define una operación de fusión (merge) que une dos símbolos adyacentes $(A, B)$ para formar un nuevo símbolo $AB$.
El objetivo es maximizar la frecuencia de los pares fusionados. En cada iteración $i$, seleccionamos el par $(x, y)$ tal que:
$$(x^*, y^*) = \operatorname*{argmax}_{(x, y) \in V \times V} \operatorname{freq}(x, y)$$
Donde $\operatorname{freq}(x, y)$ es el conteo total de la bigrama $xy$ en el corpus. El nuevo vocabulario se actualiza como:
$$V_{i+1} = V_i \cup \{x^*y^*\}$$
Este proceso se repite $k$ veces (un hiperparámetro definido por el usuario), resultando en un tamaño de vocabulario final $|V| = |V_{initial}| + k$.
La representación vectorial de una palabra $w$ deja de ser una búsqueda única en una tabla de embeddings $E \in \mathbb{R}^{|V| \times d}$. En su lugar, $w$ se descompone en una secuencia de subunidades $S = (s_1, s_2, ..., s_m)$, y la entrada a la red suele ser la secuencia de vectores correspondientes o una composición de los mismos.
Implementación práctica
La implementación de BPE no requiere gradientes; es un pre-procesamiento determinista. A continuación, se presenta una implementación funcional en Python 3 para entrenar y aplicar BPE sobre un corpus crudo.
import re
from collections import defaultdict
def get_stats(vocab):
"""
Computa la frecuencia de pares de símbolos adyacentes.
"""
pairs = defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pairs[symbols[i], symbols[i+1]] += freq
return pairs
def merge_vocab(pair, v_in):
"""
Aplica la fusión del mejor par al vocabulario actual.
"""
v_out = {}
bigram = re.escape(' '.join(pair))
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
for word in v_in:
# Reemplaza 'a b' por 'ab'
w_out = p.sub(''.join(pair), word)
v_out[w_out] = v_in[word]
return v_out
# 1. Preparación del corpus (añadir delimitador de fin de palabra </w>)
# Formato: {'c a r r o </w>': 5, 'c a r r e t a </w>': 2}
raw_corpus = ["low", "lowest", "newer", "wider", "new"]
vocab = defaultdict(int)
for word in raw_corpus:
# Separar caracteres y añadir token especial
vocab[' '.join(list(word)) + ' </w>'] += 1
# 2. Loop de entrenamiento BPE
num_merges = 10
print(f"Vocabulario inicial: {len(vocab)} tokens únicos (nivel carácter)")
for i in range(num_merges):
pairs = get_stats(vocab)
if not pairs:
break
# Encontrar el par más frecuente
best = max(pairs, key=pairs.get)
print(f"Iteración {i+1}: Fusionando {best} -> {pairs[best]} ocurrencias")
vocab = merge_vocab(best, vocab)
print("\nTokens finales (ejemplo):")
print(list(vocab.keys()))
Esta lógica genera una lista de operaciones de fusión que debe guardarse. Para la inferencia (tokenización de texto nuevo), se aplican estas fusiones en el mismo orden exacto para garantizar consistencia entre el entrenamiento y la predicción.
Análisis de comportamiento
Al desplegar BPE en sistemas de producción NMT (como OpenNMT o arquitecturas TensorFlow Seq2Seq), se observan los siguientes comportamientos:
- Compresión de Vocabulario: Es posible representar un corpus de millones de palabras con un vocabulario fijo de ~32,000 subpalabras. Esto reduce el tamaño de la capa de salida (Softmax), que es computacionalmente la parte más costosa durante el entrenamiento y la inferencia.
- Morfología Inducida: BPE tiende a mantener las raíces de las palabras comunes como tokens únicos y segmentar los sufijos/prefijos. Ejemplo: "entrenamiento" podría ser un token único, pero "reentrenamiento" podría tokenizarse como
['re', 'entrenamiento']. Esto permite al modelo generalizar significados composicionales. - Estabilidad en Rare Words: La aparición de tokens
<UNK>se reduce prácticamente a cero. Nombres propios o tecnicismos no vistos durante el entrenamiento se representan mediante sus caracteres constituyentes, permitiendo que el modelo los copie o translitere fonéticamente si es necesario.
Comparativas o referencias técnicas
Comparando BPE contra enfoques tradicionales en tareas de traducción (WMT En-De):
| Enfoque | Tamaño Vocabulario | OOV Rate (Test) | Latencia Inferencia | BLEU Score |
|---|---|---|---|---|
| Word-Level | 50,000 | 2.5% - 5% | Baja | Base |
| Character-Level | ~100 | 0% | Muy Alta (x2-x8) | -1.0 a +1.0 vs Base |
| Subword (BPE) | 30,000 | 0% | Media (similar a Word) | +1.5 a +3.0 vs Base |
El aumento en BLEU score se atribuye casi exclusivamente a la capacidad de traducir correctamente palabras compuestas y declinaciones no frecuentes en el conjunto de entrenamiento. A diferencia de los modelos basados en caracteres, BPE no sacrifica la capacidad de la red LSTM/GRU para capturar dependencias a largo plazo, ya que la longitud de la secuencia no aumenta drásticamente.
Limitaciones y casos donde no conviene usarlo
- Ambigüedad de Segmentación: BPE es un algoritmo codicioso (greedy). No busca la segmentación "lingüísticamente correcta", sino la estadísticamente frecuente. Esto puede generar particiones subóptimas que dificultan el aprendizaje semántico si el corpus de entrenamiento es pequeño.
- Dependencia del Corpus: Un modelo de BPE entrenado en un dominio (ej. noticias médicas) tendrá un rendimiento pobre al segmentar texto de otro dominio (ej. chat informal), produciendo una fragmentación excesiva (muchos caracteres individuales) que diluye el contexto.
- Idiomas no concatenativos: Si bien funciona excelente en inglés o alemán, su eficacia varía en idiomas como el chino o japonés donde la noción de "palabra" y "subpalabra" requiere tokenizadores específicos previos (como Jieba o KyTea) antes de aplicar BPE.