Ir al contenido
wannacode

El texto en trozos: tokens

En resumen: Un token es un trozo de texto (una palabra, parte de una palabra o un carácter) con el que trabaja un modelo de lenguaje; BPE los aprende uniendo los pares más frecuentes, y el precio y los límites se cuentan en tokens.

Problema

Un diccionario de palabras

El bot de soporte tiene que leer mensajes de clientes. Un modelo trabaja con números, así que un colega arma un diccionario: cada palabra de los textos de la tienda recibe un número. Estos son los textos: respuestas del soporte, avisos de entrega y reseñas.

// el texto de la tienda: respuestas del soporte, avisos de entrega, reseñas
const TEXT = [
"tu pedido se envió hoy con un mensajero",
"el pedido llegará a tu dirección mañana",
"el pedido llegará en tres días",
"el pedido llegó a la oficina de mensajería",
"tu paquete se entregó a un mensajero",
"el paquete llegará en tres días",
"el paquete estará contigo mañana",
"el paquete se retrasó un día",
"un mensajero irá a tu dirección mañana",
"un mensajero te llamará antes de la entrega",
"la entrega a una dirección cuesta 5 euros",
"la entrega a una oficina de mensajería cuesta 3 euros",
"la entrega es gratis desde 50 euros",
"la entrega tarda hasta dos días",
"el producto llegará en su embalaje original",
"puedes devolver el producto en un plazo de 14 días",
"devuelve el producto con su embalaje original",
"la devolución del producto es gratis",
"el dinero se devolverá en cinco días",
"después de devolver el producto recibirás el dinero",
"el dinero vuelve a tu tarjeta",
"puedes pagar con tarjeta o contra reembolso",
"pagar con tarjeta es seguro",
"el banco rechazó el pago con tu tarjeta",
"prueba el pago con otra tarjeta",
"el banco confirmará el pago",
"el producto llegó rápido y es muy bueno",
"estoy muy contento con el pedido",
"el mensajero fue amable y rápido",
"el producto es bueno pero llegó tarde",
];

Después cada mensaje se convierte en una fila de números:

// el diccionario: cada palabra del texto recibe un número
const vocab = [];
for (const line of TEXT)
for (const word of line.split(" "))
if (!vocab.includes(word)) vocab.push(word);
console.log(`diccionario: ${vocab.length} palabras`);
for (const message of ["el paquete llegará en tres días", "mis pedidos llegarán mañana"]) {
const ids = message.split(" ").map((word) => (vocab.includes(word) ? String(vocab.indexOf(word)) : "?"));
console.log(`${message} → ${ids.join(" ")}`);
}
Salida
diccionario: 83 palabras
el paquete llegará en tres días → 8 21 9 13 14 15
mis pedidos llegarán mañana → ? ? ? 12

El primer mensaje pasa. En el segundo, «mis» y «llegarán» no están en el texto, y «pedidos» es otra palabra que «pedido». El modelo recibe «?» en tres lugares de cuatro.

Un diccionario de todas las palabras no funciona. Cada palabra tiene varias formas (pedido, pedidos, pedir, pidió), y cada día llegan nuevas: nombres de productos, números, erratas. Y si lees letra por letra, todo texto pasa, pero se vuelve muy largo: 26 caracteres para seis palabras.

La idea

Trozos entre la letra y la palabra

Entre las letras y las palabras están los trozos frecuentes: «en», «con», «mensaje». Un modelo lee el texto como una fila de esos trozos. Cada trozo se llama token, y cortar el texto en tokens es la tokenización.

Qué trozos se vuelven tokens no lo escribe nadie a mano. Se aprenden del texto con el algoritmo BPE (byte pair encoding):

  1. Cada palabra es una lista de letras.
  2. Cuenta todos los pares de vecinos en todas las palabras. Una palabra que aparece 7 veces cuenta sus pares 7 veces.
  3. El par más frecuente se convierte en un trozo: eso es una regla. Únelo en todas partes.
  4. Repite desde el paso 2, tantas reglas como quieras.

Las reglas se guardan en orden. El texto nuevo se corta con las mismas reglas, en el mismo orden. Una palabra que no está en el texto pasa igual: en el peor caso se queda en letras. No hay «?».

Míralo

60 reglas del texto de la tienda

Una regla es un par de tokens y cuántas veces se vio. Una palabra empieza como letras:

// la palabra como lista de letras
const letters = (word) => [...word];

La unión recorre los tokens de una palabra de izquierda a derecha. Donde left y right están juntos, pone un token en su lugar y se salta los dos:

// une cada left, right que estén juntos
function merge(tokens, left, right) {
const glued = [];
let i = 0;
while (i < tokens.length) {
if (tokens[i] === left && tokens[i + 1] === right) {
glued.push(left + right);
i += 2;
} else {
glued.push(tokens[i]);
i += 1;
}
}
return glued;
}

