{"id":79,"date":"2026-02-21T23:02:05","date_gmt":"2026-02-21T23:02:05","guid":{"rendered":"https:\/\/pulki.es\/blog\/?p=79"},"modified":"2026-02-21T23:02:34","modified_gmt":"2026-02-21T23:02:34","slug":"large-language-model-llm-from-scratch-n-gram-language-models","status":"publish","type":"post","link":"https:\/\/pulki.es\/blog\/index.php\/2026\/02\/21\/large-language-model-llm-from-scratch-n-gram-language-models\/","title":{"rendered":"Large Language Model (LLM) from scratch. N-gram Language Models"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">En la entrada anterior vimos un ejemplo pr\u00e1ctico del algoritmo BPE. Entender esto es importante porque es la forma est\u00e1ndar en que las m\u00e1quinas descomponen un texto en una representaci\u00f3n que pueden manipular. Puesto que ahora sabemos hacerlo, el siguiente paso l\u00f3gico es emplear esos fragmentos (o tokens) de alguna manera, para as\u00ed generar texto elocuente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En este sentido, la estrategia m\u00e1s popular \u2014enfatizo: popular, no necesariamente mejor\u2014 para generar texto \u201cnuevo\u201d a partir de un contexto consiste en calcular la probabilidad de la siguiente palabra o token, condicional al contexto; es decir, <strong>P(siguiente&nbsp;palabra&nbsp;o&nbsp;token | contexto)<\/strong>. Si nos restringimos a esta estrategia, una manera pr\u00e1ctica de obtener esa probabilidad es usando n-gramas. No obstante, tambi\u00e9n se pueden usar redes neuronales \u2014que es lo que usa ChatGPT\u2014, entre otras alternativas.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En este punto, el lector podr\u00eda estar pregunt\u00e1ndose: \u201c\u00bfPor qu\u00e9 aprender n-gramas si no es lo que utiliza ChatGPT?\u201d. La respuesta es simple: es un marco conceptual muy ilustrativo, que nos permitir\u00e1 aprender conceptos clave y, as\u00ed, avanzar hacia el uso de redes neuronales. Como es usual, emplearemos como referencia el <strong>cap\u00edtulo 3<\/strong> de <strong>Jurafsky y Martin, 2025<\/strong>. Dicho esto, empecemos!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Language models<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un modelo de lenguaje (Language Model) es un modelo probabil\u00edstico que, dado un contexto, predice la siguiente palabra o token. Esta predicci\u00f3n se realiza construyendo una distribuci\u00f3n de probabilidad sobre las posibles siguientes palabras\/tokens. En t\u00e9rminos concretos, supongamos que tenemos 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\">Aqu\u00ed lleg\u00f3, donde otro no ha llegado,<br>don Alonso de Ercilla, que el primero<br>en un peque\u00f1o barco deslastrado,<br>con solos diez pas\u00f3 el desaguadero<br>el a\u00f1o de cincuenta y ocho entrado<br>sobre mil y quinientos, por hebrero,<br>a las dos de la tarde, el postrer d\u00eda,<br>volviendo a la dejada compa\u00f1\u00eda.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>La Araucana, Canto XXXVI.<\/em> <em>Alonso de Ercilla.<\/em><\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">y queremos predecir la palabra que viene despu\u00e9s de la secuencia <strong>{a las dos de la}<\/strong>. Para eso, podemos calcular la distribuci\u00f3n de probabilidad sobre las siguientes palabras condicional a venir de la secuencia <strong>{a las dos de la}<\/strong>. Si, por ejemplo, estamos interesados en saber la probabilidad de que la palabra siguiente sea <strong>{tarde}<\/strong>, deber\u00edamos calcular <strong>P({tarde} | {a las dos de la})<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En este punto, un lector \u00e1vido podr\u00eda preguntarse: \u00bfpor qu\u00e9 elegimos \u201ca las dos de la\u201d y no una secuencia m\u00e1s larga? El problema es que, en un corpus finito, cuanto m\u00e1s largo es el contexto, m\u00e1s raro se vuelve observar exactamente ese mismo contexto otra vez. En el extremo, si el contexto es demasiado largo, la estimaci\u00f3n se vuelve muy fr\u00e1gil: muchos contextos no aparecen nunca, y los que aparecen pocas veces producen distribuciones casi deterministas (una \u00fanica continuaci\u00f3n con probabilidad cercana a 1), lo que puede socavar la variedad del texto generado.<br>Por el contrario, si elegimos un contexto muy corto, perdemos coherencia: por ejemplo, incluso con un corpus grande, despu\u00e9s de \u201cy\u201d puede venir casi cualquier palabra. En fin, en t\u00e9rminos emp\u00edricos, el largo del contexto afecta de forma decisiva al resultado. Al escogerlo, el programador est\u00e1 imponiendo una estructura sobre c\u00f3mo \u201cdepende\u201d el siguiente token del pasado: en los modelos<strong> <\/strong>n-gram, esta idea se formaliza en la <strong>Markov assumption<\/strong>, seg\u00fan la cual la siguiente palabra depende solo de las \u00faltimas <math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>n<\/mi><mo>\u2212<\/mo><mn>1<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">n-1<\/annotation><\/semantics><\/math> palabras del contexto (si quieres saber m\u00e1s sobre cadenas de Markov, puedes ver este <a href=\"https:\/\/www.youtube.com\/watch?v=6pO6Mm2qJaE\">video<\/a>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Espec\u00edficamente, cuando fijamos cu\u00e1ntos tokens usaremos como contexto, estamos eligiendo impl\u00edcitamente <math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>n<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">n<\/annotation><\/semantics><\/math> en un modelo de n-gramas (ya que el contexto tiene tama\u00f1o <math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>n<\/mi><mo>\u2212<\/mo><mn>1<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">n-1<\/annotation><\/semantics><\/math>). Por ejemplo, un <strong>modelo de bigramas<\/strong> asume que la probabilidad de la siguiente palabra depende \u00fanicamente de la palabra anterior (si tomamos cada token como una palabra).<\/p>\n\n\n\n<p class=\"has-text-align-center wp-block-paragraph\"> <math data-latex=\"\\mathbb{P}(\\{tarde\\}|\\{la\\})\"><semantics><mrow><mi>\u2119<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>t<\/mi><mi>a<\/mi><mi>r<\/mi><mi>d<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>l<\/mi><mi>a<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">\\mathbb{P}(\\{tarde\\}|\\{la\\})<\/annotation><\/semantics><\/math>,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">por otra parte, un trigrama asume que la probabilidad de la siguiente palabra depende de las dos palabras que preceden a aquella,<\/p>\n\n\n\n<p class=\"has-text-align-center wp-block-paragraph\"><math data-latex=\"\\mathbb{P}(\\{tarde\\}|\\{de \\ la\\}).\"><semantics><mrow><mi>\u2119<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>t<\/mi><mi>a<\/mi><mi>r<\/mi><mi>d<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>d<\/mi><mi>e<\/mi><mtext>&nbsp;<\/mtext><mi>l<\/mi><mi>a<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mi>.<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\mathbb{P}(\\{tarde\\}|\\{de \\ la\\}).<\/annotation><\/semantics><\/math><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dicho esto, ahora estamos en condiciones de estimar estas probabilidades.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Estimando las probabilidades&#8230;<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">El m\u00e9todo est\u00e1ndar para estimar estas probabilidades es usar M\u00e1xima Verosimilitud. Por ejemplo, para el caso de un <strong>bigrama<\/strong>, para calcular la probabilidad  de una palabra <math data-latex=\"w_{n}\"><semantics><msub><mi>w<\/mi><mi>n<\/mi><\/msub><annotation encoding=\"application\/x-tex\">w_{n}<\/annotation><\/semantics><\/math>, dada una palabra <math data-latex=\"w_{n-1}\"><semantics><msub><mi>w<\/mi><mrow><mi>n<\/mi><mo>\u2212<\/mo><mn>1<\/mn><\/mrow><\/msub><annotation encoding=\"application\/x-tex\">w_{n-1}<\/annotation><\/semantics><\/math>, llevamos a cabo el siguiente c\u00e1lculo,<\/p>\n\n\n\n<div class=\"wp-block-math\"><math display=\"block\"><semantics><mrow><mfrac><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mrow><mi>n<\/mi><mo>\u2212<\/mo><mn>1<\/mn><\/mrow><\/msub><msub><mi>w<\/mi><mi>n<\/mi><\/msub><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><mrow><msub><mo movablelimits=\"false\">\u2211<\/mo><mi>w<\/mi><\/msub><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mrow><mi>n<\/mi><mo>\u2212<\/mo><mn>1<\/mn><\/mrow><\/msub><mi>w<\/mi><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><\/mfrac><mi>.<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\frac{C(w_{n-1}w_{n})}{\\sum_{w}C(w_{n-1}w)}.<\/annotation><\/semantics><\/math><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">El siguiente c\u00f3digo en Python calcula dicha frecuencia relativa empleando el Quijote de la Mancha de Miguel de Cervantes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import re\nfrom collections import Counter, defaultdict\nfrom typing import Dict, List, Tuple\nfrom pathlib import Path\n\nWORD_RE = re.compile(r\"&#91;^\\W\\d_]+(?:&#91;-']&#91;^\\W\\d_]+)*\", re.UNICODE)\n\ndef tokenize_words(text: str, lowercase: bool = True) -&gt; List&#91;str]:\n    if lowercase:\n        text = text.lower()\n    return WORD_RE.findall(text)\n\ndef bigrams(tokens: List&#91;str], add_start_end: bool = True) -&gt; List&#91;Tuple&#91;str, str]]:\n    if add_start_end:\n        tokens = &#91;\"&lt;s&gt;\"] + tokens + &#91;\"&lt;\/s&gt;\"]\n    return list(zip(tokens&#91;:-1], tokens&#91;1:]))\n\ndef bigram_mle_words(text: str, lowercase: bool = True, add_start_end: bool = True):\n    toks = tokenize_words(text, lowercase=lowercase)\n    bi = bigrams(toks, add_start_end=add_start_end)\n\n    context_counts = Counter(&#91;w1 for (w1, w2) in bi])   # C(w_{i-1})\n    bigram_counts  = Counter(bi)                        # C(w_{i-1}, w_i)\n\n    probs: Dict&#91;str, Dict&#91;str, float]] = defaultdict(dict)\n    for (w1, w2), c12 in bigram_counts.items():\n        probs&#91;w1]&#91;w2] = c12 \/ context_counts&#91;w1]       # MLE\n\n    return {\"tokens\": toks, \"context_counts\": context_counts, \"bigram_counts\": bigram_counts, \"probs\": probs}\n\ndef top_next(probs: Dict&#91;str, Dict&#91;str, float]], prev: str, k: int = 10):\n    if prev not in probs:\n        return &#91;]\n    return sorted(probs&#91;prev].items(), key=lambda x: x&#91;1], reverse=True)&#91;:k]\n\nif __name__ == \"__main__\":\n    path = Path(\"H:\\\\quijote.txt\") \n    text = path.read_text(encoding=\"utf-8\")\n    model = bigram_mle_words(text)\n    print(\"Tokens:\", model&#91;\"tokens\"]) \n    print(\"Top despu\u00e9s de 'tropezando':\", top_next(model&#91;\"probs\"], \"tropezando\", 5))<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Al ejecutar el c\u00f3digo, podemos ver que la palabra \u00abtropezando\u00bb, aparece en cuatro oportunidades:<br><\/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\">dificultoso, <strong>tropezando<\/strong> aqu\u00ed, cayendo all\u00ed, levant\u00e1ndose acull\u00e1,<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">y <\/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\">resucitar la ya muerta andante caballer\u00eda, y ha muchos d\u00edas que, <strong>tropezando<\/strong> aqu\u00ed,<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">y<\/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\">y don Quijote, a gran priesa, <strong>tropezando<\/strong> aqu\u00ed y cayendo<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">y, finalmente en<\/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\">Quijote, van ya <strong>tropezando<\/strong>, y han de caer del todo, sin duda alguna. Vale.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Entonces, <math data-latex=\"\\sum_{w}C(\\{tropezando\\}\\{w\\}) = 4\"><semantics><mrow><msub><mo movablelimits=\"false\">\u2211<\/mo><mi>w<\/mi><\/msub><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>t<\/mi><mi>r<\/mi><mi>o<\/mi><mi>p<\/mi><mi>e<\/mi><mi>z<\/mi><mi>a<\/mi><mi>n<\/mi><mi>d<\/mi><mi>o<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>w<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>=<\/mo><mn>4<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">\\sum_{w}C(\\{tropezando\\}\\{w\\}) = 4<\/annotation><\/semantics><\/math>, <math data-latex=\"C(\\{tropezando\\}\\{aqui\\}) = 3, \"><semantics><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>t<\/mi><mi>r<\/mi><mi>o<\/mi><mi>p<\/mi><mi>e<\/mi><mi>z<\/mi><mi>a<\/mi><mi>n<\/mi><mi>d<\/mi><mi>o<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>a<\/mi><mi>q<\/mi><mi>u<\/mi><mi>i<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>=<\/mo><mn>3<\/mn><mo separator=\"true\">,<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">C(\\{tropezando\\}\\{aqui\\}) = 3, <\/annotation><\/semantics><\/math> y <math data-latex=\"C(\\{tropezando\\}\\{y\\}) = 1.\"><semantics><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>t<\/mi><mi>r<\/mi><mi>o<\/mi><mi>p<\/mi><mi>e<\/mi><mi>z<\/mi><mi>a<\/mi><mi>n<\/mi><mi>d<\/mi><mi>o<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>y<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>=<\/mo><mn>1.<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">C(\\{tropezando\\}\\{y\\}) = 1.<\/annotation><\/semantics><\/math> Luego, la probabilidad de que \u00abaqu\u00ed\u00bb sea la pr\u00f3xima palabra despu\u00e9s de \u00abtropezando\u00bb es <math data-latex=\"\\frac{3}{4}\"><semantics><mfrac><mn>3<\/mn><mn>4<\/mn><\/mfrac><annotation encoding=\"application\/x-tex\">\\frac{3}{4}<\/annotation><\/semantics><\/math> e \u00aby\u00bb, <math data-latex=\"\\frac{1}{4}\"><semantics><mfrac><mn>1<\/mn><mn>4<\/mn><\/mfrac><annotation encoding=\"application\/x-tex\">\\frac{1}{4}<\/annotation><\/semantics><\/math>, tal cual como la ejecuci\u00f3n del c\u00f3digo predice:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"527\" height=\"20\" src=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/02\/image-1.png\" alt=\"\" class=\"wp-image-85\" style=\"width:527px;height:auto\" srcset=\"https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/02\/image-1.png 527w, https:\/\/pulki.es\/blog\/wp-content\/uploads\/2026\/02\/image-1-300x11.png 300w\" sizes=\"auto, (max-width: 527px) 100vw, 527px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Tambi\u00e9n, con fines did\u00e1cticos podemos empezar con una palabra, y elegir las que exhiben la probabilidad m\u00e1s alta de ser las siguientes. A modo de ejemplo, si partimos con la palabra \u00abFaza\u00f1a\u00bb (gracias Aradia), la secuencia que generamos es:<\/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\">Faza\u00f1a fue el cual se le dijo don quijote<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Donde las probabilidades son:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Palabra<\/strong><\/td><td><strong>Probabilidad<\/strong><\/td><\/tr><tr><td>Faza\u00f1a<\/td><td>&#8211;<\/td><\/tr><tr><td>fue<\/td><td>1<\/td><\/tr><tr><td>el<\/td><td>0.0998<\/td><\/tr><tr><td>cual<\/td><td>0.0343<\/td><\/tr><tr><td>se<\/td><td>0.0639<\/td><\/tr><tr><td>le<\/td><td>0.0677<\/td><\/tr><tr><td>dijo<\/td><td>0.0662<\/td><\/tr><tr><td>don<\/td><td>0.1648<\/td><\/tr><tr><td>quijote<\/td><td>0.8182<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Tambi\u00e9n, podemos calcular la probabilidad de esta frase como<\/p>\n\n\n\n<div class=\"wp-block-math\"><math display=\"block\"><semantics><mrow><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>F<\/mi><mi>a<\/mi><mi>z<\/mi><mi>a<\/mi><mover><mi>n<\/mi><mo stretchy=\"false\" style=\"math-style:normal;math-depth:0;\">~<\/mo><\/mover><mi>a<\/mi><mtext>&nbsp;<\/mtext><mi>f<\/mi><mi>u<\/mi><mi>e<\/mi><mtext>&nbsp;<\/mtext><mi>e<\/mi><mi>l<\/mi><mtext>&nbsp;<\/mtext><mi>c<\/mi><mi>u<\/mi><mi>a<\/mi><mi>l<\/mi><mtext>&nbsp;<\/mtext><mi>s<\/mi><mi>e<\/mi><mtext>&nbsp;<\/mtext><mi>l<\/mi><mi>e<\/mi><mtext>&nbsp;<\/mtext><mi>d<\/mi><mi>i<\/mi><mi>j<\/mi><mi>o<\/mi><mtext>&nbsp;<\/mtext><mi>d<\/mi><mi>o<\/mi><mi>n<\/mi><mtext>&nbsp;<\/mtext><mi>q<\/mi><mi>u<\/mi><mi>i<\/mi><mi>j<\/mi><mi>o<\/mi><mi>t<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>=<\/mo><mn>1<\/mn><mo>\u00d7<\/mo><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>f<\/mi><mi>u<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>F<\/mi><mi>a<\/mi><mi>z<\/mi><mi>a<\/mi><mover><mi>n<\/mi><mo stretchy=\"false\" style=\"math-style:normal;math-depth:0;\">~<\/mo><\/mover><mi>a<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>\u00d7<\/mo><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>e<\/mi><mi>l<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>f<\/mi><mi>u<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>\u00d7<\/mo><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>c<\/mi><mi>u<\/mi><mi>a<\/mi><mi>l<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>e<\/mi><mi>l<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>\u00d7<\/mo><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>s<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>c<\/mi><mi>u<\/mi><mi>a<\/mi><mi>l<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>\u00d7<\/mo><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>l<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>s<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>\u00d7<\/mo><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>d<\/mi><mi>i<\/mi><mi>j<\/mi><mi>o<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>l<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>\u00d7<\/mo><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>d<\/mi><mi>o<\/mi><mi>n<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>d<\/mi><mi>i<\/mi><mi>j<\/mi><mi>o<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>\u00d7<\/mo><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>q<\/mi><mi>u<\/mi><mi>i<\/mi><mi>j<\/mi><mi>o<\/mi><mi>t<\/mi><mi>e<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mi>|<\/mi><mo form=\"prefix\" stretchy=\"false\">{<\/mo><mi>d<\/mi><mi>o<\/mi><mi>n<\/mi><mo form=\"postfix\" stretchy=\"false\">}<\/mo><mo form=\"postfix\" stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">P(\\{Faza\u00f1a \\ fue \\ el \\ cual \\ se \\ le  \\ dijo \\  don  \\ quijote\\}) = 1 \\times  P(\\{fue\\}|\\{Faza\u00f1a\\})\\times P(\\{el\\}|\\{fue\\})\\times P(\\{cual\\}|\\{el\\})\\times P(\\{se\\}|\\{cual\\}) \\times P(\\{le\\}|\\{se\\}) \\times P(\\{dijo\\}|\\{le\\}) \\times P(\\{don\\}|\\{dijo\\})\\times P(\\{quijote\\}|\\{don\\})<\/annotation><\/semantics><\/math><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">cuyo valor viene dado por:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>0.00000013218703<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Como podemos ver, dicho valor es peque\u00f1o, al igual que el de las probabilidades condicionales. Cuando <strong>n<\/strong>, o nuestro modelo crece, las probabilidades pueden ser a\u00fan m\u00e1s peque\u00f1as, provocando un problema conocido como <em><strong>numerical underflow<\/strong><\/em>. Para evitarlo, usualmente se guardan las probabilidades en log, y luego, si queremos recuperar el n\u00famero original, s\u00f3lo debemos tomar el exponente. Este truco ser\u00e1 utilizado m\u00e1s adelante.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Evaluando modelos de lenguaje<\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La mejor manera de evaluar modelos es mediante la <strong><em>evaluaci\u00f3n extr\u00ednseca<\/em><\/strong>. En t\u00e9rminos concretos, si por ejemplo, queremos evaluar que tan bueno es un autocomplete que estamos desarrollando, una evaluaci\u00f3n extr\u00ednseca ser\u00eda determinar el porcentaje de sugerencias que fueron aceptadas por el escritor. Debido a que esta evaluaci\u00f3n es costosa, usualmente se lleva a cabo otro m\u00e9todo llamado <strong><em>evaluaci\u00f3n intr\u00ednseca<\/em><\/strong>. Este tipo de evaluaci\u00f3n, mide un modelo por una m\u00e9trica interna que refleja qu\u00e9 tan bien cumple su objetivo directo, es decir, mide la calidad del modelo independiente de su aplicaci\u00f3n. Dentro de este tipo de evaluaciones, una popular es llamada <strong><em>perplexity<\/em><\/strong>. En la siguiente secci\u00f3n la veremos en detalle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Evaluaci\u00f3n intr\u00ednseca: Perplexity. <\/em><\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Volvamos al ejemplo del <em>autocomplete<\/em>. Supongamos que entrenamos un modelo de bigramas con el texto del Quijote de la Mancha. Posteriormente, como es usual, evaluamos su eficacia en un texto de prueba. Si nuestro modelo predice mejor las siguientes palabras (o tokens) en ese conjunto, entonces estar\u00e1 \u201cmenos sorprendido\u201d cuando aparezca la palabra correcta, en comparaci\u00f3n con otro modelo. En el caso ideal, un <em>autocomplete<\/em> muy bueno asignar\u00eda una probabilidad<strong> <\/strong>muy alta a la palabra correcta y muy baja a las alternativas incorrectas.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dicho esto, es intuitivo pensar que la probabilidad que el modelo asigna al texto de prueba debe ser un ingrediente relevante para una m\u00e9trica. Sin embargo, no se suele usar la probabilidad total del texto, porque depende fuertemente de su longitud: al ser un producto de muchas probabilidades condicionales, disminuye a medida que el texto se hace m\u00e1s largo. Por eso, una m\u00e9trica adecuada debe normalizar por la cantidad de palabras (o tokens), y medir el desempe\u00f1o <strong>promedio por token<\/strong>; de ah\u00ed surge la <em>perplexity<\/em> . En particular, su f\u00f3rmula viene dada por:<\/p>\n\n\n\n<div class=\"wp-block-math\"><math display=\"block\"><semantics><mrow><mi>p<\/mi><mi>e<\/mi><mi>r<\/mi><mi>p<\/mi><mi>l<\/mi><mi>e<\/mi><mi>x<\/mi><mi>i<\/mi><mi>t<\/mi><mi>y<\/mi><mo>=<\/mo><mi>\u2119<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo separator=\"true\">,<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mo separator=\"true\">,<\/mo><mi>.<\/mi><mi>.<\/mi><mi>.<\/mi><mo separator=\"true\">,<\/mo><msub><mi>w<\/mi><mi>N<\/mi><\/msub><msup><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mrow><mo lspace=\"0em\" rspace=\"0em\">\u2212<\/mo><mfrac><mn>1<\/mn><mi>N<\/mi><\/mfrac><\/mrow><\/msup><\/mrow><annotation encoding=\"application\/x-tex\">perplexity = \\mathbb{P}(w_{1},w_{2},&#8230;,w_{N})^{-\\frac{1}{N}}<\/annotation><\/semantics><\/math><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Al observar la f\u00f3rmula, podemos ver que depende inversamente de la probabilidad conjunta. Por este motivo, una menor \u00abperplejidad\u00bb, implica un mejor modelo. Teniendo esto en mente, a continuaci\u00f3n, presentamos el c\u00f3digo que calcula la perplejidad para el modelo de bigramas entrenado en El Quijote de la Mancha, y que usa Fuente Ovejuna de Lope de Vega como texto de prueba (lo puedes encontrar <a href=\"https:\/\/gutenberg.org\/cache\/epub\/60198\/pg60198.txt\">aqu\u00ed<\/a>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import re\nimport math\nfrom collections import Counter\nfrom typing import List, Tuple\nfrom pathlib import Path\n\nWORD_RE = re.compile(r\"&#91;^\\W\\d_]+(?:&#91;-']&#91;^\\W\\d_]+)*\", re.UNICODE)\n\ndef tokenize_words(text: str, lowercase: bool = True) -&gt; List&#91;str]:\n    if lowercase:\n        text = text.lower()\n    return WORD_RE.findall(text)\n\ndef bigrams(tokens: List&#91;str], add_start_end: bool = True) -&gt; List&#91;Tuple&#91;str, str]]:\n    if add_start_end:\n        tokens = &#91;\"&lt;s&gt;\"] + tokens + &#91;\"&lt;\/s&gt;\"]\n    return list(zip(tokens&#91;:-1], tokens&#91;1:]))\n\ndef train_bigram_counts(train_text: str, lowercase: bool = True):\n    toks = tokenize_words(train_text, lowercase=lowercase)\n    bi = bigrams(toks, add_start_end=True)\n\n    context_counts = Counter(&#91;w1 for (w1, _) in bi])  # C(w_{i-1})\n    bigram_counts  = Counter(bi)                      # C(w_{i-1}, w_i)\n\n    # vocab del train (incluye tokens especiales)\n    vocab = set(toks)\n    vocab.update(&#91;\"&lt;s&gt;\", \"&lt;\/s&gt;\", \"&lt;unk&gt;\"])\n\n    return context_counts, bigram_counts, vocab\n\ndef perplexity_bigram_addk(\n    test_text: str,\n    context_counts: Counter,\n    bigram_counts: Counter,\n    vocab: set,\n    k: float = 1.0,           # k=1.0 =&gt; Laplace; k=0.1 =&gt; add-0.1; k=0 =&gt; MLE puro (puede dar inf)\n    lowercase: bool = True\n) -&gt; float:\n    # Tokeniza test y mapea OOV a &lt;unk&gt;\n    toks = tokenize_words(test_text, lowercase=lowercase)\n    toks = &#91;t if t in vocab else \"&lt;unk&gt;\" for t in toks]\n\n    bi = bigrams(toks, add_start_end=True)\n    V = len(vocab)\n\n    # log-prob promedio\n    log_sum = 0.0\n    N = 0\n\n    for w1, w2 in bi:\n        c12 = bigram_counts.get((w1, w2), 0)\n        c1  = context_counts.get(w1, 0)\n\n        if k == 0.0:\n            # MLE puro: si c12=0 =&gt; prob=0 =&gt; perplexity infinita\n            if c12 == 0 or c1 == 0:\n                return float(\"inf\")\n            p = c12 \/ c1\n        else:\n            # add-k smoothing:\n            # P(w2|w1) = (C(w1,w2)+k) \/ (C(w1)+k*V)\n            p = (c12 + k) \/ (c1 + k * V)\n\n        log_sum += math.log(p)\n        N += 1\n\n    # perplexity = exp(- (1\/N) * sum log p)\n    return math.exp(-log_sum \/ N)\n\nif __name__ == \"__main__\":\n    quijote_path = Path(r\"H:\\quijote.txt\")\n    fuente_path  = quijote_path.with_name(\"fuenteovejuna.txt\")\n\n    train_text = quijote_path.read_text(encoding=\"utf-8\")\n    test_text  = fuente_path.read_text(encoding=\"utf-8\")\n\n    context_counts, bigram_counts, vocab = train_bigram_counts(train_text)\n\n    ppl = perplexity_bigram_addk(\n        test_text,\n        context_counts,\n        bigram_counts,\n        vocab,\n        k=1.0  # prueba tambi\u00e9n k=0.1 o k=0.01\n    )\n\n    print(f\"Vocab size (train): {len(vocab)}\")\n    print(f\"Perplexity (Fuenteovejuna | trained on Quijote) with add-k={1.0}: {ppl:.4f}\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En t\u00e9rminos generales, este c\u00f3digo empieza ignorando puntuaci\u00f3n y n\u00fameros, pero permitiendo ap\u00f3strofes y guiones. Luego, la funci\u00f3n tokenize_words() pasa todo a min\u00fasculas y devuelve una lista de tokens (que en este caso son palabras completas). bigrams() construye las secuencias de dos palabras, para luego crear pares consecutivos, es decir, si en la variable tokens entra una lista similar a esta:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tokens = &#91;\"don\", \"quijote\", \"vive\"]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">el c\u00f3digo devuelve pares consecutivos con el siguiente aspecto:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;(\"don\", \"quijote\"), (\"quijote\", \"vive\")]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">train_bigram_counts() lleva a cabo el conteo de esos pares para que sea posible calcular<\/p>\n\n\n\n<div class=\"wp-block-math\"><math display=\"block\"><semantics><mrow><mover><mrow><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mi>|<\/mi><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><mo stretchy=\"false\" style=\"math-style:normal;math-depth:0;\">^<\/mo><\/mover><mo>=<\/mo><mfrac><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo separator=\"true\">,<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><\/mfrac><mi>.<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\hat{P(w_{2}|w_{1})} = \\frac{C(w_{1},w_{2})}{C(w_{1})}.<\/annotation><\/semantics><\/math><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Luego, al vocabulario visto en el texto de entrenamiento se le a\u00f1ade &lt;unk&gt; para posteriormente representar palabras desconocidas en el texto de prueba.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Finalmente, para calcular la perplejidad, primero, detectamos las palabras desconocidas, es decir, palabras que est\u00e1n en el texto de prueba y que no se encuentran en el vocabulario generado por el texto de entrenamiento:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>toks = tokenize_words(test_text)\ntoks = &#91;t if t in vocab else \"&lt;unk&gt;\" for t in toks]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">para posteriormente, hacer el conteo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>c12 = bigram_counts.get((w1, w2), 0)\nc1 = context_counts.get(w1, 0) <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">que espec\u00edficamente, calcula <math data-latex=\"C(w_{1},w_{2})\"><semantics><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo separator=\"true\">,<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">C(w_{1},w_{2})<\/annotation><\/semantics><\/math> y <math data-latex=\"C(w_{1})\"><semantics><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">C(w_{1})<\/annotation><\/semantics><\/math> del texto de entrenamiento, para <math data-latex=\"w_{1}\"><semantics><msub><mi>w<\/mi><mn>1<\/mn><\/msub><annotation encoding=\"application\/x-tex\">w_{1}<\/annotation><\/semantics><\/math> y <math data-latex=\"w_{2}\"><semantics><msub><mi>w<\/mi><mn>2<\/mn><\/msub><annotation encoding=\"application\/x-tex\">w_{2}<\/annotation><\/semantics><\/math> del texto de prueba. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En este punto, a pesar de tener el conteo del numerador y denominador, a\u00fan no podemos realizar la divisi\u00f3n. Pese a que la fracci\u00f3n est\u00e1 blindada de la divisi\u00f3n por cero (puesto que al incluir &lt;unk&gt;, <math data-latex=\"C(w_{1})\"><semantics><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">C(w_{1})<\/annotation><\/semantics><\/math> nunca ser\u00e1 cero), la fracci\u00f3n<\/p>\n\n\n\n<div class=\"wp-block-math\"><math display=\"block\"><semantics><mrow><mover><mrow><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mi>|<\/mi><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><mo stretchy=\"false\" style=\"math-style:normal;math-depth:0;\">^<\/mo><\/mover><mo>=<\/mo><mfrac><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo separator=\"true\">,<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><\/mfrac><\/mrow><annotation encoding=\"application\/x-tex\">\\hat{P(w_{2}|w_{1})} = \\frac{C(w_{1},w_{2})}{C(w_{1})}<\/annotation><\/semantics><\/math><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">puede ser efectivamente cero, si <math data-latex=\"C(w_{1},w_{2})\"><semantics><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo separator=\"true\">,<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\">)<\/mo><\/mrow><annotation encoding=\"application\/x-tex\">C(w_{1},w_{2})<\/annotation><\/semantics><\/math> es cero, es decir, si no se encontr\u00f3 un bigrama similar en el texto de prueba. Esto es un problema por dos motivos. El primero, es que la perplejidad no se puede computar, puesto que es el inverso de la probabilidad, y debido a que es cero, estar\u00edamos ante una divisi\u00f3n por cero. El segundo, es que, que no aparezca el bigrama en el texto de entrenamiento, implica directamente que tiene probabilidad 0, lo que lo convierte en un evento imposible, pero realmente no es imposible, muy probablemente es s\u00f3lo una manifestaci\u00f3n de un corpus de entrenamiento limitado, lo que en la practica, puede socavar muchas aplicaciones. Principalmente por estos motivos, es necesario llevar a cabo un <em>smoothing<\/em>. Existen m\u00faltiples tipos, pero nosotros s\u00f3lo veremos el <em>add-k<\/em>, cuyo principio es a\u00f1adir <em>k<\/em> unidades al numerador, y <em>kV<\/em> unidades al denominador, para evitar la divisi\u00f3n por cero:<\/p>\n\n\n\n<div class=\"wp-block-math\"><math display=\"block\"><semantics><mrow><mover><mrow><mi>P<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mi>|<\/mi><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\" lspace=\"0em\" rspace=\"0em\">)<\/mo><\/mrow><mo stretchy=\"false\" style=\"math-style:normal;math-depth:0;\">^<\/mo><\/mover><mo>=<\/mo><mfrac><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo separator=\"true\">,<\/mo><msub><mi>w<\/mi><mn>2<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>+<\/mo><mi>k<\/mi><\/mrow><mrow><mi>C<\/mi><mo form=\"prefix\" stretchy=\"false\">(<\/mo><msub><mi>w<\/mi><mn>1<\/mn><\/msub><mo form=\"postfix\" stretchy=\"false\">)<\/mo><mo>+<\/mo><mi>k<\/mi><mi>V<\/mi><\/mrow><\/mfrac><mi>.<\/mi><\/mrow><annotation encoding=\"application\/x-tex\">\\hat{P(w_{2}|w_{1})} = \\frac{C(w_{1},w_{2}) + k}{C(w_{1}) + kV}.<\/annotation><\/semantics><\/math><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Por simplicidad, asumiremos k=1 (conocido como Laplace smoothing), y posteriormente calcularemos la perplejidad en logs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>log_sum += math.log(p)\nN += 1\n...\n...\nreturn math.exp(-log_sum \/ N)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">cuyo n\u00famero viene dado por <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><kbd>Perplexity (Fuenteovejuna | trained on Quijote) with add-k=1.0: 7196.6119<\/kbd><\/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 una estrategia llamada <strong>n-gram<\/strong>s para predecir la siguiente palabra (o token) en un texto, condicionada a un contexto. Ese contexto puede ser la palabra inmediatamente anterior o, en general, las \u00faltimas <math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>n<\/mi><mo>\u2212<\/mo><mn>1<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">n-1<\/annotation><\/semantics><\/math> palabras. Dicha predicci\u00f3n se lleva a cabo con probabilidades condicionales que pueden ser estimadas mediante <strong>M\u00e1xima Verosimilitud (MLE)<\/strong> a partir de un corpus de entrenamiento. Con fines did\u00e1cticos, fijamos <math xmlns=\"http:\/\/www.w3.org\/1998\/Math\/MathML\"><semantics><mrow><mi>n<\/mi><mo>=<\/mo><mn>2<\/mn><\/mrow><annotation encoding=\"application\/x-tex\">n=2<\/annotation><\/semantics><\/math>, entrenamos un modelo de bigramas usando <em>El Quijote de la Mancha<\/em> y, finalmente, calculamos la <strong>perplejidad<\/strong>, un estad\u00edstico que permite realizar una evaluaci\u00f3n intr\u00ednseca del modelo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Esta estrategia es una base importante para el aprendizaje, porque captura uno de los principios centrales detr\u00e1s del funcionamiento de los <strong>LLMs<\/strong>: modelar el lenguaje como una distribuci\u00f3n de probabilidad sobre el siguiente token.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En la pr\u00f3xima entrega veremos c\u00f3mo clasificar texto, un tema clave en procesamiento del lenguaje natural. 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 un ejemplo pr\u00e1ctico del algoritmo BPE. Entender esto es importante porque es la forma est\u00e1ndar en que las m\u00e1quinas descomponen un texto en una representaci\u00f3n que pueden manipular. Puesto que ahora sabemos hacerlo, el siguiente paso l\u00f3gico es emplear esos fragmentos (o tokens) de alguna manera, para as\u00ed generar texto [&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-79","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/79","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=79"}],"version-history":[{"count":15,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/79\/revisions"}],"predecessor-version":[{"id":96,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/posts\/79\/revisions\/96"}],"wp:attachment":[{"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=79"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=79"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pulki.es\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=79"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}