En la entrada anterior motivamos el algoritmo BPE. Ahora hablaremos brevemente de su historia y explicaremos en detalle cómo funciona.
El algoritmo BPE, desarrollado por Philip Gage (1994), es un método clásico en lo que respecta a la compresión de datos. Su idea central es recorrer los datos y encontrar el par de bytes adyacentes más frecuente para, luego, sustituir esas apariciones por un byte libre. A modo de ejemplo, supongamos que tenemos 10 bytes:
| 65 | 66 | 65 | 66 | 65 | 66 | 67 | 68 | 69 | 70 |
| A | B | A | B | A | B | C | D | E | F |
En este caso, la primera fila representa el número del byte escrito en decimal. La segunda corresponde a su interpretación en ASCII. Entonces, acorde con la lógica del algoritmo, debemos recorrer esa tabla y encontrar el par de bytes más frecuente. En nuestro caso, sería:
| Carácter | Frecuencia |
| AB | 3 |
| BA | 2 |
| BC | 1 |
| CD | 1 |
| DE | 1 |
| EF | 1 |
Según la tabla, el par más frecuente es AB. A partir de aquí, debemos elegir un «byte libre». De antemano, sabemos que los bytes que van desde 65 a 70 están ocupados, y que los que van desde 0 a 64, y de 71 a 255, están libres. Para efectos de este ejemplo, utilizaremos el byte 80 como el «byte libre» que denotará el par «AB». Dicho lo anterior, podemos escribir la siguiente tabla:
| 80 | 80 | 80 | 67 | 68 | 69 | 70 |
| AB | AB | AB | C | D | E | F |
Vale la pena destacar que esta matriz ya no tiene la misma interpretación que la anterior. En este caso, 80 representa «AB» en el algoritmo, pero eso no implica que su interpretación en ASCII sea «AB» (de hecho, en ASCII es la letra P). Dicho esto, como podemos ver, un archivo que antes tenía 10 bytes pudo reducirse a 7 (y podría reducirse aún más). En la siguiente sección, veremos cómo este algoritmo ha sido adaptado para llevar a cabo una segmentación en subwords.
Adaptando BPE para construir subwords (Senrich et al., 2016).
Senrich et al. (2016), tomaron el concepto desarrollado por Gage (1994), y lo extendieron para tokenizar en modelos modernos de lenguaje.
Este algoritmo consta de dos partes: un entrenador y un codificador. En la etapa de entrenamiento, debemos tomar un texto crudo para inducir un conjunto de tokens. Luego, en la etapa de codificación, empleamos una sentencia de prueba para tokenizarla, utilizando las fusiones de la etapa previa en el orden en que fueron aprendidas. Dicho esto, empezaremos explicando la fase de entrenamiento.
Etapa de entrenamiento…
Su funcionamiento es muy similar al algoritmo propuesto por Gage (1994). Iterativamente, se fusionan tokens aledaños frecuentes para crear nuevos, cuyas cadenas de texto sean cada vez más largas. Para ilustrar la mecánica subyacente, tomaremos el ejemplo del libro que estamos usando como referencia (Jurafsky y James H. Martin, 2025). Supongamos que tenemos un corpus de 10 caracteres de largo, y que nuestro vocabulario es de 5 caracteres, es decir, A,B,C,D,E:
| A | B | D | C | A | B | E | C | A | B |
Esto nos entrega la siguiente tabla de frecuencias,
| Par | Frecuencia |
| AB | 3 |
| CA | 2 |
| DC | 1 |
| BD | 1 |
| BE | 1 |
| EC | 1 |
De aquí, deducimos que el par más frecuente es «AB». Por tanto, debemos fusionar los pares adyacentes «A | B» en «AB», lo que genera un nuevo corpus de 7 tokens, cuya estructura se muestra en la siguiente tabla:
| AB | D | C | AB | E | C | AB |
Y constará de un vocabulario de 6 tokens: A, B, C, D, E y AB. Ahora, repitiendo el proceso, el par más frecuente es «C | AB», lo que convierte nuestro corpus en:
| AB | D | CAB | E | CAB |
Y el vocabulario asociado, en el siguiente conjunto: A, B, C, D, E, AB y CAB. El algoritmo continúa hasta generar k fusiones, siendo k un número definido exógenamente.
En este ejemplo, utilizamos una cadena de texto con una palabra de 10 caracteres. No obstante, en la práctica, un corpus está compuesto por más de una palabra, que, generalmente, están separadas por un espacio en blanco. Entonces, ¿cómo lidiamos con más de una palabra? Veámoslo con un ejemplo:
Supongamos que tenemos el siguiente texto, donde «_» representa un espacio en blanco:
set_new_new_renew_reset_renew
Al igual que en el ejemplo anterior, podemos separar los caracteres de la siguiente forma:
| s | e | t | _ | n | e |
| w | _ | n | e | w | _ |
| r | e | n | e | w | _ |
| r | e | s | e | t | _ |
| r | e | n | e | w |
y caracterizar el vocabulario como: _, e, n, r, s, t y w. En este caso, tal como lo hemos hecho anteriormente, debemos contar los pares más frecuentes del texto original. En particular, el par «ne» es el más frecuente (se repite 4 veces). Entonces, la nueva tabla puede reescribirse como:
| s | e | t | _ | ne | w |
| _ | ne | w | _ | r | e |
| ne | w | _ | r | e | s |
| e | t | _ | r | e | ne |
| w |
lo que genera el siguiente vocabulario: _, e, n, r, s, t, w, ne. Repitiendo el proceso,
| s | e | t | _ | ne | w |
| _ | ne | w | _ | r | e |
| ne | w | _ | r | e | s |
| e | t | _ | r | e | ne |
| w |
el par más frecuente es «ne w». Por tanto, la nueva tabla viene dada por:
| s | e | t | _ | new | _ |
| new | _ | r | e | new | _ |
| r | e | s | e | t | _ |
| r | e | new |
y el nuevo vocabulario por: _, e, n, r, s, t, w, ne, new. Si repetimos una vez más este proceso, el par más frecuente es «_r», lo que convierte nuestro corpus en:
| s | e | t | _ | new | _ |
| new | _r | e | new | _r | e |
| s | e | t | _r | e | new |
con el siguiente vocabulario: _, e, n, r, s, t, w, ne, new, _r. De esta forma, podemos seguir hasta generar k uniones.
Etapa de codificación…
En esta etapa convertimos un texto nuevo en una secuencia de tokens usando las fusiones que hemos aprendido, en el mismo orden que fueron aprendidas, y de forma greedy (si se puede fusionar, fusiona). A modo de ejemplo, supongamos que este fue el orden de aprendizaje en la etapa de entrenamiento:
| 1) n + e | ne |
| 2) ne + w | new |
| 3) _ + r | _r |
| 4) _r + e | _re |
Además, supondremos que nuestro texto de prueba sólo contiene la palabra «new», es decir,
| n | e | w |
Luego, como tenemos una fusión «n+e = ne», el resultado sería:
| ne | w |
y finalmente, ne + w = new. Vale la pena destacar que debido a la trivialidad del ejemplo, el orden de prioridad no juega un rol importante en el resultado. Para que veas que el orden puede alterarlo, te dejo el siguiente ejercicio: dado estos dos rankings,
| 1) n + e | ne | 1) e +w | ew |
| 2) ne + w | new | 2) n + e | ne |
| 3) ne+w | new |
trata de codificar la palabra «new». Hint: para el primer ranking, terminarás con [new], y para el segundo terminarás con [n,ew].
Notas finales, y próxima entrada…
En esta entrada vimos como funciona el algoritmo BPE en el contexto de la construcción de subwords, y en el artículo en que se basaron los autores para idearlo. En la próxima entrada veremos como funciona este procedimiento en la práctica! Stay tuned!
José Miguel Muñoz Urra – jmunozu@pulki.es