Aprender son los cuatro pasos de la tarjeta anterior. Map conserva el orden de inserción, así que en un empate gana el par que se vio primero, y la salida es la misma en los cuatro lenguajes.

// aprende count reglas: cada vez une el par más frecuente
function learn(text, count) {
const freq = new Map(); // cuántas veces aparece cada palabra en el texto
for (const line of text)
for (const word of line.split(" ")) freq.set(word, (freq.get(word) ?? 0) + 1);
const split = new Map([...freq.keys()].map((word) => [word, letters(word)])); // por ahora cada palabra son solo letras
const rules = [];
for (let r = 0; r < count; r++) {
const pairs = new Map(); // los pares de vecinos, contados con la frecuencia de la palabra
for (const [word, t] of split)
for (let i = 0; i + 1 < t.length; i++) {
const key = `${t[i]} ${t[i + 1]}`;
pairs.set(key, (pairs.get(key) ?? 0) + freq.get(word));
}
let best = "", most = 0;
for (const [key, n] of pairs)
if (n > most) [best, most] = [key, n]; // el par más frecuente (en caso de empate, el que se vio primero)
const [left, right] = best.split(" ");
rules.push({ left, right, count: most });
for (const [word, t] of split) split.set(word, merge(t, left, right)); // la nueva regla vale para todas las palabras
}
return rules;
}
const rules = learn(TEXT, 60);
rules.forEach((r, k) => {
const cell = `${k + 1}. ${r.left}+${r.right} (${r.count})`;
process.stdout.write(k % 4 === 3 ? cell + "\n" : cell.padEnd(22));
});
Salida
1. e+l (25) 2. e+n (22) 3. r+o (20) 4. a+r (16)
5. e+s (16) 6. c+o (14) 7. j+e (14) 8. e+g (14)
9. d+e (12) 10. d+i (11) 11. co+n (11) 12. t+r (11)
13. l+a (11) 14. n+a (9) 15. p+a (9) 16. t+o (9)
17. d+o (8) 18. c+i (8) 19. í+a (8) 20. u+e (8)
21. t+ar (8) 22. t+a (8) 23. m+en (7) 24. men+s (7)
25. mens+a (7) 26. mensa+je (7) 27. l+l (7) 28. ll+eg (7)
29. r+e (7) 30. p+ro (7) 31. pro+d (7) 32. prod+u (7)
33. produ+c (7) 34. produc+to (7) 35. t+u (6) 36. p+e (6)
37. u+n (6) 38. ar+á (6) 39. d+ía (6) 40. en+tr (6)
41. entr+eg (6) 42. pe+di (5) 43. pedi+do (5) 44. mensaje+ro (5)
45. día+s (5) 46. t+e (5) 47. r+á (5) 48. entreg+a (5)
49. b+a (5) 50. de+v (5) 51. o+l (5) 52. v+e (5)
53. tar+je (5) 54. tarje+ta (5) 55. pa+g (5) 56. s+e (4)
57. lleg+ará (4) 58. re+c (4) 59. ci+ó (4) 60. ció+n (4)

La primera regla es «e» + «l»: el par se vio 25 veces. La segunda une «e» con «n» en «en». Las reglas se apoyan en las anteriores: «m» + «en» (y «en» es la regla 2), luego «mens», «mensa» y «mensaje» son las reglas 23 a 26. Tras la regla 44, «mensajero» es un solo token. Tras la regla 43, «pedido» también lo es.

Pruébalo

Predice la salida

Aquí están las tres primeras reglas de la salida de arriba. ¿Qué imprime el programa?

const rules = [
{ left: "e", right: "l", count: 25 },
{ left: "e", right: "n", count: 22 },
{ left: "r", right: "o", count: 20 },
];
console.log(encode("mensajero", rules).join("|"));
Respuesta

Míralo

El texto nuevo como tokens

Cortar una frase: cada palabra se vuelve letras, y luego viene cada regla en orden. show solo imprime los tokens de cada palabra con | entre ellos.

// una frase → tokens: letras y luego las reglas en orden
function encode(sentence, rules) {
const tokens = [];
for (const word of sentence.split(" ")) {
let t = letters(word);
for (const r of rules) t = merge(t, r.left, r.right);
tokens.push(...t);
}
return tokens;
}
// los tokens de cada palabra, con | entre ellos
const show = (sentence, rules) =>
sentence.split(" ").map((word) => encode(word, rules).join("|")).join(" ");

Así pasa una frase por las reglas. El contador de abajo dice cuántas letras y cuántos tokens hay:

