<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Ragunath S on Medium]]></title>
        <description><![CDATA[Stories by Ragunath S on Medium]]></description>
        <link>https://medium.com/@ragunath.4s?source=rss-1eb7398e529b------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*AzsqZ_Yk3OAcrSKN</url>
            <title>Stories by Ragunath S on Medium</title>
            <link>https://medium.com/@ragunath.4s?source=rss-1eb7398e529b------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 03:49:29 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@ragunath.4s/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Tokens aren’t words]]></title>
            <link>https://medium.com/@ragunath.4s/tokens-arent-words-a1746e9efdde?source=rss-1eb7398e529b------2</link>
            <guid isPermaLink="false">https://medium.com/p/a1746e9efdde</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[from-scratch]]></category>
            <dc:creator><![CDATA[Ragunath S]]></dc:creator>
            <pubDate>Mon, 14 Sep 2026 00:36:47 GMT</pubDate>
            <atom:updated>2026-09-14T00:36:47.438Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>A next-token engine is not a database with manners — token IDs, scale, and lookup vs generate.</strong></p><p><em>Build LLMs from Scratch · Issue 2 · also on </em><a href="https://ragunathselvaraj.substack.com/p/issue-2-tokens-arent-words-a-next"><em>the newsletter</em></a></p><p><a href="https://ragunathselvaraj.substack.com/p/issue-1-stop-renting-the-kitchen">Issue 1</a> gave you the loop: pieces so far → bet on the next one → append → repeat.</p><p>Same job today. New grain: <strong>token IDs</strong>. And one myth to retire — that an LLM is a polite search box over a giant hard drive.</p><h3>Recap, then the new idea</h3><p>Last time the pieces were characters. You printed a count table. You watched ugly text crawl out of random.choices. Honesty, not fluency.</p><p>Tonight the pieces become integers. Same loop. We put that loop next to a dumb exact-string lookup so you can feel <strong>retrieve</strong> versus <strong>generate</strong>.</p><blockquote>An LLM is a next-token machine. Scale makes it look like understanding. It is still not a database.</blockquote><h3>What an LLM actually is</h3><p>Three facts, then a lab.</p><p><strong>Tokens are pieces, not words.</strong> Your eyes want “unbelievable” to be one unit. A real tokenizer may crack it into several IDs. The model never reads like you do. It reads integers, turns each into a vector, and bets on the next ID. Words are a human convenience. Tokens are the machine’s alphabet.</p><p><strong>“Large” is about knobs, not manners.</strong> A small table finishes your <em>text</em>. A model with millions, then billions, of weights finishes your <em>argument</em> — sometimes. Scale buys coverage and style, not honesty. Fluently wrong is still wrong; it just sounds finished.</p><p><strong>Old NLP was a drawer of single tools</strong> (sentiment here, translation there). An LLM is closer to a multi-tool — still worth sharpening later. Sharpening is Issue 3’s neighborhood. Tonight stays on identity.</p><p>Buzzword nest, one breath: AI ⊃ machine learning ⊃ deep learning ⊃ generative AI ⊃ LLMs (the language corner). Image and audio generators are GenAI too. Not every GenAI thing is an LLM.</p><p>What it is <strong>not</strong>: a search engine. Not a database with manners. Lookup returns a line that already existed. Generate samples a continuation that is <em>plausible given the stats</em>. Sometimes that matches a memorized phrase. Often it is a remix. The mechanism is still “what ID is likely next?”</p><h3>Diagram: lookup vs generate</h3><figure><img alt="Same corpus, two machines — lookup vs generate" src="https://cdn-images-1.medium.com/max/1024/1*ai4lKZMRKNtjLb3MQ5vOkQ.png" /></figure><p><em>Lookup returns stored bytes. Generate samples the next token ID. Only generate is what an LLM does.</em></p><p><strong>Same corpus, two machines</strong></p><p>Corpus on disk: &quot;maya opened the notebook. …&quot;</p><p><strong>LOOKUP (database move)</strong> 1. Query: &quot;maya opened the&quot; 2. Find that exact leftover in the text 3. Return the bytes that follow — or shrug / None on a miss</p><p><strong>GENERATE (LLM move)</strong> 1. Encode the query into token IDs 2. Look at counts (or weights) after the last ID 3. Sample a next ID → append → repeat 4. Miss on the exact string? It still answers — with a guess</p><p>Same corpus. Two machines. Only one of them is what an LLM is doing.</p><h3>Lab: token IDs are not a database</h3><p>Upgrade Issue 1’s character table to <strong>token IDs</strong>, then contrast exact leftover-string search with sampling the next ID.</p><p>Save as token_ids_not_a_db.py and run python3 token_ids_not_a_db.py.</p><pre>&quot;&quot;&quot;Issue 2 lab — token IDs, next-ID counts, lookup vs generate. Stdlib only.&quot;&quot;&quot;<br> <br>import random<br>import re<br>from collections import defaultdict<br> <br>CORPUS = &quot;&quot;&quot;<br>Ravi packed a lunch. Maya packed a notebook.<br>Ravi opened the lunch. Maya opened the notebook.<br>The lunch smelled good. The notebook smelled like ink.<br>Ravi wrote unbelievable notes. Maya wrote unbelievable jokes.<br>Notes help. Jokes help. Lunch helps too.<br>&quot;&quot;&quot;<br> <br>START_ID = 0  # reserved id for start-of-text<br> <br> <br>def tokenize(text: str) -&gt; list[str]:<br>    &quot;&quot;&quot;Split into word-ish pieces: letters/digits kept, punctuation alone.&quot;&quot;&quot;<br>    text = text.lower().strip()<br>    return re.findall(r&quot;[a-z0-9]+|[.,!?;:]&quot;, text)<br> <br> <br>def build_vocab(pieces: list[str]) -&gt; dict[str, int]:<br>    &quot;&quot;&quot;Map each unique piece to an integer ID. 0 is START.&quot;&quot;&quot;<br>    vocab = {&quot;&lt;start&gt;&quot;: START_ID}<br>    for p in pieces:<br>        if p not in vocab:<br>            vocab[p] = len(vocab)<br>    return vocab<br> <br> <br>def encode(pieces: list[str], vocab: dict[str, int]) -&gt; list[int]:<br>    return [vocab[p] for p in pieces]<br> <br> <br>def invert_vocab(vocab: dict[str, int]) -&gt; dict[int, str]:<br>    return {i: p for p, i in vocab.items()}<br> <br> <br>def build_next_id_table(ids: list[int]) -&gt; dict[int, dict[int, int]]:<br>    &quot;&quot;&quot;Like Issue 1&#39;s char table, but over token IDs.&quot;&quot;&quot;<br>    seq = [START_ID] + ids<br>    table: dict[int, dict[int, int]] = defaultdict(lambda: defaultdict(int))<br>    for a, b in zip(seq, seq[1:]):<br>        table[a][b] += 1<br>    return table<br> <br> <br>def sample_next(table: dict[int, dict[int, int]], current: int) -&gt; int:<br>    followers = table.get(current)<br>    if not followers:<br>        return START_ID<br>    ids, weights = zip(*followers.items())<br>    return random.choices(ids, weights=weights, k=1)[0]<br> <br> <br>def generate_ids(table: dict[int, dict[int, int]], n: int = 12) -&gt; list[int]:<br>    cur = START_ID<br>    out = []<br>    for _ in range(n):<br>        cur = sample_next(table, cur)<br>        if cur == START_ID:<br>            break<br>        out.append(cur)<br>    return out<br> <br> <br>def lookup_exact(corpus: str, leftover: str) -&gt; str | None:<br>    &quot;&quot;&quot;Dumb &#39;database&#39;: find leftover as an exact substring; return what follows.&quot;&quot;&quot;<br>    hay = &quot; &quot;.join(corpus.lower().split())<br>    needle = &quot; &quot;.join(leftover.lower().split())<br>    idx = hay.find(needle)<br>    if idx &lt; 0:<br>        return None<br>    after = hay[idx + len(needle) :].strip()<br>    if not after:<br>        return &quot;&quot;<br>    # return the next few words after the match<br>    return &quot; &quot;.join(after.split()[:6])<br> <br> <br>def decode(ids: list[int], id_to_piece: dict[int, str]) -&gt; str:<br>    parts = [id_to_piece[i] for i in ids]<br>    out = []<br>    for p in parts:<br>        if p in &quot;.,!?;:&quot;:<br>            out.append(p)<br>        elif not out:<br>            out.append(p)<br>        else:<br>            out.append(&quot; &quot; + p)<br>    return &quot;&quot;.join(out)<br> <br> <br>if __name__ == &quot;__main__&quot;:<br>    random.seed(2)<br> <br>    pieces = tokenize(CORPUS)<br>    vocab = build_vocab(pieces)<br>    id_to_piece = invert_vocab(vocab)<br>    ids = encode(pieces, vocab)<br>    table = build_next_id_table(ids)<br> <br>    demo = &quot;Ravi wrote unbelievable notes.&quot;<br>    demo_pieces = tokenize(demo)<br>    demo_ids = encode(demo_pieces, vocab)<br> <br>    print(&quot;1) Sentence → pieces → IDs&quot;)<br>    print(&quot;   text:  &quot;, demo)<br>    print(&quot;   pieces:&quot;, demo_pieces)<br>    print(&quot;   ids:   &quot;, demo_ids)<br>    print(&quot;   note:  &#39;unbelievable&#39; is ONE piece here (our toy splitter);&quot;)<br>    print(&quot;          real tokenizers often crack it into several IDs.&quot;)<br>    print()<br> <br>    # pick an ID that appears mid-corpus<br>    probe_piece = &quot;wrote&quot;<br>    probe_id = vocab[probe_piece]<br>    followers = {id_to_piece[i]: c for i, c in table[probe_id].items()}<br>    print(f&quot;2) What follows ID {probe_id} (&#39;{probe_piece}&#39;)?&quot;)<br>    print(&quot;  &quot;, followers)<br>    print()<br> <br>    gen_ids = generate_ids(table, n=14)<br>    print(&quot;3) Generate (sample next IDs from the count table)&quot;)<br>    print(&quot;   ids: &quot;, gen_ids)<br>    print(&quot;   text:&quot;, decode(gen_ids, id_to_piece))<br>    print()<br> <br>    hit_query = &quot;maya opened the&quot;<br>    miss_query = &quot;ravi opened the notebook&quot;<br>    print(&quot;4) Lookup vs generate&quot;)<br>    print(&quot;   lookup hit for&quot;, repr(hit_query), &quot;→&quot;, repr(lookup_exact(CORPUS, hit_query)))<br>    print(&quot;   lookup miss for&quot;, repr(miss_query), &quot;→&quot;, repr(lookup_exact(CORPUS, miss_query)))<br>    last = tokenize(miss_query)[-1]<br>    last_id = vocab[last]<br>    cur = last_id<br>    cont = []<br>    for _ in range(5):<br>        cur = sample_next(table, cur)<br>        if cur == START_ID:<br>            break<br>        cont.append(cur)<br>    print(<br>        &quot;   generate after ID&quot;,<br>        last_id,<br>        f&quot;(&#39;{last}&#39;) →&quot;,<br>        repr(decode(cont, id_to_piece)),<br>        &quot;(fluent guess, not a retrieved line)&quot;,<br>    )</pre><p>With random.seed(2) you should see: the demo sentence as pieces and IDs; followers of wrote → unbelievable; a mashed-up generated line; a <strong>lookup hit</strong> for maya opened the, a <strong>lookup miss</strong> for ravi opened the notebook (never written that way), and a <strong>generate</strong> guess after notebook anyway.</p><p><strong>Lookup vs generate (cheat sheet)</strong></p><ul><li><strong>Lookup:</strong> needs an exact leftover · returns stored bytes · miss = empty hands</li><li><strong>Generate:</strong> needs the last token ID · samples a plausible next ID · miss on the string? still talks</li></ul><h3>What you built today</h3><ul><li>A tiny tokenizer: text → word-ish pieces → integer IDs.</li><li>A next-ID count table: Issue 1’s engine, coarser grain.</li><li>A side-by-side: exact leftover lookup vs sampling the next ID.</li><li>A sharper definition: <strong>LLM = scaled next-token machine</strong>, not a polite hard drive.</li></ul><p>You did <strong>not</strong> build a real subword tokenizer, a transformer, or a chatbot. You named the job and showed why “it answered” ≠ “it found the row.”</p><h3>Next issue</h3><p>Issue 3: pretrain vs finetune — same engine, different stages of teaching it.</p><p>Change CORPUS to a paragraph you wrote, keep the miss-query deliberately <em>almost</em> right, and watch lookup shrug while generate keeps talking. That shrug is the point.</p><p>—</p><p><em>Further reading (optional):</em> Sebastian Raschka, <em>Build a Large Language Model (From Scratch)</em> — a full book-length path if you want the long form after these labs. This issue is not a summary of that book.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a1746e9efdde" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stop renting the kitchen]]></title>
            <link>https://medium.com/@ragunath.4s/stop-renting-the-kitchen-20ea5fbcadef?source=rss-1eb7398e529b------2</link>
            <guid isPermaLink="false">https://medium.com/p/20ea5fbcadef</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[from-scratch]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Ragunath S]]></dc:creator>
            <pubDate>Mon, 14 Sep 2026 00:25:54 GMT</pubDate>
            <atom:updated>2026-09-14T00:25:54.637Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>A baby next-token engine in plain Python — no libraries, no API key.</strong></p><p><em>Build LLMs from Scratch · Issue 1 · also on </em><a href="https://ragunathselvaraj.substack.com/p/issue-1-stop-renting-the-kitchen"><em>the newsletter</em></a></p><p>Most people “learn LLMs” the way you’d learn restaurants by ordering takeout.</p><p>You call an API. Something fluent comes back. You ship a demo. Then someone asks <em>what is a token?</em> or <em>why is it confidently wrong?</em> and the whole thing turns into a shrug.</p><p>This series is the kitchen. One mini-lab per issue. Plain English, one picture, code you can paste into a file and run. No paid model. No framework. No branding tour.</p><p>Today you will not train GPT. You will build the <em>job</em> an LLM is actually doing — predict the next piece — so the rest of the series has somewhere to sit.</p><h3>The trap</h3><p>The usual path looks like this:</p><ol><li>Open a chat UI.</li><li>Wrap it in a script.</li><li>Tell people you “built an LLM app.”</li><li>Freeze when the output is weird and you cannot name the moving parts.</li></ol><p>That path is fine for product demos. It is a terrible foundation if you want to debug, interview, or train anything yourself.</p><p>Two reasons to go slower:</p><ul><li><strong>Debug intuition.</strong> You will know what prompting can fix, what finetuning is for, and what only a better base model can buy.</li><li><strong>Builder pride.</strong> Open weights and closed APIs are both tools. Understanding the machine is the skill.</li></ul><h3>The only job (for now)</h3><p>Strip the costume and a language model is answering one question:</p><blockquote>Given the pieces so far, what comes next?</blockquote><p>Do that well, at huge scale, and it <em>looks</em> like writing, reasoning, and coding. The look is a side effect. The job is next-piece prediction.</p><p>In later issues those “pieces” will be tokens (subword IDs). Tonight they are characters, so you can see the loop with your eyes.</p><p><strong>The loop (read top to bottom, then jump back):</strong></p><ol><li>Start with raw text</li><li>Break it into pieces</li><li>Look at the context so far</li><li>Bet on the next piece</li><li>Append it</li><li>Go back to step 3 and repeat</li></ol><p>That loop is the engine. Chat UIs are the dining room.</p><h3>Lab: a next-token engine with zero libraries</h3><p>We will count how often one character follows another in a tiny story, then sample the next character from those counts. No torch. No API key. If this runs, you have a baby language model.</p><p>Save as baby_next_token.py and run python3 baby_next_token.py.</p><pre>&quot;&quot;&quot;Issue 1 lab — character-level next-token engine. Stdlib only.&quot;&quot;&quot;<br> <br>import random<br>from collections import defaultdict<br> <br>STORY = &quot;&quot;&quot;<br>sam sat. sam saw a cat. the cat sat. sam saw the sun.<br>the sun sat. a cat saw sam. sam sat in the sun.<br>&quot;&quot;&quot;<br> <br>START = &quot;~&quot;  # fake &quot;start of text&quot; so we can open a sentence<br> <br> <br>def build_table(text: str) -&gt; dict[str, dict[str, int]]:<br>    &quot;&quot;&quot;For each character, count what comes next.&quot;&quot;&quot;<br>    text = START + &quot;&quot;.join(text.lower().split())  # squash whitespace<br>    table: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))<br>    for a, b in zip(text, text[1:]):<br>        table[a][b] += 1<br>    return table<br> <br> <br>def sample_next(table: dict[str, dict[str, int]], current: str) -&gt; str:<br>    followers = table.get(current)<br>    if not followers:<br>        return &quot;.&quot;<br>    chars, weights = zip(*followers.items())<br>    return random.choices(chars, weights=weights, k=1)[0]<br> <br> <br>def generate(table: dict[str, dict[str, int]], n: int = 80) -&gt; str:<br>    ch = START<br>    out = []<br>    for _ in range(n):<br>        ch = sample_next(table, ch)<br>        if ch == START:<br>            continue<br>        out.append(ch)<br>    return &quot;&quot;.join(out)<br> <br> <br>if __name__ == &quot;__main__&quot;:<br>    random.seed(7)<br>    table = build_table(STORY)<br>    print(&quot;What follows &#39;s&#39;? &quot;, dict(table[&quot;s&quot;]))<br>    print(&quot;What follows &#39;a&#39;? &quot;, dict(table[&quot;a&quot;]))<br>    print()<br>    print(&quot;Sample 1:&quot;, generate(table))<br>    print(&quot;Sample 2:&quot;, generate(table))</pre><p>You should see something like: after s the model mostly wants a (from <em>sam</em> / <em>sat</em> / <em>saw</em>). After a it has seen m, t, w, c. The “sentences” it writes will be ugly. That is the point. Fluency is what happens when this same loop is trained on a library, not a paragraph.</p><p><strong>API takeout vs this lab</strong></p><ul><li><strong>API takeout:</strong> hidden weights · hidden tokenizer · “it just works”</li><li><strong>This lab:</strong> a dict of counts you can print · characters you can see · you watch it fail in public</li></ul><h3>What you built today</h3><ul><li>A mental model: <strong>next piece, then append, then repeat.</strong></li><li>A count table: the cheapest possible “weights.”</li><li>A sampler: the cheapest possible “generate” button.</li><li>A reason to distrust fluency: ugly output is honesty. Pretty output is scale.</li></ul><p>You did <strong>not</strong> build a transformer, a tokenizer, or a chatbot. Do not skip ahead and call a wrapper a lab.</p><h3>Next issue</h3><p>Issue 2: what an LLM actually is — tokens vs words, why “large” matters, and why a next-token engine is not a database with manners.</p><p>If you want a preview, change STORY to a paragraph you wrote and run the file again. The table changes. The loop does not. That is the whole series in one sentence.</p><p>—</p><p><em>Further reading (optional):</em> Sebastian Raschka, <em>Build a Large Language Model (From Scratch)</em> — a full book-length path if you want the long form after these labs. This issue is not a summary of that book.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=20ea5fbcadef" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>