{"id":68,"date":"2026-01-08T11:16:27","date_gmt":"2026-01-08T11:16:27","guid":{"rendered":"https:\/\/pulki.es\/blog\/?p=68"},"modified":"2026-01-08T11:44:24","modified_gmt":"2026-01-08T11:44:24","slug":"large-language-model-llm-from-scratch-algoritmo-bpe-ejemplo-practico","status":"publish","type":"post","link":"https:\/\/pulki.es\/blog\/index.php\/2026\/01\/08\/large-language-model-llm-from-scratch-algoritmo-bpe-ejemplo-practico\/","title":{"rendered":"Large Language Model (LLM) from scratch. Algoritmo BPE: Ejemplo pr\u00e1ctico."},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">En la entrada anterior vimos c\u00f3mo funciona el algoritmo BPE. En esta entrega veremos c\u00f3mo los modelos LLM a gran escala tokenizan en la pr\u00e1ctica, y luego tokenizaremos un corpus artesanalmente utilizando Python.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>El tokenizador de OpenAI para el modelo gpt4o<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Como referencia pr\u00e1ctica, veremos c\u00f3mo tokeniza GPT-4o con <em><a href=\"https:\/\/tiktokenizer.vercel.app\/\">tiktokenizer<\/a><\/em>; su tokenizador usa un vocabulario grande (~200K tokens), t\u00edpico de sistemas comerciales. Por ejemplo, para el siguiente extracto de Altazor, de Vicente Huidobro:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Todas las lenguas est\u00e1n muertas<br>Muertas en manos del vecino tr\u00e1gico<br>Hay que resucitar las lenguas<br>Con sonoras risas<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">El n\u00famero total de tokens viene dado por 32:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"715\" height=\"460\" src=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-1.png\" alt=\"\" class=\"wp-image-70\" srcset=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-1.png 715w, https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-1-300x193.png 300w\" sizes=\"auto, (max-width: 715px) 100vw, 715px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">y cada token tiene un id \u00fanico, que en este caso vienen dados por:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"713\" height=\"336\" src=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-2.png\" alt=\"\" class=\"wp-image-71\" srcset=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-2.png 713w, https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-2-300x141.png 300w\" sizes=\"auto, (max-width: 713px) 100vw, 713px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Un ejercicio interesante es ver qu\u00e9 ocurre cuando traducimos este texto al ingl\u00e9s, y calculamos sus tokens:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"712\" height=\"455\" src=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-3.png\" alt=\"\" class=\"wp-image-72\" srcset=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-3.png 712w, https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/01\/image-3-300x192.png 300w\" sizes=\"auto, (max-width: 712px) 100vw, 712px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Con un n\u00famero similar de palabras, la lengua inglesa suele generar menos tokens. A eso se le llama <strong>sobre-segmentaci\u00f3n<\/strong>: entre otras razones, el tokenizador \u201crompe\u201d m\u00e1s el texto cuando el idioma est\u00e1 menos favorecido por las frecuencias del corpus de entrenamiento (en este caso, el espa\u00f1ol). El resultado es m\u00e1s tokens para decir lo mismo, lo que encarece contexto y c\u00f3mputo (Jurafsky y Martin, 2025). Moraleja pragm\u00e1tica: si quieres ahorrar dinero, escribe instrucciones en ingl\u00e9s!.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Creando nuestros propios tokens en Python<\/strong>!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Para construir nuestro tokenizador usaremos la librer\u00eda <strong>tokenizers<\/strong> de Python, que implementa <strong>Byte-Level BPE<\/strong>. Hasta aqu\u00ed ya entendemos qu\u00e9 es BPE; lo que falta es el componente <em>byte-level<\/em>. A continuaci\u00f3n lo explicamos brevemente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Byte-level BPE<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En la entrada anterior describimos BPE partiendo desde <strong>caracteres<\/strong>. En <em>byte-level BPE<\/em> el punto de partida es distinto: antes de aplicar BPE, el texto se convierte a una <strong>secuencia de bytes<\/strong> (normalmente en <strong>UTF-8<\/strong>). Esto tiene una consecuencia clave: como cualquier texto en UTF-8 siempre es una secuencia de bytes entre 0x00 y 0xFF, el tokenizador dispone de un \u201calfabeto\u201d base de <strong>256 s\u00edmbolos<\/strong> (uno por byte). A partir de ah\u00ed, BPE aprende a <strong>fusionar<\/strong> pares frecuentes de s\u00edmbolos para formar tokens m\u00e1s largos. Para verlo, nota c\u00f3mo se codifican algunos caracteres en UTF-8:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td class=\"has-text-align-left\" data-align=\"left\"><strong>Car\u00e1cter<\/strong><\/td><td><strong>UTF-8<\/strong> (Hex)<\/td><td><strong>Unicode<\/strong><\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">a<\/td><td>0x61 (1 byte)<\/td><td>U+0061<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">\u00f1<\/td><td>0xC3 0xB1 (2 bytes)<\/td><td>U+00F1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Si ahora tomamos el texto <code>\u00f1\ud83d\ude05<\/code>, su representaci\u00f3n UTF-8 es:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>Texto <\/td><td>UTF-8 (Hex)<\/td><td>Unicode<\/td><\/tr><tr><td>\u00f1\ud83d\ude05<\/td><td>0xC3 0xB1 0xF0 0x9F 0x98 0x85 (6 bytes)<\/td><td>U+00F1 U+1F605<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">La pregunta natural es: <strong>\u00bfcu\u00e1ntos tokens produce?<\/strong><br>En <em>byte-level<\/em> el \u201cpeor caso\u201d (sin fusiones \u00fatiles) es directo: <strong>1 token por byte<\/strong>, as\u00ed que <code>\u00f1\ud83d\ude05<\/code> se representar\u00eda con <strong>6 tokens<\/strong>. Sin embargo, en la pr\u00e1ctica BPE aprende merges, y puede ocurrir que el tokenizador tenga un token que corresponda exactamente a la secuencia <strong>0xC3 0xB1<\/strong> (es decir, \u201c\u00f1\u201d), o incluso un token para el emoji completo. En ese caso, el mismo texto podr\u00eda bajar a <strong>5 tokens<\/strong> (si \u201c\u00f1\u201d es un token) o a\u00fan menos (si el emoji tambi\u00e9n se fusiona), es decir <strong>2 tokens<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Esto explica por qu\u00e9 con byte-level BPE <strong>casi no existen \u201cpalabras desconocidas\u201d<\/strong>: siempre se puede \u201ccaer\u201d al nivel byte y representar cualquier texto. El problema ahora es la <strong>sobre-segmentaci\u00f3n<\/strong>: si tu corpus no contiene suficientes ejemplos de ciertos patrones (jerga, nombres propios, emojis, mezclas de idiomas), esos fragmentos se partir\u00e1n en m\u00e1s tokens, haciendo las secuencias m\u00e1s largas y aumentando el coste. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dada esta breve introducci\u00f3n, ahora estamos en condiciones de crear tokens.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Generando tokens con Cervantes<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Para este ejemplo, generaremos los tokens utilizando El Quijote de la Mancha de Miguel de Cervantes como input (lo puedes encontrar <a href=\"https:\/\/babel.upm.es\/~angel\/teaching\/pps\/quijote.txt\">aqu\u00ed<\/a> en texto plano!). Para esto, emplearemos el siguiente c\u00f3digo en Python:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from tokenizers import ByteLevelBPETokenizer\n\ntokenizer = ByteLevelBPETokenizer()\ntokenizer.train(\n    files=&#91;\"quijote.txt\"],\n    vocab_size=25000,    \n    min_frequency=2,    \n    special_tokens=&#91;\"&lt;s&gt;\", \"&lt;\/s&gt;\", \"&lt;pad&gt;\", \"&lt;unk&gt;\"]\n)\n\nvocab = tokenizer.get_vocab()\nprint(\"Tama\u00f1o del vocabulario:\", len(vocab))<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">donde files, es una lista que contiene la ruta de los archivos que servir\u00e1n como corpus, vocab_size es el tama\u00f1o objetivo del diccionario de tokens que va a tener el tokenizador, min_frequency es el umbral m\u00ednimo de apariciones que debe tener un patr\u00f3n en nuestro corpus para ser fusionado, y por \u00faltimo, special_tokens, es una lista de tokens que se deben incluir s\u00ed o s\u00ed en el vocabulario.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La ejecuci\u00f3n de este c\u00f3digo da un <strong>tama\u00f1o total de vocabulario<\/strong> de 22.349 tokens. Para el mini-LLM que tenemos en mente, este n\u00famero es m\u00e1s que aceptable. Sin embargo, en LLM&#8217;s grandes, necesitar\u00edamos entre 50k-200k tokens para tener un modelo relativamente robusto.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ahora que ya contamos con un tokenizador entrenado, el siguiente paso es evaluar su desempe\u00f1o. Para ello, ejecutamos las siguientes l\u00edneas de c\u00f3digo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TEXT = \"\u00a1A ti lo agradezco, Dios, que cielo y tierra gu\u00edas! \u00a1V\u00e1lganme tus virtudes, gloriosa santa Mar\u00eda! Desde aqu\u00ed dejo a Castilla pues que al Rey he en ira\"\nenc = tokenizer.encode(TEXT)\nprint(\"Texto:\", TEXT)\nprint(\"N\u00ba tokens:\", len(enc.ids))\nprint(\"IDs:\", enc.ids)\nprint(\"Tokens:\", enc.tokens)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Donde la variable TEXT contiene un texto que no est\u00e1 en el corpus original (es un fragmento del <a href=\"https:\/\/www.cervantesvirtual.com\/obra-visor\/texto-modernizado-del-cantar-de-mio-cid--0\/html\/0175c3aa-82b2-11df-acc7-002185ce6064_6.html\">Cantar del M\u00edo Cid<\/a> modernizado). Habiendo dicho esto, el presente texto entrega un total de 38 tokens. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En este sentido, con la finalidad de ilustrar el problema de la <strong>sobre-segmentaci\u00f3n<\/strong>, entrenaremos un tokenizador sobre un corpus deliberadamente pobre y observaremos c\u00f3mo esto incrementa el n\u00famero de tokens necesarios para codificar la misma frase de prueba. Para ello, utilizaremos el siguiente corpus:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">En el 238 a. C. la tomaron los romanos, como consecuencia de la derrota cartaginesa en la Primera Guerra P\u00fanica. Cerde\u00f1a se convirti\u00f3 en provincia romana, y las ciudades costeras que exist\u00edan fueron engrandecidas y embellecidas, mientras que se fundaron colonias como Turris Lybissonis y Feronia, que fueron pobladas por inmigrantes romanos.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Dejando todo lo dem\u00e1s constante, el n\u00famero de tokens de la frase de prueba es ahora 127, considerablemente m\u00e1s que cuando ten\u00edamos un corpus m\u00e1s rico. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Por \u00faltimo, vale la pena recalcar que este proyecto consiste en construir nuestro propio LLM (peque\u00f1o) desde cero. Para esto, antes de entrenar el modelo, necesitamos dos piezas: <strong>un corpus<\/strong> (los textos con los que aprender\u00e1) y <strong>un tokenizador<\/strong> entrenado sobre ese corpus. A escala industrial, lo caro no es solo entrenar: es <strong>reunir, limpiar, filtrar y deduplicar<\/strong> cantidades enormes de texto. Para ponerlo en perspectiva, en GPT-2 OpenAI entren\u00f3 con WebText: un dataset de algo m\u00e1s de <a href=\"https:\/\/cdn.openai.com\/better-language-models\/language_models_are_unsupervised_multitask_learners.pdf\">8 millones de documentos, que suman aproximadamente 40 GB de texto<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En esta serie no vamos a perseguir esa escala: trabajaremos con un corpus acotado, suficiente para entender el flujo completo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Notas finales, y pr\u00f3xima entrada\u2026<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En esta entrada vimos un peque\u00f1o ejemplo de como funciona el algoritmo BPE, que es clave en la construcci\u00f3n de un LLM. En la pr\u00f3xima entrega introduciremos los modelos de lenguaje n-gram: modelos probabil\u00edsticos que asignan probabilidad a la pr\u00f3xima palabra y a oraciones completas. Stay tuned!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Jos\u00e9 Miguel Mu\u00f1oz Urra \u2013&nbsp;<a href=\"mailto:jmunozu@pulki.es\">jmunozu@pulki.es<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>En la entrada anterior vimos c\u00f3mo funciona el algoritmo BPE. En esta entrega veremos c\u00f3mo los modelos LLM a gran escala tokenizan en la pr\u00e1ctica, y luego tokenizaremos un corpus artesanalmente utilizando Python. El tokenizador de OpenAI para el modelo gpt4o Como referencia pr\u00e1ctica, veremos c\u00f3mo tokeniza GPT-4o con tiktokenizer; su tokenizador usa un vocabulario [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-68","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/68","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=68"}],"version-history":[{"count":5,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/68\/revisions"}],"predecessor-version":[{"id":77,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/68\/revisions\/77"}],"wp:attachment":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=68"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=68"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=68"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}