// une cada left, right que estén juntos
function merge(tokens, left, right) {
const glued = [];
let i = 0;
while (i < tokens.length) {
if (tokens[i] === left && tokens[i + 1] === right) {
glued.push(left + right);
i += 2;
} else {
glued.push(tokens[i]);
i += 1;
}
}
return glued;
}
// una frase → tokens: letras y luego las reglas en orden
function encode(sentence, rules) {
const tokens = [];
for (const word of sentence.split(" ")) {
let t = letters(word);
for (const r of rules) t = merge(t, r.left, r.right);
tokens.push(...t);
}
return tokens;
}
Paso a paso, en palabras
  1. Cada palabra empieza como letras: 26 fichas, 26 tokens.
  2. Regla 1: e + l. Donde las dos están juntas, pasan a ser un token, en cada palabra.
  3. Regla 2: e + n. Cada regla recorre toda la frase, en el orden en que se aprendió.
  4. Las reglas 16 a 18 no tienen nada que unir aquí, por eso el número salta a 19.
  5. Regla 57: lleg + ará. «llegará» ya es un token.
  6. Las 60 reglas: 26 letras, 10 tokens. «paquete» queda en 4 partes: esta palabra es rara en el texto.
const rules = learn(TEXT, 60);
const sentences = ["el paquete llegará en tres días", "mis pedidos llegarán mañana", "queja por un televisor roto"];
for (const s of sentences) {
console.log(show(s, rules));
console.log(` letras ${s.replaceAll(" ", "").length}, tokens ${encode(s, rules).length}`);
}
Salida
el pa|q|ue|te llegará en tr|es días
letras 26, tokens 10
m|i|s pedido|s llegará|n m|a|ñ|a|na
letras 24, tokens 12
q|ue|j|a p|o|r un t|el|e|v|i|s|o|r ro|to
letras 23, tokens 18

La frase conocida son 10 tokens en lugar de 26 letras. «paquete» queda en cuatro trozos: está en el texto solo cuatro veces, y las reglas van a pares más frecuentes. En la segunda frase «pedidos» se convierte en «pedido|s» y «llegarán» en «llegará|n»; «mis» queda en letras, pero ninguna palabra es «?».

La tercera frase trata de una queja, y casi todas sus palabras faltan en el texto de la tienda. 23 letras se convierten en 18 tokens: se une muy poco. El texto nuevo se puede leer, pero cuesta más tokens.

El modelo ve los números de los tokens, igual que en el diccionario de palabras: cada token tiene un número en la lista de todos los tokens, y esa lista es el vocabulario del modelo.

Pruébalo

Completa

¿Cuánto avanza i cuando left y right se unen en un solo token?

// une cada left, right que estén juntos
function merge(tokens, left, right) {
  const glued = [];
  let i = 0;
  while (i < tokens.length) {
    if (tokens[i] === left && tokens[i + 1] === right) {
      glued.push(left + right);
      i += ;
    } else {
      glued.push(tokens[i]);
      i += 1;
    }
  }
  return glued;
}

La idea

El precio y el límite van en tokens

Un modelo lee y escribe tokens. Por eso todo lo que se mide de él se mide en tokens:

  • el precio: los proveedores cobran por millón de tokens, por separado la entrada y la respuesta;
  • el límite de entrada: cuánto texto toma el modelo de una vez (esa es la ventana de contexto);
  • el límite de respuesta: cuántos tokens puede escribir antes de parar.

Un colega estima el precio por palabras. Así de lejos queda eso con los mismos tres mensajes, 200 000 mensajes al mes y un precio de ejemplo de 3,00 € por millón de tokens:

const rules = learn(TEXT, 60);
const messages = ["el paquete llegará en tres días", "mis pedidos llegarán mañana", "queja por un televisor roto"];
let words = 0, tokens = 0;
for (const m of messages) {
const w = m.split(" ").length, t = encode(m, rules).length;
words += w;
tokens += t;
console.log(`${m}: ${w} palabras, ${t} tokens`);
}
console.log(`tokens por palabra: ${(tokens / words).toFixed(2)}`);
const price = 3; // € por millón de tokens, un precio de ejemplo
const month = 200000;
console.log(`${month} mensajes al mes, 3.00 € por millón de tokens (un precio de ejemplo):`);
console.log(" por palabras: " + (month * (words / messages.length) * price / 1e6).toFixed(2) + " €");
console.log(" por tokens: " + (month * (tokens / messages.length) * price / 1e6).toFixed(2) + " €");
Salida
el paquete llegará en tres días: 6 palabras, 10 tokens
mis pedidos llegarán mañana: 4 palabras, 12 tokens
queja por un televisor roto: 5 palabras, 18 tokens
tokens por palabra: 2.67
200000 mensajes al mes, 3.00 € por millón de tokens (un precio de ejemplo):
por palabras: 3.00 €
por tokens: 8.00 €

