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ñasstring[] 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úmerovar vocab = new List<string>();foreach (var line in text) foreach (var word in line.Split(' ')) if (!vocab.Contains(word)) vocab.Add(word);Console.WriteLine($"diccionario: {vocab.Count} palabras");foreach (var message in new[] { "el paquete llegará en tres días", "mis pedidos llegarán mañana" }){ var ids = message.Split(' ').Select(word => vocab.Contains(word) ? vocab.IndexOf(word).ToString() : "?"); Console.WriteLine($"{message} → {string.Join(" ", ids)}");}diccionario: 83 palabrasel paquete llegará en tres días → 8 21 9 13 14 15mis pedidos llegarán mañana → ? ? ? 12El 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):
- Cada palabra es una lista de letras.
- Cuenta todos los pares de vecinos en todas las palabras. Una palabra que aparece 7 veces cuenta sus pares 7 veces.
- El par más frecuente se convierte en un trozo: eso es una regla. Únelo en todas partes.
- 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:
// una regla: left y right juntos pasan a ser un tokenrecord Rule(string Left, string Right, int Count);
// la palabra como lista de letrasstatic List<string> Letters(string word) => word.Select(c => c.ToString()).ToList();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 juntosstatic List<string> Merge(List<string> tokens, string left, string right){ var glued = new List<string>(); int i = 0; while (i < tokens.Count) { if (i + 1 < tokens.Count && tokens[i] == left && tokens[i + 1] == right) { glued.Add(left + right); i += 2; } else { glued.Add(tokens[i]); i += 1; } } return glued;}Aprender son los cuatro pasos de la tarjeta anterior. OrderedDictionary 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 frecuentestatic List<Rule> Learn(string[] text, int count){ var freq = new OrderedDictionary<string, int>(); // cuántas veces aparece cada palabra en el texto foreach (var line in text) foreach (var word in line.Split(' ')) freq[word] = freq.GetValueOrDefault(word) + 1; var split = new OrderedDictionary<string, List<string>>(); // por ahora cada palabra son solo letras foreach (var word in freq.Keys) split[word] = Letters(word); var rules = new List<Rule>(); for (int r = 0; r < count; r++) { var pairs = new OrderedDictionary<(string, string), int>(); // los pares de vecinos, contados con la frecuencia de la palabra foreach (var (word, t) in split) for (int i = 0; i + 1 < t.Count; i++) pairs[(t[i], t[i + 1])] = pairs.GetValueOrDefault((t[i], t[i + 1])) + freq[word]; var best = pairs.MaxBy(pair => pair.Value); // el par más frecuente (en caso de empate, el que se vio primero) var (left, right) = best.Key; rules.Add(new Rule(left, right, best.Value)); foreach (var word in split.Keys.ToList()) split[word] = Merge(split[word], left, right); // la nueva regla vale para todas las palabras } return rules;}var rules = Learn(text, 60);for (int k = 0; k < rules.Count; k++){ var r = rules[k]; var cell = $"{k + 1}. {r.Left}+{r.Right} ({r.Count})"; Console.Write(k % 4 == 3 ? cell + "\n" : $"{cell,-22}");}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?
List<Rule> rules =[ new Rule("e", "l", 25), new Rule("e", "n", 22), new Rule("r", "o", 20),];Console.WriteLine(string.Join("|", Encode("mensajero", rules)));Las reglas van en orden y cada una recorre toda la palabra. La primera busca «e» justo antes de «l», pero en «mensajero» no hay «el»: no pasa nada. La segunda une «e» con «n» en «mensajero». La tercera une «r» con «o». Lo que queda es m|en|s|a|j|e|ro. «m|e|n|s|a|j|e|ro» sería correcto si faltara la segunda regla, y «m|en|s|a|j|e|r|o» si faltara la tercera.
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 ordenstatic List<string> Encode(string sentence, List<Rule> rules){ var tokens = new List<string>(); foreach (var word in sentence.Split(' ')) { var t = Letters(word); foreach (var r in rules) t = Merge(t, r.Left, r.Right); tokens.AddRange(t); } return tokens;}// los tokens de cada palabra, con | entre ellosstatic string Show(string sentence, List<Rule> rules) => string.Join(" ", sentence.Split(' ').Select(word => string.Join("|", Encode(word, rules))));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 juntosstatic List<string> Merge(List<string> tokens, string left, string right){ var glued = new List<string>(); int i = 0; while (i < tokens.Count) { if (i + 1 < tokens.Count && tokens[i] == left && tokens[i + 1] == right) { glued.Add(left + right); i += 2; } else { glued.Add(tokens[i]); i += 1; } } return glued;}
// una frase → tokens: letras y luego las reglas en ordenstatic List<string> Encode(string sentence, List<Rule> rules){ var tokens = new List<string>(); foreach (var word in sentence.Split(' ')) { var t = Letters(word); foreach (var r in rules) t = Merge(t, r.Left, r.Right); tokens.AddRange(t); } return tokens;}Paso a paso, en palabras
- Cada palabra empieza como letras: 26 fichas, 26 tokens.
- Regla 1: e + l. Donde las dos están juntas, pasan a ser un token, en cada palabra.
- Regla 2: e + n. Cada regla recorre toda la frase, en el orden en que se aprendió.
- Las reglas 16 a 18 no tienen nada que unir aquí, por eso el número salta a 19.
- Regla 57: lleg + ará. «llegará» ya es un token.
- Las 60 reglas: 26 letras, 10 tokens. «paquete» queda en 4 partes: esta palabra es rara en el texto.
var rules = Learn(text, 60);string[] sentences = ["el paquete llegará en tres días", "mis pedidos llegarán mañana", "queja por un televisor roto"];foreach (var s in sentences){ Console.WriteLine(Show(s, rules)); Console.WriteLine($" letras {s.Replace(" ", "").Length}, tokens {Encode(s, rules).Count}");}el pa|q|ue|te llegará en tr|es días letras 26, tokens 10m|i|s pedido|s llegará|n m|a|ñ|a|na letras 24, tokens 12q|ue|j|a p|o|r un t|el|e|v|i|s|o|r ro|to letras 23, tokens 18La 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
static List<string> Merge(List<string> tokens,
string left, string right)
{
var glued = new List<string>();
int i = 0;
while (i < tokens.Count)
{
if (i + 1 < tokens.Count
&& tokens[i] == left && tokens[i + 1] == right)
{
glued.Add(left + right);
i += ;
}
else
{
glued.Add(tokens[i]);
i += 1;
}
}
return glued;
}2: los dos tokens viejos ya están en el nuevo, así que el siguiente que hay que revisar es el que viene después. Con 1 el token derecho se miraría otra vez y aparecería dos veces: «e», «n» daría «en», «n».
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:
var rules = Learn(text, 60);string[] messages = ["el paquete llegará en tres días", "mis pedidos llegarán mañana", "queja por un televisor roto"];int words = 0, tokens = 0;foreach (var m in messages){ int w = m.Split(' ').Length, t = Encode(m, rules).Count; words += w; tokens += t; Console.WriteLine($"{m}: {w} palabras, {t} tokens");}Console.WriteLine($"tokens por palabra: {(double)tokens / words:F2}");double price = 3; // € por millón de tokens, un precio de ejemploint month = 200000;Console.WriteLine($"{month} mensajes al mes, 3.00 € por millón de tokens (un precio de ejemplo):");Console.WriteLine(" por palabras: " + $"{month * ((double)words / messages.Length) * price / 1e6:F2} €");Console.WriteLine(" por tokens: " + $"{month * ((double)tokens / messages.Length) * price / 1e6:F2} €");el paquete llegará en tres días: 6 palabras, 10 tokensmis pedidos llegarán mañana: 4 palabras, 12 tokensqueja por un televisor roto: 5 palabras, 18 tokenstokens por palabra: 2.67200000 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.
Cada par se cuenta con 1, no con la frecuencia de la palabra. Así «mensajero», que está en el texto varias veces, cuenta sus pares una sola vez, como una palabra vista una vez. Las reglas van a los pares que están en muchas palabras distintas, no a los que aparecen más veces. Debe ser freq[word].
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.
la respuesta: 8 tokenslí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
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Dictionary y HashSet · Lambdas y delegados · record, switch y patrones · Qué es el aprendizaje automático
- Continúa en: Predecir el siguiente token · Embeddings: el significado como vectores · La ventana de contexto · Los límites del modelo
- Ejercicios sobre el tema: Cómo funciona un modelo de lenguaje