{"id":61,"date":"2025-12-30T08:31:15","date_gmt":"2025-12-30T08:31:15","guid":{"rendered":"https:\/\/pulki.es\/blog\/?p=61"},"modified":"2025-12-30T08:31:15","modified_gmt":"2025-12-30T08:31:15","slug":"large-language-model-llm-from-scratch-algoritmo-bpe","status":"publish","type":"post","link":"https:\/\/pulki.es\/blog\/index.php\/2025\/12\/30\/large-language-model-llm-from-scratch-algoritmo-bpe\/","title":{"rendered":"Large Language Model (LLM) from scratch. Algoritmo BPE."},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">En la entrada <a href=\"https:\/\/pulki.es\/blog\/index.php\/2025\/11\/25\/large-language-model-llm-from-scratch-intro\/\">anterior<\/a> motivamos el algoritmo BPE. Ahora hablaremos brevemente de su historia y explicaremos en detalle c\u00f3mo funciona.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">El algoritmo BPE, desarrollado por <a href=\"https:\/\/www.derczynski.com\/papers\/archive\/BPE_Gage.pdf\">Philip Gage (1994)<\/a>, es un m\u00e9todo cl\u00e1sico en lo que respecta a la compresi\u00f3n de datos. Su idea central es recorrer los datos y encontrar el par de bytes adyacentes m\u00e1s frecuente para, luego, sustituir esas apariciones por un byte libre. A modo de ejemplo, supongamos que tenemos 10 bytes:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td class=\"has-text-align-center\" data-align=\"center\">65<\/td><td class=\"has-text-align-center\" data-align=\"center\">66<\/td><td class=\"has-text-align-center\" data-align=\"center\">65<\/td><td class=\"has-text-align-center\" data-align=\"center\">66<\/td><td class=\"has-text-align-center\" data-align=\"center\">65<\/td><td class=\"has-text-align-center\" data-align=\"center\">66<\/td><td class=\"has-text-align-center\" data-align=\"center\">67<\/td><td class=\"has-text-align-center\" data-align=\"center\">68<\/td><td class=\"has-text-align-center\" data-align=\"center\">69<\/td><td class=\"has-text-align-center\" data-align=\"center\">70<\/td><\/tr><tr><td class=\"has-text-align-center\" data-align=\"center\">A<\/td><td class=\"has-text-align-center\" data-align=\"center\">B<\/td><td class=\"has-text-align-center\" data-align=\"center\">A<\/td><td class=\"has-text-align-center\" data-align=\"center\">B<\/td><td class=\"has-text-align-center\" data-align=\"center\">A<\/td><td class=\"has-text-align-center\" data-align=\"center\">B<\/td><td class=\"has-text-align-center\" data-align=\"center\">C<\/td><td class=\"has-text-align-center\" data-align=\"center\">D<\/td><td class=\"has-text-align-center\" data-align=\"center\">E<\/td><td class=\"has-text-align-center\" data-align=\"center\">F<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">En este caso, la primera fila representa el n\u00famero del byte escrito en decimal. La segunda corresponde a su interpretaci\u00f3n en ASCII. Entonces, acorde con la l\u00f3gica del algoritmo, debemos recorrer esa tabla y encontrar el par de bytes m\u00e1s frecuente. En nuestro caso, ser\u00eda:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>Car\u00e1cter<\/td><td>Frecuencia<\/td><\/tr><tr><td>AB<\/td><td>3<\/td><\/tr><tr><td>BA<\/td><td>2<\/td><\/tr><tr><td>BC<\/td><td>1<\/td><\/tr><tr><td>CD<\/td><td>1<\/td><\/tr><tr><td>DE<\/td><td>1<\/td><\/tr><tr><td>EF<\/td><td>1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Seg\u00fan la tabla, el par m\u00e1s frecuente es AB. A partir de aqu\u00ed, debemos elegir un \u00abbyte libre\u00bb. De antemano, sabemos que los bytes que van desde 65 a 70 est\u00e1n ocupados, y que los que van desde 0 a 64, y de 71 a 255, est\u00e1n libres. Para efectos de este ejemplo, utilizaremos el byte 80 como el \u00abbyte libre\u00bb que denotar\u00e1 el par \u00abAB\u00bb. Dicho lo anterior, podemos escribir la siguiente tabla:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td class=\"has-text-align-center\" data-align=\"center\">80<\/td><td class=\"has-text-align-center\" data-align=\"center\">80<\/td><td class=\"has-text-align-center\" data-align=\"center\">80<\/td><td class=\"has-text-align-center\" data-align=\"center\">67<\/td><td class=\"has-text-align-center\" data-align=\"center\">68<\/td><td class=\"has-text-align-center\" data-align=\"center\">69<\/td><td class=\"has-text-align-center\" data-align=\"center\">70<\/td><\/tr><tr><td class=\"has-text-align-center\" data-align=\"center\">AB<\/td><td class=\"has-text-align-center\" data-align=\"center\">AB<\/td><td class=\"has-text-align-center\" data-align=\"center\">AB<\/td><td class=\"has-text-align-center\" data-align=\"center\">C<\/td><td class=\"has-text-align-center\" data-align=\"center\">D<\/td><td class=\"has-text-align-center\" data-align=\"center\">E<\/td><td class=\"has-text-align-center\" data-align=\"center\">F<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Vale la pena destacar que esta matriz ya no tiene la misma interpretaci\u00f3n que la anterior. En este caso, 80 representa \u00abAB\u00bb en el algoritmo, pero eso no implica que su interpretaci\u00f3n en ASCII sea \u00abAB\u00bb (de hecho, en ASCII es la letra P). Dicho esto, como podemos ver, un archivo que antes ten\u00eda 10 bytes pudo reducirse a 7 (y podr\u00eda reducirse a\u00fan m\u00e1s). En la siguiente secci\u00f3n, veremos c\u00f3mo este algoritmo ha sido adaptado para llevar a cabo una segmentaci\u00f3n en <em>subwords<\/em>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Adaptando BPE para construir <em>subwords<\/em> (Senrich et al., 2016).<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/aclanthology.org\/P16-1162.pdf\">Senrich et al. (2016)<\/a>, tomaron el concepto desarrollado por Gage  (1994), y lo extendieron para tokenizar en modelos modernos de lenguaje. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00f3n, 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Etapa de entrenamiento&#8230;<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Su funcionamiento es muy similar al algoritmo propuesto por Gage (1994). Iterativamente, se fusionan tokens aleda\u00f1os frecuentes para crear nuevos, cuyas cadenas de texto sean cada vez m\u00e1s largas. Para ilustrar la mec\u00e1nica subyacente, tomaremos el ejemplo del libro que estamos usando como referencia <a href=\"https:\/\/web.stanford.edu\/~jurafsky\/slp3\/ed3book_aug25.pdf\">(Jurafsky y James H. Martin, 2025<\/a>). 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:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td class=\"has-text-align-center\" data-align=\"center\">A<\/td><td class=\"has-text-align-center\" data-align=\"center\">B<\/td><td class=\"has-text-align-center\" data-align=\"center\">D<\/td><td class=\"has-text-align-center\" data-align=\"center\">C<\/td><td class=\"has-text-align-center\" data-align=\"center\">A<\/td><td class=\"has-text-align-center\" data-align=\"center\">B<\/td><td class=\"has-text-align-center\" data-align=\"center\">E<\/td><td class=\"has-text-align-center\" data-align=\"center\">C<\/td><td class=\"has-text-align-center\" data-align=\"center\">A<\/td><td class=\"has-text-align-center\" data-align=\"center\">B<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Esto nos entrega la siguiente tabla de frecuencias,<\/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>Par<\/strong><\/td><td><strong>Frecuencia<\/strong><\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">AB<\/td><td>3<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">CA<\/td><td>2<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">DC<\/td><td>1<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">BD<\/td><td>1<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">BE<\/td><td>1<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">EC<\/td><td>1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">De aqu\u00ed, deducimos que el par m\u00e1s frecuente es \u00abAB\u00bb. Por tanto, debemos fusionar los pares adyacentes \u00abA | B\u00bb en \u00abAB\u00bb, lo que genera un nuevo corpus de 7 tokens, cuya estructura se muestra en la siguiente tabla:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td class=\"has-text-align-center\" data-align=\"center\">AB<\/td><td class=\"has-text-align-center\" data-align=\"center\">D<\/td><td class=\"has-text-align-center\" data-align=\"center\">C<\/td><td class=\"has-text-align-center\" data-align=\"center\">AB<\/td><td class=\"has-text-align-center\" data-align=\"center\">E<\/td><td>C<\/td><td>AB<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Y constar\u00e1 de un vocabulario de 6 tokens: A, B, C, D, E y AB. Ahora, repitiendo el proceso, el par m\u00e1s frecuente es \u00abC | AB\u00bb, lo que convierte nuestro corpus en:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>AB<\/td><td>D<\/td><td>CAB<\/td><td>E<\/td><td>CAB<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Y el vocabulario asociado, en el siguiente conjunto: A, B, C, D, E, AB y CAB. El algoritmo contin\u00faa hasta generar <em>k<\/em> fusiones, siendo <em>k<\/em> un n\u00famero definido ex\u00f3genamente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En este ejemplo, utilizamos una cadena de texto con una palabra de 10 caracteres. No obstante, en la pr\u00e1ctica, un corpus est\u00e1 compuesto por m\u00e1s de una palabra, que, generalmente, est\u00e1n separadas por un espacio en blanco. Entonces, \u00bfc\u00f3mo lidiamos con m\u00e1s de una palabra? Ve\u00e1moslo con un ejemplo:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Supongamos que tenemos el siguiente texto, donde \u00ab_\u00bb representa un espacio en blanco:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>set_new_new_renew_reset_renew<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Al igual que en el ejemplo anterior, podemos separar los caracteres de la siguiente forma:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>s<\/td><td>e<\/td><td>t<\/td><td>_<\/td><td>n<\/td><td>e<\/td><\/tr><tr><td>w<\/td><td>_<\/td><td>n<\/td><td>e<\/td><td>w<\/td><td>_<\/td><\/tr><tr><td>r<\/td><td>e<\/td><td>n<\/td><td>e<\/td><td>w<\/td><td>_<\/td><\/tr><tr><td>r<\/td><td>e<\/td><td>s<\/td><td>e<\/td><td>t<\/td><td>_<\/td><\/tr><tr><td>r<\/td><td>e<\/td><td>n<\/td><td>e<\/td><td>w<\/td><td><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e1s frecuentes del texto original. En particular, el par \u00abne\u00bb es el m\u00e1s frecuente (se repite 4 veces). Entonces, la nueva tabla puede reescribirse como:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>s<\/td><td>e<\/td><td>t<\/td><td>_<\/td><td>ne<\/td><td>w<\/td><\/tr><tr><td>_<\/td><td>ne<\/td><td>w<\/td><td>_<\/td><td>r<\/td><td>e<\/td><\/tr><tr><td>ne<\/td><td>w<\/td><td>_<\/td><td>r<\/td><td>e<\/td><td>s<\/td><\/tr><tr><td>e<\/td><td>t<\/td><td>_<\/td><td>r<\/td><td>e<\/td><td>ne<\/td><\/tr><tr><td>w<\/td><td><\/td><td><\/td><td><\/td><td><\/td><td><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">lo que genera el siguiente vocabulario: _, e, n, r, s, t, w, ne. Repitiendo el proceso,<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>s<\/td><td>e<\/td><td>t<\/td><td>_<\/td><td>ne<\/td><td>w<\/td><\/tr><tr><td>_<\/td><td>ne<\/td><td>w<\/td><td>_<\/td><td>r<\/td><td>e<\/td><\/tr><tr><td>ne<\/td><td>w<\/td><td>_<\/td><td>r<\/td><td>e<\/td><td>s<\/td><\/tr><tr><td>e<\/td><td>t<\/td><td>_<\/td><td>r<\/td><td>e<\/td><td>ne<\/td><\/tr><tr><td>w<\/td><td><\/td><td><\/td><td><\/td><td><\/td><td><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">el par m\u00e1s frecuente es \u00abne w\u00bb. Por tanto, la nueva tabla viene dada por:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>s<\/td><td>e<\/td><td>t<\/td><td>_<\/td><td>new<\/td><td>_<\/td><\/tr><tr><td>new<\/td><td>_<\/td><td>r<\/td><td>e<\/td><td>new<\/td><td>_<\/td><\/tr><tr><td>r<\/td><td>e<\/td><td>s<\/td><td>e<\/td><td>t<\/td><td>_<\/td><\/tr><tr><td>r<\/td><td>e<\/td><td>new<\/td><td><\/td><td><\/td><td><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">y el nuevo vocabulario por: _, e, n, r, s, t, w, ne, new. Si repetimos una vez m\u00e1s este proceso, el par m\u00e1s frecuente es \u00ab_r\u00bb, lo que convierte nuestro corpus en:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>s<\/td><td>e<\/td><td>t<\/td><td>_<\/td><td>new<\/td><td>_<\/td><\/tr><tr><td>new<\/td><td>_r<\/td><td>e<\/td><td>new<\/td><td>_r<\/td><td>e<\/td><\/tr><tr><td>s<\/td><td>e<\/td><td>t<\/td><td>_r<\/td><td>e<\/td><td>new<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">con el siguiente vocabulario: _, e, n, r, s, t, w, ne, new, _r. De esta forma, podemos seguir hasta generar <em>k <\/em>uniones.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Etapa de codificaci\u00f3n&#8230;<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <em>greedy<\/em> (si se puede fusionar, fusiona). A modo de ejemplo, supongamos que este fue el orden de aprendizaje en la etapa de entrenamiento:<\/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\">1) n + e<\/td><td>ne<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">2) ne + w<\/td><td>new<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">3) _ + r<\/td><td>_r<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\">4) _r + e<\/td><td>_re<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Adem\u00e1s, supondremos que nuestro texto de prueba s\u00f3lo contiene la palabra \u00abnew\u00bb, es decir,<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td class=\"has-text-align-center\" data-align=\"center\">n<\/td><td class=\"has-text-align-center\" data-align=\"center\">e<\/td><td class=\"has-text-align-center\" data-align=\"center\">w<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Luego, como tenemos una fusi\u00f3n \u00abn+e = ne\u00bb, el resultado ser\u00eda:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td class=\"has-text-align-center\" data-align=\"center\">ne<\/td><td class=\"has-text-align-center\" data-align=\"center\">w<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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 <em>rankings<\/em>,<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>1) n + e<\/td><td>ne<\/td><td>1) e +w<\/td><td>ew<\/td><\/tr><tr><td>2) ne + w<\/td><td>new<\/td><td>2) n + e<\/td><td>ne<\/td><\/tr><tr><td><\/td><td><\/td><td>3) ne+w<\/td><td>new<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">trata de codificar la palabra \u00abnew\u00bb. <strong>Hint<\/strong>: para el primer ranking, terminar\u00e1s con [new], y para el segundo terminar\u00e1s con [n,ew].<\/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 como funciona el algoritmo BPE en el contexto de la construcci\u00f3n de <em>subwords<\/em>, y en el art\u00edculo en que se basaron los autores para idearlo. En la pr\u00f3xima entrada veremos como funciona este procedimiento en la pr\u00e1ctica! Stay tuned!<\/p>\n\n\n\n<p class=\"has-text-align-left 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 motivamos el algoritmo BPE. Ahora hablaremos brevemente de su historia y explicaremos en detalle c\u00f3mo funciona. El algoritmo BPE, desarrollado por Philip Gage (1994), es un m\u00e9todo cl\u00e1sico en lo que respecta a la compresi\u00f3n de datos. Su idea central es recorrer los datos y encontrar el par de bytes adyacentes [&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-61","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/61","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=61"}],"version-history":[{"count":5,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/61\/revisions"}],"predecessor-version":[{"id":66,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/61\/revisions\/66"}],"wp:attachment":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=61"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=61"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=61"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}