La estimación por palabras es 3,00 €, por tokens 8,00 €: más de dos veces y media. En promedio son 2,67 tokens por palabra. El mensaje raro pesa más: 5 palabras, 18 tokens. El precio es un ejemplo, y los números exactos los da el proveedor: la respuesta de la API dice cuántos tokens entraron y cuántos salieron. Cómo mantenerse dentro de un presupuesto está en la lección sobre costo.

Pruébalo

Encuentra el error

Un colega escribe su propio BPE y obtiene reglas distintas: «mensajero» nunca se vuelve un solo token. ¿En qué línea está el error?

Elige la línea con el error.

La idea

Cuándo duele

  • Lo raro cuesta más. Los nombres de productos, los números de pedido, las erratas y las palabras de otro idioma se cortan en trozos pequeños. Los tokenizadores reales se entrenan sobre todo con texto en inglés, así que el mismo texto en español suele necesitar más tokens que en inglés, y en búlgaro o griego todavía más.
  • El modelo no ve letras. «mensajero» es para él un trozo o unos pocos trozos, no nueve letras. Por eso contar letras, dar vuelta una palabra y hacer cuentas con números largos salen mal: «1042» son cuatro tokens aquí, y con otro modelo pueden ser dos.
  • Cada modelo tiene su propio tokenizador. El mismo mensaje es un número distinto de tokens con modelos distintos. No estimes el precio por palabras ni lo traslades de un modelo a otro: cuenta con el tokenizador del modelo o lee cuántos tokens informó la API.
  • La respuesta puede cortarse a la mitad. Cuando el modelo llega al límite de respuesta, se detiene en el token al que llegó, incluso a mitad de una palabra. Mira el ejercicio.
Más precisamente, para curiosos

Bytes en lugar de letras. Los tokenizadores de los modelos reales suelen ser BPE sobre los bytes de UTF-8, no sobre letras. Así pasa cualquier texto, incluso los emoji y los alfabetos que nunca vieron. Una letra como «ñ» o «á» son 2 bytes, y una letra cirílica también, así que una palabra rara en otra escritura puede dar más tokens que letras tiene.

El espacio es parte del token. Aquí cada palabra se corta por separado y los espacios no cuentan. Los tokenizadores reales pegan el espacio a la palabra que sigue: « es» con el espacio delante es un token. Por eso «es» al inicio del texto y « es» en medio son tokens distintos.

Cuántos tokens hay. El vocabulario de un modelo real tiene, por ejemplo, de 50 000 a 200 000 tokens, aprendidos de una cantidad enorme de texto. Para el inglés la regla aproximada es un token por cada 4 caracteres. También hay tokens especiales que no son texto, como «fin de texto».

Las herramientas reales. Los tokenizadores ya hechos son bibliotecas: tiktoken y los tokenizers de Hugging Face en Python, y sus versiones para JavaScript, Java y C#. El trabajo con modelos suele hacerse en Python, pero un tokenizador es solo una tabla de reglas, y las reglas son las mismas en cualquier lenguaje. Los SDK de los proveedores también tienen una llamada que cuenta los tokens de un mensaje antes de enviarlo.

Ejercicio

La respuesta cortada en el límite

Resuélvelo en tu editor.

Un modelo escribe la respuesta token por token y se detiene cuando llega al límite de respuesta. Haz lo mismo: toma la respuesta, córtala en tokens palabra por palabra y detente después de 6 tokens. De la palabra que se corta, conserva solo los tokens que caben. Imprime cuántos tokens tiene toda la respuesta y qué queda hasta el límite.

Salida
la respuesta: 8 tokens
límite 6, hasta el límite: tu pedido llegará en cin

«cinco» son tres tokens, «ci|n|co», y el límite deja sitio solo para los dos primeros. El cliente ve «tu pedido llegará en cin» y no se entera de en cuántos días llega. Por eso el programa comprueba si la respuesta se detuvo en el límite, y o da más espacio o le pide al modelo una respuesta más corta.

Más tareas

  • Más reglas. Aprende 100 y 200 reglas. ¿En cuántos tokens se convierten las tres frases de la lección? ¿Cuándo deja de ayudar?
  • La palabra más cara. Encuentra la palabra del texto de la tienda que tiene más tokens después de 60 reglas.

Test

Ponte a prueba

1. El diccionario de palabras enteras ha visto «pedido» pero no «pedidos». ¿Qué hace BPE con «pedidos»?
2. ¿Por qué las reglas se aplican al texto nuevo en el orden en que se aprendieron?
3. Un colega estima el precio del bot por palabras. ¿Por qué la estimación sale más baja que la factura?
4. El modelo dice que «mensajero» tiene una «e», pero tiene dos. ¿Cuál es la razón más probable?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·