<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Technology &#8211; Thejesh GN</title>
	<atom:link href="https://thejeshgn.com/category/technology/feed/" rel="self" type="application/rss+xml" />
	<link>https://thejeshgn.com</link>
	<description>A container for all my views with excerpts from technology, travel, films, books, kannada, friends and other interests. I am Thejesh GN, friends call me Thej.</description>
	<lastBuildDate>Thu, 18 Jun 2026 18:32:11 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://thejeshgn.com/wp-content/uploads/2015/08/cropped-thejeshgn_icon1-150x150.png</url>
	<title>Technology &#8211; Thejesh GN</title>
	<link>https://thejeshgn.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">9645742</site>	<item>
		<title>Thank you for attending Back to Basics: Build Your Own LLM from Scratch</title>
		<link>https://thejeshgn.com/2026/06/18/thank-you-for-attending-back-to-basics-build-your-own-llm-from-scratch/</link>
					<comments>https://thejeshgn.com/2026/06/18/thank-you-for-attending-back-to-basics-build-your-own-llm-from-scratch/#respond</comments>
		
		<dc:creator><![CDATA[Thejesh GN]]></dc:creator>
		<pubDate>Thu, 18 Jun 2026 12:34:19 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Large Language Model]]></category>
		<category><![CDATA[Study at IITM]]></category>
		<guid isPermaLink="false">https://thejeshgn.com/?p=39075</guid>

					<description><![CDATA[I sent this email to all workshop attendees. It made sense to publish it as a post as well. Thank you for attending the &#8220;Back to Basics: Build Your Own LLM from Scratch&#8221; session. It was great to see so much curiosity and many questions in the room, the feedback many of you&#46;&#46;&#46;]]></description>
										<content:encoded><![CDATA[
<p>I sent this email to all workshop attendees. It made sense to publish it as a post as well.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="761" src="https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster-1024x761.png" alt="" class="wp-image-39076" srcset="https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster-1024x761.png 1024w, https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster-300x223.png 300w, https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster-768x570.png 768w, https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster-1536x1141.png 1536w, https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster-720x535.png 720w, https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster-520x386.png 520w, https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster-320x238.png 320w, https://thejeshgn.com/wp-content/uploads/2007/07/workshop_poster.png 1800w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p></p>



<p>Thank you for attending the &#8220;<a href="https://thejeshgn.com/2026/06/14/back-to-basics-build-your-own-llm-from-scratch/" rel="noreferrer noopener" target="_blank">Back to Basics: Build Your Own LLM from Scratch</a>&#8221; session. It was great to see so much curiosity and many questions in the room, the feedback many of you shared echoed this sentiment. I&#8217;m really glad you found it useful.&nbsp;</p>



<p>As we discussed during the session, I have put together a detailed blog post that walks through everything we covered, including the slides. You can also find the code from the sessions on GitHub and Codeberg.</p>



<ol class="wp-block-list">
<li>Blog with slides and code: <a href="https://thejeshgn.com/2026/06/14/back-to-basics-build-your-own-llm-from-scratch/" target="_blank" rel="noreferrer noopener">https://thejeshgn.com/2026/06/14/back-to-basics-build-your-own-llm-from-scratch/</a></li>



<li>GitHub: <a href="https://github.com/thejeshgn/workshop-back-basics-llm-build-scratch" target="_blank" rel="noreferrer noopener">https://github.com/thejeshgn/workshop-back-basics-llm-build-scratch</a></li>



<li>Codeberg: <a href="https://codeberg.org/thejeshgn/workshop-back-basics-llm-build-scratch" target="_blank" rel="noreferrer noopener">https://codeberg.org/thejeshgn/workshop-back-basics-llm-build-scratch</a></li>
</ol>



<p>I would also like all of you to continue building on what you learned. You could do that by just forking the code above and adding more features. Some ideas to improve are</p>



<ol class="wp-block-list">
<li>Swap the simple character tokenizer we used for a word-level tokenizer, or go further and implement a BERT-style WordPiece/subword tokenizer, then compare vocabulary size and how each handles unseen words. Also, what effect does it have on the model?</li>



<li>Train on a relatively larger corpus rather than the very small sample we used in the workshop. Project Gutenberg&#8217;s <a href="https://www.gutenberg.org/browse/scores/top" target="_blank" rel="noreferrer noopener">Top 100</a> is a great source of popular, public-domain books that are not very large.</li>



<li>You can also use thematic corpora, such as public-domain poetry collections, recipe collections, or the collected works of Gandhi or Ambedkar, and see how the model&#8217;s generated text picks up the style and vocabulary of that specific body of work. For some of it, you will also have to write data cleaners. Also, be mindful when downloading external content; make sure you don&#8217;t overload their servers and use public domain content. Check whether they already provide downloads in BitTorrent or in other formats, instead of scraping first.</li>



<li>Experiment with model parameters (in GPTConfig) such as context length, heads, and layers once your pipeline works end to end. See how changes in parameters change the model and its output.</li>
</ol>



<p>If you build something interesting, feel free to reach out. I&#8217;d love to hear what you create. Thanks again for being part of the session.</p>



<hr class="wp-block-separator has-text-color has-vivid-purple-color has-alpha-channel-opacity has-vivid-purple-background-color has-background is-style-dots"/>



<div class="wp-block-columns has-background is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex" style="background:linear-gradient(137deg,rgb(255,206,236) 0%,rgb(152,150,240) 62%)">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p></p>



<p>You can read this blog using <a href="https://feeds.thejeshgn.com/thejeshgn" target="_blank" rel="noreferrer noopener">RSS Feed</a>. But if you are the person who loves getting emails, then you can join my readers by <a href="https://thejeshgn.com/subscribe/" target="_blank" rel="noreferrer noopener">signing</a> up.</p>


<div class="wp-block-jetpack-subscriptions__supports-newline wp-block-jetpack-subscriptions__show-subs wp-block-jetpack-subscriptions">
		<div>
			<div>
				<div>
					<p style="width: 25%;max-width: 100%;">
						<a href="https://thejeshgn.com/?post_type=post&#038;p=39075" style="width: calc(100% - 10px);font-size: 16px;padding: 15px 23px 15px 23px;margin: 0; margin-left: 10px;border-color: black;border-radius: 6px;border-width: 2px; background-color: #000000; color: #FFFFFF; text-decoration: none; white-space: nowrap; margin-left: 0">Subscribe</a>
					</p>
				</div>
			</div>
		</div>
	</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>
</div>



<p> </p>
]]></content:encoded>
					
					<wfw:commentRss>https://thejeshgn.com/2026/06/18/thank-you-for-attending-back-to-basics-build-your-own-llm-from-scratch/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">39075</post-id>	</item>
		<item>
		<title>Back to Basics: Build Your Own LLM from Scratch</title>
		<link>https://thejeshgn.com/2026/06/14/back-to-basics-build-your-own-llm-from-scratch/</link>
					<comments>https://thejeshgn.com/2026/06/14/back-to-basics-build-your-own-llm-from-scratch/#respond</comments>
		
		<dc:creator><![CDATA[Thejesh GN]]></dc:creator>
		<pubDate>Sat, 13 Jun 2026 19:06:52 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[AI 🤖]]></category>
		<category><![CDATA[Assisted by AI 🤖]]></category>
		<category><![CDATA[IITM Paradox]]></category>
		<category><![CDATA[Large Language Model]]></category>
		<category><![CDATA[Study at IITM]]></category>
		<location><![CDATA[Chennai]]></location>
		<guid isPermaLink="false">https://thejeshgn.com/?p=39026</guid>

					<description><![CDATA[I did a workshop titled &#8220;Back to Basics: Build Your Own LLM from Scratch&#8221; at IITM/Paradox 2026, which kind of included some basic theory on how a transformer works, and then building a very small LLM. The idea was to demystify an LLM (or transformer) by understanding what goes on and then building&#46;&#46;&#46;]]></description>
										<content:encoded><![CDATA[
<p>I did a workshop titled &#8220;Back to Basics: Build Your Own LLM from Scratch&#8221; at <a href="https://thejeshgn.com/tag/iitm-paradox/?order=asc">IITM/Paradox 2026</a>, which kind of included some basic theory on how a transformer works, and then building a very small LLM. The idea was to demystify an LLM (or transformer) by understanding what goes on and then building one to deepen our understanding. I had to skip some slides because the planned session was only two hours. Ideally, I want it to be around 4 hours, split into 2 sessions: one for theory and one for lab. Maybe next time, when I plan, I will make it 4 hours so I can do it at a slower pace.</p>



<p>Of course, there are other similar workshops available online, and some of them are linked in the references section. This is just my take on it and what I used for my own understanding.</p>



<p>Suppose you want to try it at your own pace. Try the <a href="https://thejeshgn.com/wp-content/uploads/2026/06/back-basics-llm-build-scratch.html">slides</a> below and then use annotated code to read and run. Slides and code are in a repo (<a href="https://codeberg.org/thejeshgn/workshop-back-basics-llm-build-scratch">CB</a>, <a href="https://github.com/thejeshgn/workshop-back-basics-llm-build-scratch">GH</a>) too if you prefer that.</p>



<iframe width="99%" height="550px" src="https://thejeshgn.com/wp-content/uploads/2026/06/back-basics-llm-build-scratch.html" scrolling="no" frameBorder="0"></iframe>



<p></p>



<figure class="wp-block-image size-full"><img decoding="async" width="766" height="927" src="https://thejeshgn.com/wp-content/uploads/2017/02/character_gpt_workshop_diagram.png" alt="" class="wp-image-39038" srcset="https://thejeshgn.com/wp-content/uploads/2017/02/character_gpt_workshop_diagram.png 766w, https://thejeshgn.com/wp-content/uploads/2017/02/character_gpt_workshop_diagram-248x300.png 248w, https://thejeshgn.com/wp-content/uploads/2017/02/character_gpt_workshop_diagram-720x871.png 720w, https://thejeshgn.com/wp-content/uploads/2017/02/character_gpt_workshop_diagram-520x629.png 520w, https://thejeshgn.com/wp-content/uploads/2017/02/character_gpt_workshop_diagram-320x387.png 320w" sizes="(max-width: 766px) 100vw, 766px" /></figure>



<p></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = &quot;&gt;=3.10&quot;
# dependencies = &#x5B;
#   &quot;torch==2.8.0&quot;,
# ]
# &#x5B;tool.uv]
# extra-index-url = &#x5B;&quot;https://download.pytorch.org/whl/cpu&quot;]
# ///
&quot;&quot;&quot;
build_and_test.py  (ANNOTATED VERSION FOR WORKSHOP)
A minimal, single-file GPT for &quot;Back to Basics: Build Your Own LLM from Scratch&quot;.

═══════════════════════════════════════════════════════════════════════════════
WALKTHROUGH MAP — suggested order
═══════════════════════════════════════════════════════════════════════════════

  ① GPTConfig            ← slide &quot;The whole model in 5 numbers&quot;
  ② CharTokenizer        ← slides &quot;Tokens&quot; / &quot;Tokenizer&quot;
  ③ GPT.__init__/forward ← slide &quot;Recap: what we just built&quot; (the big pipeline)
  ④ CausalSelfAttention  ← slides &quot;Single-head attention&quot; → &quot;Combining heads with Wo&quot;
  ⑤ FeedForward          ← slide &quot;Feed-Forward Network (FFN)&quot;
  ⑥ TransformerBlock     ← slide &quot;One full transformer block&quot;
  ⑦ get_batch            ← (where x/y &quot;next-token&quot; pairs come from)
  ⑧ train()              ← slides &quot;Cross-entropy&quot; → &quot;Training&quot;
  ⑨ GPT.generate()       ← slide &quot;Generation: from logits to text&quot;

Comments marked 💬 are things worth SAYING out loud.
Comments marked ❓ are good questions to ASK the room.
Comments marked ⚠️ are common gotchas / likely audience questions.

Usage:
    # Train on a text file (CPU only, by design)
    uv run build_and_test.py train --data ../data/shakespeare.txt --max-steps 2000

    # Generate from a saved checkpoint
    uv run build_and_test.py generate --checkpoint ../checkpoints/run1/final_checkpoint.pt --prompt &quot;To be, or not &quot; --num-new-tokens 200 --temperature 0.8 --top-k 40 --seed 42
&quot;&quot;&quot;

import argparse
import csv
import math
import os
import sys
import time
from dataclasses import dataclass, asdict

import torch
import torch.nn as nn
import torch.nn.functional as F


# ═════════════════════════════════════════════════════════════════════════════
# ① CONFIG                                  &#x5B;Slide: &quot;The whole model in 5 numbers&quot;]
# ═════════════════════════════════════════════════════════════════════════════
# 💬 &quot;These five numbers ARE the model. Everything else is derived from them.&quot;
#    Point back to this class every time a shape like (B, T, C) appears below:
#       B = batch_size, T ≤ block_size, C = n_embd.

@dataclass
class GPTConfig:
    # Architecture (the 5 numbers from the slides)
    vocab_size: int = 65        # how many unique tokens (set from data, after tokenizer)
    n_embd:     int = 256       # C: the vector size each token is represented by
    n_head:     int = 4         # parallel attention heads (d_k = n_embd/n_head = 64)
    block_size: int = 256       # T_max: the longest context the model can ever see
    n_layer:    int = 4         # how many TransformerBlocks we stack

# Training knobs — deliberately NOT in GPTConfig:
# 💬 &quot;These shape the *training run*, not the *model*. A checkpoint doesn&#039;t need them.&quot;
batch_size = 32   # sequences per training step (the B in (B, T, C))
dropout = 0.1     # ⚠️ on during training, automatically off in eval()
                  #    &#x5B;Slide: &quot;Training vs. inference&quot; — dropout row]

output_path = &quot;../checkpoints/run1&quot;

# ═════════════════════════════════════════════════════════════════════════════
# ② TOKENIZER                                    &#x5B;Slides: &quot;Tokens&quot; / &quot;Tokenizer&quot;]
# ═════════════════════════════════════════════════════════════════════════════
# 💬 &quot;A tokenizer has exactly two jobs: encode (text → IDs) and decode (IDs → text).
#    We picked character-level — the simplest of the three choices on the slide.
#    GPT/LLaMA/Claude use subword BPE; same idea, fancier vocab.&quot;
#
# ❓ Ask: &quot;If our vocab is the 65 unique characters in Shakespeare, what happens
#    when you prompt with an emoji?&quot; → see encode(): it silently drops unknowns.

class CharTokenizer:
    &quot;&quot;&quot;Smallest possible tokenizer: one character = one token.&quot;&quot;&quot;

    def __init__(self, vocab: list&#x5B;str]):
        self.vocab = vocab
        # stoi = &quot;string to int&quot;, itos = &quot;int to string&quot; — two dicts, that&#039;s it.
        self.stoi = {ch: i for i, ch in enumerate(vocab)}
        self.itos = {i: ch for i, ch in enumerate(vocab)}

    @classmethod
    def from_text(cls, text: str) -&gt; &quot;CharTokenizer&quot;:
        # 💬 &quot;The vocab is just every unique character that occurs in the data.&quot;
        # Sorted so the vocab is deterministic across runs
        # ⚠️ Without sorted(), set() ordering varies → token IDs change between runs
        #    → an old checkpoint would decode to garbage. This one line is why we
        #    can reload checkpoints reliably.
        vocab = sorted(list(set(text)))
        return cls(vocab)

    def encode(self, s: str) -&gt; list&#x5B;int]:
        # text → list of integers.  &#x5B;Slide: &quot;Tokenizer&quot; — encode example]
        # `if c in self.stoi`: characters not in the training data are dropped.
        return &#x5B;self.stoi&#x5B;c] for c in s if c in self.stoi]

    def decode(self, ids: list&#x5B;int]) -&gt; str:
        # integers → text. Perfect inverse of encode (for known chars).
        return &quot;&quot;.join(self.itos&#x5B;i] for i in ids)

    @property
    def vocab_size(self) -&gt; int:
        # 💬 &quot;This becomes the first of our 5 numbers — vocab_size in GPTConfig.&quot;
        return len(self.vocab)


# ═════════════════════════════════════════════════════════════════════════════
# ④ ATTENTION             &#x5B;Slides: &quot;Why attention?&quot; → &quot;Combining heads with Wo&quot;]
# ═════════════════════════════════════════════════════════════════════════════
# 💬 &quot;This class is the heart of the workshop. The 7 numbered steps in forward()
#    map one-to-one onto the attention slides. Everything else is plumbing.&quot;
#
# Teaching tip: walk forward() with a concrete shape, e.g. B=32, T=256, C=256,
# n_head=4, d_k=64 — and write the shapes on the board as you go.

class CausalSelfAttention(nn.Module):
    &quot;&quot;&quot;Multi-head causal self-attention. One linear projects to Q,K,V together.&quot;&quot;&quot;

    def __init__(self, cfg: GPTConfig):
        super().__init__()
        # d_k = n_embd / n_head must divide evenly — each head gets a clean slice.
        # &#x5B;Slide: &quot;Multi-head attention&quot; — d_k = n_embd / n_head]
        assert cfg.n_embd % cfg.n_head == 0, &quot;n_embd must be divisible by n_head&quot;
        self.n_head = cfg.n_head
        self.n_embd = cfg.n_embd
        self.d_k = cfg.n_embd // cfg.n_head

        # 💬 &quot;The slides show three separate matrices Wq, Wk, Wv, each (C × C).
        #    In code we fuse them into ONE (C × 3C) matrix for efficiency —
        #    one matmul instead of three. Same math, same parameter count.&quot;
        # &#x5B;Slide: &quot;Single-head attention: Q, K, V&quot;]
        self.qkv = nn.Linear(cfg.n_embd, 3 * cfg.n_embd)

        # Wo from the slides: lets the heads talk to each other after running
        # independently. Without it, heads would be siloed.
        # &#x5B;Slide: &quot;Combining heads with Wo&quot;]
        self.proj = nn.Linear(cfg.n_embd, cfg.n_embd)
        self.dropout = nn.Dropout(dropout)

        # 💬 &quot;The causal mask is NOT learned — it&#039;s a fixed triangle of 1s.
        #    Row i has 1s up to column i: &#039;token i may look at tokens 0..i&#039;.&quot;
        # &#x5B;Slide: &quot;Causal mask&quot;] and &#x5B;Slide: &quot;What gets learned, what stays fixed&quot;]
        # register_buffer = &quot;part of the model, moves with .to(device),
        # saved in state_dict, but NO gradients&quot; — perfect for a constant.
        mask = torch.tril(torch.ones(cfg.block_size, cfg.block_size))
        self.register_buffer(&quot;mask&quot;, mask.view(1, 1, cfg.block_size, cfg.block_size))

    def forward(self, x):
        B, T, C = x.shape  # batch, seq_len, n_embd — write these on the board

        # ── 1) Project to Q, K, V ──────────────── &#x5B;Slide: &quot;Q, K, V&quot;]
        # One big matmul gives (B, T, 3C); split() carves it into three (B, T, C).
        # 💬 &quot;Query: what am I looking for? Key: what do I offer? Value: what do
        #    I pass along if matched?&quot;
        q, k, v = self.qkv(x).split(self.n_embd, dim=2)

        # ── 2) Split into heads ────────── &#x5B;Slide: &quot;Multi-head attention&quot;]
        # (B, T, C) → (B, T, n_head, d_k) → transpose → (B, n_head, T, d_k)
        # 💬 &quot;No new computation here — we&#039;re just reshaping so each head can run
        #    the SAME attention math independently on its own d_k-sized slice.&quot;
        q = q.view(B, T, self.n_head, self.d_k).transpose(1, 2)
        k = k.view(B, T, self.n_head, self.d_k).transpose(1, 2)
        v = v.view(B, T, self.n_head, self.d_k).transpose(1, 2)

        # ── 3) Scaled dot-product scores ──── &#x5B;Slide: &quot;Attention scores (scaled)&quot;]
        # (B, nh, T, d_k) @ (B, nh, d_k, T) → (B, nh, T, T)
        # 💬 &quot;A T×T grid per head: how relevant is every token to every other token.&quot;
        # ⚠️ The 1/√d_k scaling is the line students forget. Without it, dot
        #    products grow with d_k → softmax saturates → gradients vanish.
        scores = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(self.d_k))

        # ── 4) Causal mask ──────────────────────── &#x5B;Slide: &quot;Causal mask&quot;]
        # Where the triangle has 0 (future positions), drop in -inf.
        # 💬 &quot;-inf BEFORE softmax becomes exactly 0 AFTER softmax — the model
        #    literally cannot peek at the answer.&quot;
        # &#x5B;:T, :T] crops the precomputed block_size mask to the actual seq length.
        scores = scores.masked_fill(self.mask&#x5B;:, :, :T, :T] == 0, float(&quot;-inf&quot;))

        # ── 5) Softmax → attention weights ──── &#x5B;Slide: &quot;Softmax intuition&quot;]
        # Each row becomes a probability distribution: positive, sums to 1,
        # behaves like &quot;importance&quot;.
        attn = F.softmax(scores, dim=-1)
        attn = self.dropout(attn)  # regularization: randomly drop some attention links

        # ── 6) Apply attention to V ────────────── &#x5B;Slide: &quot;Apply attention&quot;]
        # (B, nh, T, T) @ (B, nh, T, d_k) → (B, nh, T, d_k)
        # 💬 &quot;Each output row is a weighted BLEND of value vectors from earlier
        #    positions. THIS is the heart of the transformer.&quot;
        out = attn @ v

        # ── 7) Re-combine heads + Wo ──── &#x5B;Slide: &quot;Combining heads with Wo&quot;]
        # (B, nh, T, d_k) → (B, T, nh, d_k) → (B, T, C): concat of all heads.
        # ⚠️ .contiguous() is needed because transpose only changes the view,
        #    not memory layout — .view() requires contiguous memory.
        out = out.transpose(1, 2).contiguous().view(B, T, C)
        out = self.proj(out)  # Wo: mixes information across heads
        return out


# ═════════════════════════════════════════════════════════════════════════════
# ⑤ FFN                                  &#x5B;Slide: &quot;Feed-Forward Network (FFN)&quot;]
# ═════════════════════════════════════════════════════════════════════════════
# 💬 &quot;Attention mixes information ACROSS tokens; the FFN processes each token
#    INDEPENDENTLY — same two layers applied to every position. Expand to 4×
#    (room to think), non-linearity, compress back.&quot;

class FeedForward(nn.Module):
    &quot;&quot;&quot;Two-layer MLP: expand to 4x, GELU, compress back.&quot;&quot;&quot;

    def __init__(self, cfg: GPTConfig):
        super().__init__()
        d_ff = 4 * cfg.n_embd            # the classic 4× expansion (slide: d_ff = 4d)
        self.fc1 = nn.Linear(cfg.n_embd, d_ff)   # W1: expand  (C → 4C)
        self.fc2 = nn.Linear(d_ff, cfg.n_embd)   # W2: compress (4C → C)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # expand → GELU (GPT&#039;s choice over ReLU) → compress → dropout
        # ❓ Ask: &quot;Why is the non-linearity essential?&quot; → without it, fc2(fc1(x))
        #    collapses into a single linear layer; depth would buy nothing.
        return self.dropout(self.fc2(F.gelu(self.fc1(x))))


# ═════════════════════════════════════════════════════════════════════════════
# ⑥ TRANSFORMER BLOCK                  &#x5B;Slide: &quot;One full transformer block&quot;]
# ═════════════════════════════════════════════════════════════════════════════
# 💬 &quot;This is the repeating unit — the slide diagram in 3 lines of code.
#    PRE-norm: LayerNorm goes BEFORE each sublayer (GPT-2/modern convention),
#    and the residual &#039;x +&#039; is the highway that lets gradients flow through
#    deep stacks.&quot;
# &#x5B;Slide: &quot;Residual + LayerNorm&quot;]

class TransformerBlock(nn.Module):
    &quot;&quot;&quot;Pre-norm block: LN -&gt; Attn -&gt; +residual -&gt; LN -&gt; FFN -&gt; +residual&quot;&quot;&quot;

    def __init__(self, cfg: GPTConfig):
        super().__init__()
        self.ln1 = nn.LayerNorm(cfg.n_embd)   # γ, β — learned (2 × n_embd params)
        self.attn = CausalSelfAttention(cfg)
        self.ln2 = nn.LayerNorm(cfg.n_embd)   # second LN, own γ, β
        self.ffn = FeedForward(cfg)

    def forward(self, x):
        # 💬 Read these aloud as: &quot;x plus attention-of-normalized-x&quot; —
        #    the residual means each sublayer only learns a CORRECTION to x.
        x = x + self.attn(self.ln1(x))   # sublayer 1: communicate (across tokens)
        x = x + self.ffn(self.ln2(x))    # sublayer 2: compute (per token)
        return x


# ═════════════════════════════════════════════════════════════════════════════
# ③ THE FULL GPT                          &#x5B;Slide: &quot;Recap: what we just built&quot;]
# ═════════════════════════════════════════════════════════════════════════════
# 💬 Teaching tip: show __init__ + forward() FIRST as the bird&#039;s-eye view —
#    it mirrors the recap-slide pipeline line by line — then descend into
#    attention. People hold details better once they&#039;ve seen the skeleton.

class GPT(nn.Module):
    &quot;&quot;&quot;The whole model: embeddings + N blocks + final LN + LM head.&quot;&quot;&quot;

    def __init__(self, cfg: GPTConfig):
        super().__init__()
        self.cfg = cfg

        # Token embedding table: (vocab_size × n_embd), learned lookup.
        # &#x5B;Slide: &quot;Embeddings&quot; — &quot;initialized randomly, updated during training&quot;]
        self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.n_embd)

        # LEARNED positional encoding (GPT-style), one vector per position.
        # &#x5B;Slide: &quot;Positional encoding&quot; — the &#039;Learned&#039; flavor, not sinusoidal]
        self.pos_emb = nn.Embedding(cfg.block_size, cfg.n_embd)

        self.drop = nn.Dropout(dropout)

        # The stack: n_layer identical-shaped blocks, each with its OWN weights.
        # &#x5B;Slide: &quot;Stacking layers&quot;]
        self.blocks = nn.ModuleList(&#x5B;TransformerBlock(cfg) for _ in range(cfg.n_layer)])

        self.ln_f = nn.LayerNorm(cfg.n_embd)  # final LN before the head

        # LM head: project (B, T, C) back to (B, T, vocab) — a score per token.
        # &#x5B;Slide: &quot;Output logits&quot;]
        self.head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False)

        # 💬 WEIGHT TYING: the output head and the input embedding SHARE one
        #    matrix (used transposed in the matmul). Saves vocab_size × n_embd
        #    params and works well in practice — mentioned on the logits slide.
        # ⚠️ This is why the parameter count printout is ~16K lower than the
        #    worked example on the slides (which counts the head separately).
        self.head.weight = self.tok_emb.weight

        # Initialize all weights small and Gaussian (std 0.02, the GPT-2 recipe).
        # ❓ &quot;Why not zeros?&quot; → all-zero weights = all neurons identical = no
        #    symmetry breaking; nothing distinct to learn.
        self.apply(self._init_weights)

    def _init_weights(self, m):
        if isinstance(m, nn.Linear):
            nn.init.normal_(m.weight, mean=0.0, std=0.02)
            if m.bias is not None:
                nn.init.zeros_(m.bias)
        elif isinstance(m, nn.Embedding):
            nn.init.normal_(m.weight, mean=0.0, std=0.02)

    def num_parameters(self) -&gt; int:
        # 💬 Compare the printout with the slide&#039;s worked example (~3.25M).
        # &#x5B;Slide: &quot;Parameter count: worked example&quot;]
        return sum(p.numel() for p in self.parameters() if p.requires_grad)

    def forward(self, idx, targets=None):
        &quot;&quot;&quot;
        idx: (B, T) token IDs
        targets: (B, T) next-token IDs (for training); None for inference
        returns: logits (B, T, vocab_size), loss or None

        💬 &quot;ONE forward pass serves both training and inference — same brain,
           different loop. targets=None is the only switch.&quot;
        &#x5B;Slide: &quot;Training vs. inference&quot;]
        &quot;&quot;&quot;
        B, T = idx.shape
        assert T &lt;= self.cfg.block_size, f&quot;sequence length {T} &gt; block_size {self.cfg.block_size}&quot;

        # ── The recap-slide pipeline, line by line ──
        tok = self.tok_emb(idx)                                       # (B, T, C)  token IDs → vectors
        pos = self.pos_emb(torch.arange(T, device=idx.device))        # (T, C)     position 0..T-1 → vectors
        x = self.drop(tok + pos)                                      # (B, T, C)  X = E + position
        # ⚠️ tok is (B,T,C), pos is (T,C) — broadcasting adds the same position
        #    vectors to every sequence in the batch. Worth pausing on.

        for block in self.blocks:        # n_layer blocks, each refines x
            x = block(x)
        x = self.ln_f(x)                 # final LayerNorm  &#x5B;Slide: &quot;Stacking layers&quot;]
        logits = self.head(x)                                         # (B, T, vocab)

        loss = None
        if targets is not None:
            # ── TRAINING branch ──        &#x5B;Slides: &quot;Cross-entropy loss&quot;]
            # 💬 &quot;The model predicts the next token at EVERY position in
            #    parallel — T predictions per sequence, not 1. That&#039;s why
            #    transformer training is so efficient.&quot;
            # Flatten (B, T, vocab) → (B·T, vocab) and (B, T) → (B·T,)
            # because F.cross_entropy wants (N, classes) and (N,) of true IDs.
            # ⚠️ cross_entropy takes raw LOGITS — it applies softmax + -log(p_t)
            #    internally. Don&#039;t softmax twice (a classic live-coding bug).
            loss = F.cross_entropy(
                logits.view(-1, logits.size(-1)),
                targets.view(-1),
            )
        return logits, loss

    # ═════════════════════════════════════════════════════════════════════════
    # ⑨ GENERATION                  &#x5B;Slide: &quot;Generation: from logits to text&quot;]
    # ═════════════════════════════════════════════════════════════════════════
    @torch.no_grad()   # inference: no gradients, no backprop — weights frozen
    def generate(self, idx, num_new_tokens: int, temperature: float = 1.0,
                 top_k: int | None = None) -&gt; torch.Tensor:
        &quot;&quot;&quot;Autoregressively generate num_new_tokens tokens.

        💬 &quot;The slide&#039;s loop: last logits → softmax → sample → append → repeat.
           One token at a time. This is how GPT writes a sentence.&quot;
        &quot;&quot;&quot;
        self.eval()  # switches dropout OFF &#x5B;Slide: &quot;Training vs. inference&quot;]
        for _ in range(num_new_tokens):
            # If the running text exceeds block_size, keep only the last
            # block_size tokens — the model can&#039;t attend beyond its context.
            # 💬 &quot;This IS the &#039;context window&#039; people talk about in big LLMs.&quot;
            idx_cond = idx if idx.size(1) &lt;= self.cfg.block_size else idx&#x5B;:, -self.cfg.block_size:]

            logits, _ = self(idx_cond)   # full forward pass; loss is None here

            # Take only the LAST position&#039;s logits — the next-token prediction.
            # (Training used all T positions; inference uses just one.)
            # TEMPERATURE: divide logits before softmax.
            #   &lt;1.0 sharpens (more confident/repetitive), &gt;1.0 flattens (wilder).
            # ⚠️ max(temperature, 1e-8) guards against divide-by-zero at temp=0.
            logits = logits&#x5B;:, -1, :] / max(temperature, 1e-8)

            # TOP-K: keep only the k highest-scoring tokens, set the rest to
            # -inf (so softmax gives them probability 0). Stops the model from
            # ever sampling a wildly unlikely character.
            if top_k is not None and top_k &gt; 0:
                v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
                logits&#x5B;logits &lt; v&#x5B;:, &#x5B;-1]]] = float(&quot;-inf&quot;)  # v&#x5B;:, &#x5B;-1]] = k-th best score

            probs = F.softmax(logits, dim=-1)                 # scores → probabilities
            next_id = torch.multinomial(probs, num_samples=1) # SAMPLE (not argmax) (B, 1)
            # ❓ Ask: &quot;What changes if we use argmax instead?&quot; → deterministic,
            #    and typically loops/repeats. Sampling is where variety comes from.
            idx = torch.cat(&#x5B;idx, next_id], dim=1)            # append &amp; loop
        return idx


# ═════════════════════════════════════════════════════════════════════════════
# ⑦ DATA: making (input, target) pairs
# ═════════════════════════════════════════════════════════════════════════════
# 💬 &quot;Where do the &#039;answer keys&#039; come from? The text itself. The target is the
#    input shifted one character to the right — free labels, no human needed.
#    This is what &#039;self-supervised&#039; means.&quot;
#
#    data:   &#x5B;T, h, e, _, c, a, t]
#    x  =     &#x5B;T, h, e, _, c, a]
#    y  =        &#x5B;h, e, _, c, a, t]   ← y&#x5B;i] is the &#039;next token&#039; after x&#x5B;i]

def get_batch(data: torch.Tensor, block_size: int, batch_size: int,
              device: torch.device) -&gt; tuple&#x5B;torch.Tensor, torch.Tensor]:
    &quot;&quot;&quot;Sample batch_size random windows of length block_size from data.&quot;&quot;&quot;
    # Random start indices; -1 leaves room for the shifted target.
    ix = torch.randint(0, len(data) - block_size - 1, (batch_size,))
    x = torch.stack(&#x5B;data&#x5B;i:i + block_size] for i in ix])          # inputs
    y = torch.stack(&#x5B;data&#x5B;i + 1:i + 1 + block_size] for i in ix])  # same, shifted +1
    return x.to(device), y.to(device)
    # ⚠️ Random windows ≠ epochs. We sample with replacement, so &quot;one epoch&quot;
    #    isn&#039;t well-defined here — we just count steps. Fine at this scale.


# ═════════════════════════════════════════════════════════════════════════════
# ⑧ TRAIN COMMAND        &#x5B;Slides: &quot;Why we minimize it&quot; → &quot;Training&quot;]
# ═════════════════════════════════════════════════════════════════════════════
# 💬 The training slide&#039;s loop, in code:
#    batch → forward → loss → backward (gradients) → optimizer step → repeat.
#    Map each line of the loop below onto that diagram as you scroll.

def train(args):
    device = torch.device(&quot;cpu&quot;)  # CPU-only, by design for the workshop
    torch.manual_seed(1337)       # fixed seed → everyone in the room gets the
                                  # same loss curve. All of us get same model.

    # ── 1) Load data: just a plain text file ──
    if not os.path.exists(args.data):
        sys.exit(f&quot;Data file not found: {args.data}&quot;)
    with open(args.data, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
        text = f.read()
    print(f&quot;Loaded {len(text):,} characters from {args.data}&quot;)

    # ── 2) Build tokenizer FROM the data ──
    # 💬 &quot;vocab_size isn&#039;t chosen by us — it falls out of the data. For tiny
    #    Shakespeare it&#039;s 65: letters, digits, punctuation, newline.&quot;
    tokenizer = CharTokenizer.from_text(text)
    print(f&quot;Vocab size: {tokenizer.vocab_size}&quot;)

    # ── 3) Encode the whole corpus ONCE; split 90/10 train/val ──
    # ❓ Ask: &quot;Why hold out a validation set?&quot; → train loss can fall from
    #    memorization; val loss tells us if the model GENERALIZES. Watch the
    #    gap between the two columns in the printout.
    data = torch.tensor(tokenizer.encode(text), dtype=torch.long)
    n_train = int(0.9 * len(data))
    train_data = data&#x5B;:n_train]
    val_data = data&#x5B;n_train:]
    print(f&quot;Train tokens: {len(train_data):,}   Val tokens: {len(val_data):,}&quot;)

    # ── 4) Build the model from the 5 numbers ──
    cfg = GPTConfig(vocab_size=tokenizer.vocab_size)
    model = GPT(cfg).to(device)
    # 💬 Pause on this printout and reconcile it with the parameter-count
    #    slides (~3.25M). Slight difference = weight tying (head not double-counted).
    print(f&quot;Model parameters: {model.num_parameters():,}&quot;)

    # ── 5) Optimizer: AdamW ──   &#x5B;Slide: &quot;From gradients to weight updates&quot;]
    # 💬 &quot;AdamW = the slide&#039;s `w -= lr × gradient`, but with per-weight adaptive
    #    step sizes from running averages of past gradients. Used by GPT-2/3,
    #    LLaMA — and by us.&quot;
    # weight_decay gently pulls weights toward 0 — regularization.
    optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)

    # LR schedule: linear WARMUP (first 100 steps), then COSINE DECAY to min_lr.
    # 💬 &quot;Warmup: start gentle while weights are random garbage. Cosine: take
    #    smaller steps as we converge — like slowing down when parallel parking.&quot;
    # ❓ &quot;What&#039;s lr at step 0? At step `warmup`? At step max_steps?&quot; → trace it.
    def lr_at(step: int, max_steps: int, base_lr: float = 3e-4,
              warmup: int = 100, min_lr: float = 3e-5) -&gt; float:
        if step &lt; warmup:
            return base_lr * (step + 1) / warmup            # ramp 0 → base_lr
        progress = (step - warmup) / max(1, max_steps - warmup)
        progress = min(1.0, progress)
        return min_lr + 0.5 * (base_lr - min_lr) * (1 + math.cos(math.pi * progress))

    # ── 6) Training loop ──
    # Fixed sample prompts so the audience can WATCH the same prompt improve
    # from noise → words → Shakespeare-ish as training progresses.
    sample_prompts = &#x5B;&quot;To be, or not &quot;, &quot;For I am falser than vows made in&quot;]
    log_path = f&quot;{output_path}/loss_log.csv&quot;      # for graphing the loss curve later
    log_file = open(log_path, &quot;w&quot;, newline=&quot;&quot;)
    log_writer = csv.writer(log_file)
    log_writer.writerow(&#x5B;&quot;step&quot;, &quot;train_loss&quot;, &quot;val_loss&quot;, &quot;lr&quot;])

    # Derived intervals: 10 evals, 5 sample dumps, 4 checkpoints per run,
    # regardless of --max-steps.
    eval_every = max(1, args.max_steps // 10)
    sample_every = max(1, args.max_steps // 5)
    ckpt_every = max(1, args.max_steps // 4)

    t0 = time.time()
    model.train()  # dropout ON
    for step in range(args.max_steps):
        # Set this step&#039;s learning rate (PyTorch optimizers read it from
        # param_groups; we overwrite it each step with our schedule).
        lr = lr_at(step, args.max_steps)
        for g in optimizer.param_groups:
            g&#x5B;&quot;lr&quot;] = lr

        # ══ THE four lines that ARE training ══  &#x5B;Slide: &quot;Training&quot;]
        xb, yb = get_batch(train_data, cfg.block_size, batch_size, device)
        _, loss = model(xb, yb)               # 1. forward → loss (one number)

        optimizer.zero_grad(set_to_none=True) # 2. clear last step&#039;s gradients
        # ⚠️ Forgetting zero_grad is THE classic bug: PyTorch ACCUMULATES
        #    gradients by default, so they&#039;d pile up across steps.
        loss.backward()                       # 3. backprop: chain rule, automatic
                                              #    &#x5B;Slide: &quot;How backprop computes gradients&quot;]
        optimizer.step()                      # 4. nudge EVERY weight a tiny bit

        # ── Periodic eval + logging ──
        if step % eval_every == 0 or step == args.max_steps - 1:
            model.eval()                      # dropout OFF for a fair measurement
            with torch.no_grad():             # no gradient bookkeeping needed
                xv, yv = get_batch(val_data, cfg.block_size, batch_size, device)
                _, val_loss = model(xv, yv)
            elapsed = time.time() - t0
            # 💬 Narrate the first line: loss ≈ 4.17 ≈ ln(65) — the loss of a
            #    UNIFORM guess over 65 chars (&quot;Model B&quot; on the cross-entropy
            #    slide). Watching it fall below that = the model is learning.
            print(f&quot;step {step:5d} | lr {lr:.2e} | train {loss.item():.4f} | &quot;
                  f&quot;val {val_loss.item():.4f} | {elapsed:.1f}s&quot;)
            log_writer.writerow(&#x5B;step, loss.item(), val_loss.item(), lr])
            log_file.flush()                  # so the CSV is graphable mid-run
            model.train()                     # back to training mode

        # ── Periodic samples: the workshop&#039;s &quot;wow&quot; moment ──
        # 💬 Early samples are gibberish; mid-run grows words and line breaks;
        #    late samples look like a drunk Shakespeare. Same prompts each time
        #    makes the progress visible.
        if step % sample_every == 0 and step &gt; 0:
            model.eval()
            for p in sample_prompts:
                ids = torch.tensor(&#x5B;tokenizer.encode(p)], dtype=torch.long, device=device)
                out = model.generate(ids, num_new_tokens=80, temperature=0.8, top_k=20)
                generated = tokenizer.decode(out&#x5B;0].tolist())
                print(f&quot;   sample: {generated!r}&quot;)
            model.train()

        # ── Periodic checkpoints (resume / compare across training stages) ──
        if step &gt; 0 and step % ckpt_every == 0:
            save_checkpoint(model, tokenizer, cfg, f&quot;{output_path}/ckpt_step_{step}.pt&quot;)

    # Final checkpoint — this is what the generate command loads.
    save_checkpoint(model, tokenizer, cfg, f&quot;{output_path}/final_checkpoint.pt&quot;)
    log_file.close()
    print(f&quot;Done. Wrote final_checkpoint.pt and {log_path}.&quot;)


def save_checkpoint(model: GPT, tokenizer: CharTokenizer, cfg: GPTConfig, path: str):
    # 💬 &quot;A checkpoint must contain everything needed to rebuild the model:
    #    1. the weights, 2. the 5 numbers (shape), 3. the vocab (so token IDs
    #    decode to the same characters). Forget the vocab → garbage output.&quot;
    torch.save({
        &quot;model_state&quot;: model.state_dict(),   # every learned tensor, by name
        &quot;config&quot;: asdict(cfg),               # the 5 numbers
        &quot;vocab&quot;: tokenizer.vocab,            # the character list
    }, path)
    print(f&quot;  saved {path}&quot;)


# ═════════════════════════════════════════════════════════════════════════════
# GENERATE COMMAND — checkpoint in, text out
# ═════════════════════════════════════════════════════════════════════════════
# 💬 &quot;Inference = rebuild the exact same model, load frozen weights, loop
#    generate(). No loss, no gradients, no optimizer — compare the two columns
#    of the &#039;Training vs. inference&#039; slide.&quot;

def generate(args):
    device = torch.device(&quot;cpu&quot;)
    if args.seed is not None:
        torch.manual_seed(args.seed)  # same seed + same prompt → same output
        # 💬 Good demo: run twice with --seed 42 (identical), then without (varies).

    if not os.path.exists(args.checkpoint):
        sys.exit(f&quot;Checkpoint not found: {args.checkpoint}&quot;)
    # ⚠️ weights_only=False because our checkpoint also carries config + vocab
    #    (not just tensors). Fine for our OWN files; for untrusted downloads
    #    you&#039;d want weights_only=True (it restricts unpickling).
    ckpt = torch.load(args.checkpoint, map_location=device, weights_only=False)

    # Rebuild the exact architecture and tokenizer the checkpoint was saved with:
    cfg = GPTConfig(**ckpt&#x5B;&quot;config&quot;])        # the 5 numbers → same shapes
    tokenizer = CharTokenizer(ckpt&#x5B;&quot;vocab&quot;]) # same vocab → same ID↔char mapping

    model = GPT(cfg).to(device)
    model.load_state_dict(ckpt&#x5B;&quot;model_state&quot;])  # pour the learned weights back in
    model.eval()                                # inference mode: dropout off

    # prompt → IDs → generate → IDs → text. The full round trip from slide 1.
    ids = torch.tensor(&#x5B;tokenizer.encode(args.prompt)], dtype=torch.long, device=device)
    out = model.generate(
        ids,
        num_new_tokens=args.num_new_tokens,
        temperature=args.temperature,   # ❓ live demo: try 0.2 vs 1.5 and compare
        top_k=args.top_k,
    )
    print(tokenizer.decode(out&#x5B;0].tolist()))


# ═════════════════════════════════════════════════════════════════════════════
# CLI — two subcommands, as promised on the &quot;Hands-on: the plan&quot; slide
# ═════════════════════════════════════════════════════════════════════════════

def main():
    parser = argparse.ArgumentParser(description=&quot;Tiny GPT: train and generate&quot;)
    sub = parser.add_subparsers(dest=&quot;cmd&quot;, required=True)

    # train: only data + steps are CLI args; architecture lives in GPTConfig.
    p_train = sub.add_parser(&quot;train&quot;, help=&quot;Train the model from a text file&quot;)
    p_train.add_argument(&quot;--data&quot;, required=True, help=&quot;path to UTF-8 text file&quot;)
    p_train.add_argument(&quot;--max-steps&quot;, type=int, default=2000)
    p_train.set_defaults(func=train)

    # generate: checkpoint + prompt + the sampling knobs from the slides.
    p_gen = sub.add_parser(&quot;generate&quot;, help=&quot;Generate text from a checkpoint&quot;)
    p_gen.add_argument(&quot;--checkpoint&quot;, required=True)
    p_gen.add_argument(&quot;--prompt&quot;, required=True)
    p_gen.add_argument(&quot;--num-new-tokens&quot;, type=int, default=500)
    p_gen.add_argument(&quot;--temperature&quot;, type=float, default=0.8)
    p_gen.add_argument(&quot;--top-k&quot;, type=int, default=40)
    p_gen.add_argument(&quot;--seed&quot;, type=int, default=None)
    p_gen.set_defaults(func=generate)

    args = parser.parse_args()
    args.func(args)   # dispatch to train() or generate()


if __name__ == &quot;__main__&quot;:
    main()

</pre></div>


<hr class="wp-block-separator has-text-color has-vivid-purple-color has-alpha-channel-opacity has-vivid-purple-background-color has-background is-style-dots"/>



<div class="wp-block-columns has-background is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex" style="background:linear-gradient(137deg,rgb(255,206,236) 0%,rgb(152,150,240) 62%)">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p></p>



<p>You can read this blog using <a href="https://feeds.thejeshgn.com/thejeshgn" target="_blank" rel="noreferrer noopener">RSS Feed</a>. But if you are the person who loves getting emails, then you can join my readers by <a href="https://thejeshgn.com/subscribe/" target="_blank" rel="noreferrer noopener">signing</a> up.</p>


<div class="wp-block-jetpack-subscriptions__supports-newline wp-block-jetpack-subscriptions__show-subs wp-block-jetpack-subscriptions">
		<div>
			<div>
				<div>
					<p style="width: 25%;max-width: 100%;">
						<a href="https://thejeshgn.com/?post_type=post&#038;p=39026" style="width: calc(100% - 10px);font-size: 16px;padding: 15px 23px 15px 23px;margin: 0; margin-left: 10px;border-color: black;border-radius: 6px;border-width: 2px; background-color: #000000; color: #FFFFFF; text-decoration: none; white-space: nowrap; margin-left: 0">Subscribe</a>
					</p>
				</div>
			</div>
		</div>
	</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>
</div>



<p> </p>
]]></content:encoded>
					
					<wfw:commentRss>https://thejeshgn.com/2026/06/14/back-to-basics-build-your-own-llm-from-scratch/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">39026</post-id>	</item>
		<item>
		<title>Exploring Epicure the Food Embedding Model</title>
		<link>https://thejeshgn.com/2026/06/11/exploring-epicure-the-food-embedding-model/</link>
					<comments>https://thejeshgn.com/2026/06/11/exploring-epicure-the-food-embedding-model/#respond</comments>
		
		<dc:creator><![CDATA[Thejesh GN]]></dc:creator>
		<pubDate>Thu, 11 Jun 2026 09:54:38 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[arXiv]]></category>
		<category><![CDATA[Embedding Models]]></category>
		<category><![CDATA[Free and Open Source]]></category>
		<category><![CDATA[Metapath2Vec]]></category>
		<category><![CDATA[Word2Vec]]></category>
		<guid isPermaLink="false">https://thejeshgn.com/?p=38996</guid>

					<description><![CDATA[FlavorGraph is a large-scale graph network that combines data from over a million recipes with chemical compound information from 1,500+ flavor molecules to predict ingredient pairings. It uses graph embedding methods to represent foods as dense vectors, enabling data-driven food pairing suggestions that go beyond human or chef intuition. In FlavorGraph, the chemical&#46;&#46;&#46;]]></description>
										<content:encoded><![CDATA[
<p><a href="https://www.nature.com/articles/s41598-020-79422-8" target="_blank" rel="noreferrer noopener">FlavorGraph</a> is a large-scale graph network that combines data from over a million recipes with chemical compound information from 1,500+ flavor molecules to predict ingredient pairings. It uses graph embedding methods to <a href="https://github.com/lamypark/FlavorGraph" target="_blank" rel="noreferrer noopener">represent foods as dense vectors</a>, enabling data-driven food pairing suggestions that go beyond human or chef intuition. In FlavorGraph, the chemical and recipe context signals are fused at training time via a fixed metapath design, leaving no inference-time knob to adjust their relative weights in the final embeddings.</p>



<p>One could call <a href="https://arxiv.org/abs/2605.22391" target="_blank" rel="noreferrer noopener">Epicure</a> an enhanced FlavorGraph. It builds on <a href="https://github.com/lamypark/FlavorGraph" target="_blank" rel="noreferrer noopener">FlavorGraph</a> to produce 300-D embeddings, but instead of a single embedding that combines both chemical and recipe context signals. It has three embedding models <a href="https://huggingface.co/Kaikaku/epicure-cooc" target="_blank" rel="noreferrer noopener">Cooc</a>, <a href="https://huggingface.co/Kaikaku/epicure-chem" target="_blank" rel="noreferrer noopener">Chem</a>, and <a href="https://huggingface.co/Kaikaku/epicure-core" target="_blank" rel="noreferrer noopener">Core</a>. That way, as a user, you can choose the embedding you want. It also includes more recipes from other languages, not just English. Recipes from English, Chinese, Russian, Vietnamese, Spanish, Turkish, Indonesian, German, and Indian-English are included. They are machine-translated into English for this. It&#8217;s good to see Indian recipes covered as well.</p>



<p>They have also normalized the raw ingredient strings to <a target="_blank" href="https://huggingface.co/Kaikaku/epicure-core/blob/main/itos.json" rel="noreferrer noopener">1,790 canonical entries</a> using an LLM. I found this interesting. I also found that this doesn&#8217;t necessarily cover everything that an Indian recipe needs. For example, there are only coriander and coriander_root. It&#8217;s usually coriander seeds or coriander leaves in India. In their list, there is no way to differentiate it.</p>



<p>The most interesting part is how they traverse the graph to construct metapaths for training Metapath2Vec models. The three models follow different logic to construct it. But they all have the same architecture and hyperparameters: <code>300-dim embeddings, walks_per_node=100, walk_length=50, context_size=7, 5 negative samples, batch_size=32,768, lr=0.0025, 20 epochs, no warm restart</code>.</p>



<p>From the <a href="https://arxiv.org/html/2605.22391v1#S2" target="_blank" rel="noreferrer noopener">paper</a>, the methodology (I have embedded the <a href="https://thejeshgn.com/wp-content/uploads/2026/06/epicure_method.svg">SVG flowchart </a>below) is easy to understand and, if required, can be replicated. But the raw data is not available, though sources are mentioned. Maybe it can be replicated with a different dataset. They have also used LLMs quite a bit in the data and training pipeline. For me, constructing the metapaths was the most interesting part.</p>



<ol class="wp-block-list">
<li>Epicure-Cooc. Walks the Cooc graph: pure I–I random walks weighted by NPMI. No compound nodes.</li>



<li>Epicure-Core. Walks the typed-compound graph and injects pure I–I walks at <code>--ii_repeat=10</code> alongside the typed-compound metapaths. Edge transitions are weighted so I–C hops are not oversampled relative to the smaller I–I edge set. The resulting embedding blends chemical and recipe-context signal.</li>



<li>Epicure-Chem. Walks the typed-compound graph but with &#8211;ii_repeat=0: the I–I templates are absent and the only walks the skip-gram sees are compound-mediated. The chemistry extreme of the family.</li>
</ol>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://thejeshgn.com/wp-content/uploads/2026/06/epicure_method.svg"><img decoding="async" src="https://thejeshgn.com/wp-content/uploads/2026/06/epicure_method.svg" alt="" class="wp-image-38997" style="width:500px"/></a><figcaption class="wp-element-caption">Epicure Methodology Flowchart</figcaption></figure>
</div>


<p>You can explore the <a href="https://huggingface.co/spaces/Kaikaku/epicure-explorer" target="_blank" rel="noreferrer noopener">embedding online</a> or using a simple script below. In my exploration, I found that it is okay to use the nearest neighbors for an ingredient in a recipe, in chemistry, or in mixed contexts. But I don&#8217;t think it&#8217;s at a level where we could just replace ingredients. It also doesn&#8217;t have any information about allergens, texture, etc. But it is small and can be used to build on top of it.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://huggingface.co/spaces/Kaikaku/epicure-explorer"><img loading="lazy" decoding="async" width="1353" height="1102" src="https://thejeshgn.com/wp-content/uploads/2007/07/epicure_embeddings_explorer.png" alt="Online explorere Epicure three sibling ingredient embeddings." class="wp-image-39004" srcset="https://thejeshgn.com/wp-content/uploads/2007/07/epicure_embeddings_explorer.png 1353w, https://thejeshgn.com/wp-content/uploads/2007/07/epicure_embeddings_explorer-300x244.png 300w, https://thejeshgn.com/wp-content/uploads/2007/07/epicure_embeddings_explorer-1024x834.png 1024w, https://thejeshgn.com/wp-content/uploads/2007/07/epicure_embeddings_explorer-768x626.png 768w, https://thejeshgn.com/wp-content/uploads/2007/07/epicure_embeddings_explorer-720x586.png 720w, https://thejeshgn.com/wp-content/uploads/2007/07/epicure_embeddings_explorer-520x424.png 520w, https://thejeshgn.com/wp-content/uploads/2007/07/epicure_embeddings_explorer-320x261.png 320w" sizes="auto, (max-width: 1353px) 100vw, 1353px" /></a><figcaption class="wp-element-caption"><a href="https://huggingface.co/spaces/Kaikaku/epicure-explorer">Online explorer</a> Epicure three sibling ingredient embeddings.</figcaption></figure>
</div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# /// script
# requires-python = &quot;&gt;=3.11&quot;
# dependencies = &#x5B;
#   &quot;numpy&quot;,
#   &quot;huggingface_hub&quot;,
# ]
# ///

from epicure import Epicure

m = Epicure.from_pretrained(&quot;Kaikaku/epicure-core&quot;)

print(&quot;neighbors : chicken&quot;)
print(m.neighbors(&quot;chicken&quot;, k=5))

print(&quot;neighbors : coriander&quot;)
print(m.neighbors(&quot;coriander&quot;, k=5))

print(&quot;slerp : rice, cuisine:South_Asian&quot;)
print(m.slerp(&quot;rice&quot;, &quot;cuisine:South_Asian&quot;, theta_deg=30, k=5))

print(&quot;closest_mode : chocolate&quot;)
print(m.closest_mode(&quot;chocolate&quot;, kind=&quot;factor&quot;, k=3))
</pre></div>


<p></p>



<p class="has-luminous-vivid-amber-background-color has-background">To run the above script, include the epicure.py file from the <a href="https://huggingface.co/Kaikaku/epicure-core/tree/main" target="_blank" rel="noreferrer noopener">HF repo</a> in the same folder. The epicure package on PyPI is a different package.</p>



<p></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
# Output of above script
neighbors : chicken
&#x5B;(&#039;pork&#039;, 0.5807677507400513), (&#039;beef&#039;, 0.5712239742279053), (&#039;chicken_broth&#039;, 0.5498523116111755), (&#039;peanut&#039;, 0.5233361124992371), (&#039;cream_of_chicken_soup&#039;, 0.5217206478118896)]
neighbors : coriander
&#x5B;(&#039;cumin&#039;, 0.7044016122817993), (&#039;scallion&#039;, 0.6711025238037109), (&#039;chili_pepper&#039;, 0.6609357595443726), (&#039;turmeric&#039;, 0.6468461155891418), (&#039;chicken_broth&#039;, 0.6463753581047058)]
slerp : rice, cuisine:South_Asian
&#x5B;(&#039;turmeric&#039;, 0.7607102225586673), (&#039;mustard_seed&#039;, 0.756659547118539), (&#039;fenugreek_seed&#039;, 0.7468295496269882), (&#039;coriander&#039;, 0.7428115853809022), (&#039;cumin&#039;, 0.7388300342030064)]
closest_mode : chocolate
&#x5B;(&#039;F_15/M0&#039;, &#039;American sweet confections and dessert bases&#039;, 0.7751955986022949), (&#039;F_7/M1&#039;, &#039;Sweet liqueurs and confections&#039;, 0.7553147673606873), (&#039;F_8/M4&#039;, &#039;Sweet confections and dessert ingredients&#039;, 0.7396374344825745)]

</pre></div>


<h3 class="wp-block-heading">Definitions</h3>



<p><a href="https://en.wikipedia.org/wiki/Word2vec" target="_blank" rel="noreferrer noopener">Word2Vec</a> is a neural method for building word embeddings from raw text. Words appearing in similar contexts end up close together in the vector space. It comes in two variants skip-gram and CBOW.</p>



<ul class="wp-block-list">
<li>skip-gram: predict the surrounding context words from a target word</li>



<li>CBOW: predict the target word from its surrounding context</li>
</ul>



<p><a href="https://ericdongyx.github.io/papers/KDD17-dong-chawla-swami-metapath2vec.pdf" target="_blank" rel="noreferrer noopener">Metapath2Vec (PDF)</a> It does random walks through the graph guided by a defined metapath, then feeds those walks to a skip-gram model to learn node embeddings. It works well for Heterogeneous graphs. Basically, it&#8217;s like creating sentences based on the metapath, then running Word2Vec skip-gram on them.</p>



<ul class="wp-block-list">
<li>A metapath is a typed node sequence, such as author – paper – venue – paper – author. It&#8217;s predefined by manually by the model designer.</li>
</ul>



<hr class="wp-block-separator has-text-color has-vivid-purple-color has-alpha-channel-opacity has-vivid-purple-background-color has-background is-style-dots"/>



<div class="wp-block-columns has-background is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex" style="background:linear-gradient(137deg,rgb(255,206,236) 0%,rgb(152,150,240) 62%)">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p></p>



<p>You can read this blog using <a href="https://feeds.thejeshgn.com/thejeshgn" target="_blank" rel="noreferrer noopener">RSS Feed</a>. But if you are the person who loves getting emails, then you can join my readers by <a href="https://thejeshgn.com/subscribe/" target="_blank" rel="noreferrer noopener">signing</a> up.</p>


<div class="wp-block-jetpack-subscriptions__supports-newline wp-block-jetpack-subscriptions__show-subs wp-block-jetpack-subscriptions">
		<div>
			<div>
				<div>
					<p style="width: 25%;max-width: 100%;">
						<a href="https://thejeshgn.com/?post_type=post&#038;p=38996" style="width: calc(100% - 10px);font-size: 16px;padding: 15px 23px 15px 23px;margin: 0; margin-left: 10px;border-color: black;border-radius: 6px;border-width: 2px; background-color: #000000; color: #FFFFFF; text-decoration: none; white-space: nowrap; margin-left: 0">Subscribe</a>
					</p>
				</div>
			</div>
		</div>
	</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>
</div>



<p> </p>
]]></content:encoded>
					
					<wfw:commentRss>https://thejeshgn.com/2026/06/11/exploring-epicure-the-food-embedding-model/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">38996</post-id>	</item>
		<item>
		<title>Embedding user code in your app using Extism</title>
		<link>https://thejeshgn.com/2026/05/19/embedding-user-code-in-your-app-using-extism/</link>
					<comments>https://thejeshgn.com/2026/05/19/embedding-user-code-in-your-app-using-extism/#respond</comments>
		
		<dc:creator><![CDATA[Thejesh GN]]></dc:creator>
		<pubDate>Tue, 19 May 2026 14:40:06 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[Free and Open Source]]></category>
		<category><![CDATA[Power User]]></category>
		<category><![CDATA[WASM]]></category>
		<category><![CDATA[Web Assembly]]></category>
		<guid isPermaLink="false">https://thejeshgn.com/?p=38848</guid>

					<description><![CDATA[Every application I love has some kind of power-user mode where I can add my own code or scripting to make it useful to me. Simple examples are Firefox with its addons or VLC with its plugins. Ideally, any significant or valuable application should have such a feature. But as a developer or&#46;&#46;&#46;]]></description>
										<content:encoded><![CDATA[
<p>Every application I love has some kind of power-user mode where I can add my own code or scripting to make it useful to me. Simple examples are Firefox with its addons or VLC with its <a target="_blank" href="https://addons.videolan.org/browse?cat=325&amp;ord=latest" rel="noreferrer noopener">plugins</a>. Ideally, any significant or valuable application should have such a feature.</p>



<p>But as a developer or builder, it&#8217;s not easy to build such a feature securely. I have tried it before with embedding Lua with some restrictions. But it isn&#8217;t great in sandboxing. I&#8217;ve always had an eye on the WASM-based implementation because browsers have decent sandboxing. Very recently, when I was <a href="https://thejeshgn.com/2026/05/08/weekly-notes-19-2026/" target="_blank" rel="noreferrer noopener">updating Navidrome</a>, I came across its plugin system, which uses <a href="https://extism.org/" target="_blank" rel="noreferrer noopener">Extism</a> and is based on WebAssembly. Ideally, at some point, even WordPress can do this, allowing us to sandbox plugins. </p>



<p>So I wrote a simple plugin to try out this infrastructure. I wrote a plugin with internet access to a given URL. Its only goal is to test the plugin&#8217;s ability to make web API calls in a controlled way. And  to explore how plugins work. The language I chose for the plugin is Python, but there are many <a href="https://extism.org/docs/concepts/pdk" target="_blank" rel="noreferrer noopener">better supported languages</a>. I tested it using CLI. And it&#8217;s not difficult to call it from a host language.</p>



<p>I installed and used Extism <a href="https://extism.org/docs/install" target="_blank" rel="noreferrer noopener">CLI</a>, version 1.6.2. Direct binary download from the repository. I wrote a simple Python script that, as a plugin, expects a JSON input containing URL and payload for a web post. There are other ways to pass the data to the plugin, but JSON seemed most flexible.</p>



<p>Inside the plugin, I read the JSON input, parse the parameters, and make a web POST request using <code>Existims Http</code>. Remember, you need to use the <a href="https://github.com/extism/python-pdk" target="_blank" rel="noreferrer noopener">Python PDK (Plugin Development Kit)</a>. Those PDKs can have some limitations of their own. So be mindful when choosing a plugin language. </p>



<p>Also as you can see, a plugin can have many functions. The ones annotated with <code>@extism.plugin_fn</code> can be called from the host environment. The code is easy to understand.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
#webhook.py
import json
import extism
from extism import Http

@extism.plugin_fn
def post_json():
    params = extism.input_json()

    url = params&#x5B;&quot;url&quot;]
    payload = params&#x5B;&quot;payload&quot;]

    response = Http.request(
        url,
        meth=&quot;POST&quot;,
        headers={&quot;Content-Type&quot;: &quot;application/json&quot;},
        body=json.dumps(payload),
    )

    result = {
        &quot;status&quot;: response.status_code,
        &quot;body&quot;: response.data_str(),
    }

    extism.output_str(json.dumps(result))
</pre></div>


<p>To call this plugin first I need to build it. To build use the <code>extism-py</code> which is installed as part of Python PDK. Then run</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
extism-py webhook.py webhook.wasm
</pre></div>


<p>That will produce <code>webhook.wasm</code>, the plugin code that one would probably ship. You can call or invoke it via the CLI for testing. As you can see, I have to grant internet access to the WASM by allowing access to the url <code>webhook.site</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
extism call webhook.wasm post_json \
--allow-host &quot;webhook.site&quot; --wasi \
--input &#039;{ &quot;url&quot;: &quot;https://webhook.site/0b757518-7120-4919-a12f-252d3dfbc8b5&quot;, &quot;payload&quot;: { &quot;name&quot;: &quot;Thejesh from inside the sandbox&quot;, &quot;seq&quot;: 1 } }&#039;
</pre></div>


<p>But in real world you would be calling it from host code/program. Let&#8217;s say your host language is also Python, then you can call this WASM plugin using the following piece of code. Again, it&#8217;s not that difficult.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
# host.py
# /// script
# dependencies = &#x5B;&quot;extism&quot;]
# ///

import json
import extism

wasm_file = &quot;webhook.wasm&quot;

input_data = {
    &quot;url&quot;: &quot;https://webhook.site/dc895b92-0b21-46db-a9c0-766dd87e8b0f&quot;,
    &quot;payload&quot;: {
        &quot;name&quot;: &quot;Thejesh from inside the sandbox&quot;,
        &quot;seq&quot;: 1,
    },
}

manifest = {
    &quot;wasm&quot;: &#x5B;{&quot;path&quot;: wasm_file}],
    &quot;allowed_hosts&quot;: &#x5B;&quot;webhook.site&quot;],
}

with extism.Plugin(
    manifest,
    wasi=True,
) as plugin:
    result = plugin.call(&quot;post_json&quot;, json.dumps(input_data).encode())
    print(result.decode())

</pre></div>


<p>All of a sudden, safe plugin implementation doesn&#8217;t sound that difficult, isn&#8217;t it?</p>



<hr class="wp-block-separator has-text-color has-vivid-purple-color has-alpha-channel-opacity has-vivid-purple-background-color has-background is-style-dots"/>



<div class="wp-block-columns has-background is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex" style="background:linear-gradient(137deg,rgb(255,206,236) 0%,rgb(152,150,240) 62%)">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p></p>



<p>You can read this blog using <a href="https://feeds.thejeshgn.com/thejeshgn" target="_blank" rel="noreferrer noopener">RSS Feed</a>. But if you are the person who loves getting emails, then you can join my readers by <a href="https://thejeshgn.com/subscribe/" target="_blank" rel="noreferrer noopener">signing</a> up.</p>


<div class="wp-block-jetpack-subscriptions__supports-newline wp-block-jetpack-subscriptions__show-subs wp-block-jetpack-subscriptions">
		<div>
			<div>
				<div>
					<p style="width: 25%;max-width: 100%;">
						<a href="https://thejeshgn.com/?post_type=post&#038;p=38848" style="width: calc(100% - 10px);font-size: 16px;padding: 15px 23px 15px 23px;margin: 0; margin-left: 10px;border-color: black;border-radius: 6px;border-width: 2px; background-color: #000000; color: #FFFFFF; text-decoration: none; white-space: nowrap; margin-left: 0">Subscribe</a>
					</p>
				</div>
			</div>
		</div>
	</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>
</div>



<p> </p>
]]></content:encoded>
					
					<wfw:commentRss>https://thejeshgn.com/2026/05/19/embedding-user-code-in-your-app-using-extism/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">38848</post-id>	</item>
		<item>
		<title>30 Days of DXing</title>
		<link>https://thejeshgn.com/2026/04/20/30-days-of-dxing/</link>
					<comments>https://thejeshgn.com/2026/04/20/30-days-of-dxing/#respond</comments>
		
		<dc:creator><![CDATA[Thejesh GN]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 09:45:23 +0000</pubDate>
				<category><![CDATA[Life]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[30DayChallenges]]></category>
		<category><![CDATA[30DaysOfDXing]]></category>
		<category><![CDATA[Amateur (ham) Radio]]></category>
		<category><![CDATA[DXing]]></category>
		<category><![CDATA[SWL]]></category>
		<guid isPermaLink="false">https://thejeshgn.com/?p=38666</guid>

					<description><![CDATA[#30DaysOfDXing is where I am trying to receive various radio wave transmissions and listen to them using either my Radio, SDR, etc. I am currently using ShortwaveSchedule, Short-Wave Info, ShorWave DB, QSL.net, etc. as my sources. Look at the project page for more details. I am a radio enthusiast and also an E&#38;C&#46;&#46;&#46;]]></description>
										<content:encoded><![CDATA[
<p><a href="https://thejeshgn.com/projects/30-days-of-dxing/" target="_blank" rel="noreferrer noopener">#30DaysOfDXing</a> is where I am trying to receive various radio wave transmissions and listen to them using either my <a href="https://thejeshgn.com/2022/03/14/swl-shortwave-listening-using-eton-elite-traveler/" target="_blank" rel="noreferrer noopener">Radio</a>, <a href="https://thejeshgn.com/2018/10/22/getting-started-with-software-defined-radio-using-rtl-sdr/" target="_blank" rel="noreferrer noopener">SDR</a>, etc. I am currently using <a href="https://shortwaveschedule.com" target="_blank" rel="noreferrer noopener">ShortwaveSchedule</a>, <a href="https://www.short-wave.info" target="_blank" rel="noreferrer noopener">Short-Wave Info</a>, <a href="https://shortwavedb.org/" target="_blank" rel="noreferrer noopener">ShorWave DB</a>, <a href="https://admin.qsl.net/index.php" target="_blank" rel="noreferrer noopener">QSL.net</a>, etc. as my sources. Look at the project page for more details.</p>



<p>I am a radio enthusiast and also an E&amp;C engineer. Though fields and waves, or antenna theory, were not my favorite subjects in engineering, I love radio waves and listening to them. I think if I had more practice-oriented classes during my engineering studies, I would have loved those subjects much more than I do now that I practice. May be something for me to remember when I teach or share.</p>



<p class="has-luminous-vivid-amber-background-color has-background">DXing, taken from DX, the telegraphic shorthand for &#8220;distance&#8221; or &#8220;distant&#8221;, is the hobby of receiving and identifying distant radio or television signals, or making two-way radio contact with distant stations in amateur radio, citizens band radio or other two-way radio communications. &#8211; <a href="https://en.wikipedia.org/wiki/DXing">Wikipedia</a></p>



<p>If you are new to Short Wave Listening (SWL) or the HAM radio world, <a href="https://www.dxing.info/introduction.dx" target="_blank" rel="noreferrer noopener">DXing</a> generally means listening to distant signals. Still, I am not limiting myself to only the &#8220;distant&#8221; signals in this project, nor to radio or television signals. I am going to try local signals as well, for example, <a href="https://en.wikipedia.org/wiki/Non-directional_beacon" target="_blank" rel="noreferrer noopener">Non-Directional Beacon (NDB)</a> from Bangalore Airport is a fair game. I might also do local FM stations, just to learn. The focus is on learning and trying new things.</p>



<figure class="wp-block-audio aligncenter"><audio controls src="https://thejeshgn.com/wp-content/uploads/2026/04/sw-13710khz-20260415-1834gmt-bengaluru.mp3"></audio><figcaption class="wp-element-caption"><em>SW 13710kHz on 20260415 at 1834 GMT heard in Bengaluru. Transmission by China Radio International, Kunming Anning, China. Received using Eton Elite Traveler.</em></figcaption></figure>



<p>My plan is not to spend more than 30 minutes a day on this. I should be able to achieve it. I have already started logging them at <a href="https://thejeshgn.com/projects/30-days-of-dxing/" target="_blank" rel="noreferrer noopener">#30DaysOfDXing</a>, along with all the details. If it interests you, contact me. Maybe we can do it together.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><a href="https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-scaled.jpg" rel="lightbox[38666]"><img loading="lazy" decoding="async" width="1024" height="768" data-id="19700" src="https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-1024x768.jpg" alt="NESDR and RH 795" class="wp-image-19700" srcset="https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-1024x768.jpg 1024w, https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-300x225.jpg 300w, https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-768x576.jpg 768w, https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-1536x1152.jpg 1536w, https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-2048x1536.jpg 2048w, https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-720x540.jpg 720w, https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-520x390.jpg 520w, https://thejeshgn.com/wp-content/uploads/2021/01/wp-16118952817434718948263944756454-320x240.jpg 320w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption">NESDR and  RH 795 with SMA Male to BNC Female cable.</figcaption></figure>



<figure class="wp-block-image size-large"><a href="https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-scaled.jpg" rel="lightbox[38666]"><img loading="lazy" decoding="async" width="1024" height="768" data-id="21656" src="https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-1024x768.jpg" alt="Eton Elite Traveler Radio" class="wp-image-21656" srcset="https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-1024x768.jpg 1024w, https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-300x225.jpg 300w, https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-768x576.jpg 768w, https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-1536x1152.jpg 1536w, https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-2048x1536.jpg 2048w, https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-720x540.jpg 720w, https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-520x390.jpg 520w, https://thejeshgn.com/wp-content/uploads/2022/03/wp-1647254739433-320x240.jpg 320w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption">Eton Elite Traveler Radio</figcaption></figure>
</figure>



<hr class="wp-block-separator has-text-color has-vivid-purple-color has-alpha-channel-opacity has-vivid-purple-background-color has-background is-style-dots"/>



<div class="wp-block-columns has-background is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex" style="background:linear-gradient(137deg,rgb(255,206,236) 0%,rgb(152,150,240) 62%)">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p></p>



<p>You can read this blog using <a href="https://feeds.thejeshgn.com/thejeshgn" target="_blank" rel="noreferrer noopener">RSS Feed</a>. But if you are the person who loves getting emails, then you can join my readers by <a href="https://thejeshgn.com/subscribe/" target="_blank" rel="noreferrer noopener">signing</a> up.</p>


<div class="wp-block-jetpack-subscriptions__supports-newline wp-block-jetpack-subscriptions__show-subs wp-block-jetpack-subscriptions">
		<div>
			<div>
				<div>
					<p style="width: 25%;max-width: 100%;">
						<a href="https://thejeshgn.com/?post_type=post&#038;p=38666" style="width: calc(100% - 10px);font-size: 16px;padding: 15px 23px 15px 23px;margin: 0; margin-left: 10px;border-color: black;border-radius: 6px;border-width: 2px; background-color: #000000; color: #FFFFFF; text-decoration: none; white-space: nowrap; margin-left: 0">Subscribe</a>
					</p>
				</div>
			</div>
		</div>
	</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>
</div>



<p> </p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://thejeshgn.com/2026/04/20/30-days-of-dxing/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://thejeshgn.com/wp-content/uploads/2026/04/sw-13710khz-20260415-1834gmt-bengaluru.mp3" length="927952" type="audio/mpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">38666</post-id>	</item>
		<item>
		<title>Running Bash programs on Moodle CodeRunner</title>
		<link>https://thejeshgn.com/2026/03/09/running-bash-programs-on-moodle-coderunner/</link>
					<comments>https://thejeshgn.com/2026/03/09/running-bash-programs-on-moodle-coderunner/#respond</comments>
		
		<dc:creator><![CDATA[Thejesh GN]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 06:09:45 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[CodeRunner]]></category>
		<category><![CDATA[Free and Open Source]]></category>
		<category><![CDATA[MooC]]></category>
		<category><![CDATA[Moodle]]></category>
		<location><![CDATA[APU]]></location>
		<guid isPermaLink="false">https://thejeshgn.com/?p=38270</guid>

					<description><![CDATA[I have been teaching a course at APU that includes Bash scripting. I have a love-hate relationship with Bash. It&#8217;s a weird combination of a programming language and an iterative CLI. It&#8217;s confusing, easy to make mistakes, and hard to debug, but on the other hand, it&#8217;s available on almost all systems. It&#46;&#46;&#46;]]></description>
										<content:encoded><![CDATA[
<p>I have been teaching a <a href="https://thejeshgn.com/projects/exploring-digital-technology/">course at APU</a> that includes Bash scripting. I have a love-hate relationship with <a href="https://www.gnu.org/software/bash/manual/html_node/index.html" target="_blank" rel="noreferrer noopener">Bash</a>. It&#8217;s a weird combination of a programming language and an iterative CLI. It&#8217;s confusing, easy to make mistakes, and hard to debug, but on the other hand, it&#8217;s available on almost all systems. It builds on the power of CLI and CLI tools available in the OS. Easy to write small and useful scripts that can automate your daily painful work. Hence, it&#8217;s worth knowing a bit of it even today.</p>



<p>The platform we (APU) use is Moodle. I have been in the MOOC industry for a decade now, and I have heard of Moodle so much, but this is the first time I have used it to run a course. To run a programming course, you will need an easy programming environment to challenge students. In my previous cases, we have used <a href="https://nsjail.dev/" target="_blank" rel="noreferrer noopener">an NSJail-based</a> environment with <a href="https://thejeshgn.com/tag/course-builder/">CourseBuilder</a> (now called Seek), which works really well. But in this case, for <a href="https://moodle.org/">Moodle</a>, it&#8217;s <a href="https://coderunner.org.nz/" target="_blank" rel="noreferrer noopener">CodeRunner</a> plugin. It seems fairly easy to use. That said, Bash is not supported out of the box as user language. So I had to use Python to make it possible. This also assumes the environment (CodeRunner/JOBE) has Bash installed, though not directly accessible through API as user language.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; auto-links: false; title: ; notranslate">
import subprocess

script = &quot;&quot;&quot;{{ TEST.testcode | e(&#039;py&#039;) }}&quot;&quot;&quot; + &#039;\n&#039; + &quot;&quot;&quot;{{ STUDENT_ANSWER | e(&#039;py&#039;) }}&quot;&quot;&quot; + &#039;\n&#039; + &quot;&quot;&quot;{{ TEST.extra | e(&#039;py&#039;) }}&quot;&quot;&quot;
input = &quot;&quot;&quot;{{ TEST.stdin | e(&#039;py&#039;) }}&quot;&quot;&quot;

with open(&#039;__prog__.sh&#039;, &#039;w&#039;) as outfile:
    outfile.write(script)

result = subprocess.run(&#x5B;&#039;/bin/bash&#039;, &#039;__prog__.sh&#039;], capture_output=True, text=True, input=input, timeout=5)
stdout = result.stdout.strip()
print(stdout)
</pre></div>


<p>The template Python code takes the <code>TEST.testcod</code>e and prepends it to the user-entered <code>STUDENT_ANSWER</code> code. It also takes <code>TEST.extra</code> code and appends it. Then, it runs it as a bash script using Python subprocess by passing <code>TEST.stdin</code> as input. Captures STDOUT and prints it for comparison.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash.png" rel="lightbox[38270]"><img loading="lazy" decoding="async" width="1024" height="419" src="https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-1024x419.png" alt="" class="wp-image-38267" srcset="https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-1024x419.png 1024w, https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-300x123.png 300w, https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-768x314.png 768w, https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-1536x629.png 1536w, https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-2048x838.png 2048w, https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-720x295.png 720w, https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-520x213.png 520w, https://thejeshgn.com/wp-content/uploads/2026/03/python_code_customization_to_run_bash-320x131.png 320w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption">CodeRunner in Moodle. Customization using Templates for using Python to run Bash scripts written by user.</figcaption></figure>
</div>


<p>Example Question 1: Read an input as <code>score</code>. If the <code>score</code> is greater than or equal to 40, then print <code>P</code>. If the score is less than 40, then print <code>U</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs.png" rel="lightbox[38270]"><img loading="lazy" decoding="async" width="1024" height="504" src="https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-1024x504.png" alt="" class="wp-image-38268" srcset="https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-1024x504.png 1024w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-300x148.png 300w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-768x378.png 768w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-1536x756.png 1536w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-2048x1008.png 2048w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-720x354.png 720w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-520x256.png 520w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_inputs-320x157.png 320w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption">CodeRunner in Moodle. Example Bash question test case where STDIO is read and used. </figcaption></figure>
</div>


<p>Example Question 2: Write a function called <code>cube</code>. If a number is passed, it returns the cube of that number. For example <code>cube 5 # will return 25</code></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code.png" rel="lightbox[38270]"><img loading="lazy" decoding="async" width="1024" height="504" src="https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-1024x504.png" alt="" class="wp-image-38269" srcset="https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-1024x504.png 1024w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-300x148.png 300w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-768x378.png 768w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-1536x756.png 1536w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-2048x1008.png 2048w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-720x354.png 720w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-520x256.png 520w, https://thejeshgn.com/wp-content/uploads/2026/03/bash_question_testcase_with_additional_code-320x157.png 320w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption">CodeRunner in Moodle. Example Bash question where we want to call a function user has written at the end. So it can be tested.</figcaption></figure>
</div>


<p>We did about three in-class labs using this. It worked well for all our cases. Though I must say we tried only Bash basics. Maybe there are use cases where this might break, but I think for most of it, this should work. </p>



<hr class="wp-block-separator has-text-color has-vivid-purple-color has-alpha-channel-opacity has-vivid-purple-background-color has-background is-style-dots"/>



<div class="wp-block-columns has-background is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex" style="background:linear-gradient(137deg,rgb(255,206,236) 0%,rgb(152,150,240) 62%)">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p></p>



<p>You can read this blog using <a href="https://feeds.thejeshgn.com/thejeshgn" target="_blank" rel="noreferrer noopener">RSS Feed</a>. But if you are the person who loves getting emails, then you can join my readers by <a href="https://thejeshgn.com/subscribe/" target="_blank" rel="noreferrer noopener">signing</a> up.</p>


<div class="wp-block-jetpack-subscriptions__supports-newline wp-block-jetpack-subscriptions__show-subs wp-block-jetpack-subscriptions">
		<div>
			<div>
				<div>
					<p style="width: 25%;max-width: 100%;">
						<a href="https://thejeshgn.com/?post_type=post&#038;p=38270" style="width: calc(100% - 10px);font-size: 16px;padding: 15px 23px 15px 23px;margin: 0; margin-left: 10px;border-color: black;border-radius: 6px;border-width: 2px; background-color: #000000; color: #FFFFFF; text-decoration: none; white-space: nowrap; margin-left: 0">Subscribe</a>
					</p>
				</div>
			</div>
		</div>
	</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>
</div>



<p> </p>
]]></content:encoded>
					
					<wfw:commentRss>https://thejeshgn.com/2026/03/09/running-bash-programs-on-moodle-coderunner/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">38270</post-id>	</item>
		<item>
		<title>Motorola T82 Extreme is my PMR446 Walkie-Talkie</title>
		<link>https://thejeshgn.com/2026/02/09/motorola-t82-extreme-is-my-pmr446-walkie-talkie/</link>
					<comments>https://thejeshgn.com/2026/02/09/motorola-t82-extreme-is-my-pmr446-walkie-talkie/#respond</comments>
		
		<dc:creator><![CDATA[Thejesh GN]]></dc:creator>
		<pubDate>Mon, 09 Feb 2026 01:50:54 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Amateur (ham) Radio]]></category>
		<category><![CDATA[Gadgets]]></category>
		<category><![CDATA[Geek-Fun]]></category>
		<category><![CDATA[India 🇮🇳]]></category>
		<category><![CDATA[Motorcycling]]></category>
		<category><![CDATA[Review]]></category>
		<category><![CDATA[Riding Gear]]></category>
		<category><![CDATA[Travel Gadgets]]></category>
		<guid isPermaLink="false">https://thejeshgn.com/?p=38059</guid>

					<description><![CDATA[As a kid, I always wanted a set of Walkie Talkies to chat with friends. We got cell phones as adults, but that desire remained. A few years back, I took the HAM exam, but then COVID hit us, and I didn&#8217;t apply for a call sign. Now I have to retake the&#46;&#46;&#46;]]></description>
										<content:encoded><![CDATA[
<p>As a kid, I always wanted a set of Walkie Talkies to chat with friends. We got cell phones as adults, but that desire remained. A few years back, I took the HAM exam, but then COVID hit us, and I didn&#8217;t apply for a call sign. Now I have to retake the exams online, as the system now doesn&#8217;t allow me to apply for a call sign with paper exam results. I will take that exam again. But in the meantime, I also wanted something that I could use with friends and family who I don&#8217;t think are interested in taking an exam or applying for a license. Hence this search.</p>






<p>I do have a kind of Walkie-talkie in the form of <a href="https://thejeshgn.com/2024/10/30/review-bluarmor-c30-mesh-intercom-for-bikers/">Helmet communications</a>. There are some issues with using it as a general communications device. Some that matter to me are</p>



<ul class="wp-block-list">
<li>It&#8217;s not open; it&#8217;s proprietary. The only part of that ecosystem that is open is <a href="https://community.sena.com/hc/en-us/articles/360001060263-Universal-Intercom-Pairing" target="_blank" rel="noreferrer noopener">Sena&#8217;s Universal Intercom protocol</a>, which uses Bluetooth. It comes with its own limitations.</li>



<li>It&#8217;s made for helmet communication. Even if you plan to reuse it, it&#8217;s challenging to use in other situations due to its form factor.</li>



<li>It uses 2.5 GHz (the same frequency range as Bluetooth, Wi-Fi, etc.), so its range is somewhat limited. In our tests, it was around 700-900m direct line of sight. Some support a proprietary mesh network to increase the range.</li>
</ul>



<h3 class="wp-block-heading">Requirements</h3>



<p>With all this, I was left to look for a walkie-talkie. </p>



<ul class="wp-block-list">
<li>Is the license free in India? Ideally, across the world, but at least in India.</li>



<li>Open protocols</li>



<li>Decent range</li>



<li>Multi purpose</li>



<li>Easy to use, it shouldn&#8217;t take more than five minutes to learn its functionality</li>



<li>Easy to manage, charge, and rugged. I plan to use it everywhere.</li>



<li>Widely recognized brand and model. So I don&#8217;t get stopped at the airport and other places while I am carrying it.</li>



<li>Supported in India</li>



<li>True walkie-talkie, not PTT over the cell network.</li>
</ul>



<h3 class="wp-block-heading">License-free and open</h3>



<p>There are two bands used for public, license-free communication without encryption. The <a target="_blank" href="https://en.wikipedia.org/wiki/Family_Radio_Service" rel="noreferrer noopener">Family Radio Service (FRS)</a> is used in the USA and Canada. FRS operates between 462.5625 MHz and 467.7125 MHz. And <a target="_blank" href="https://en.wikipedia.org/wiki/PMR446" rel="noreferrer noopener">PMR446 (private mobile radio</a> used in most of Europe and India.</p>



<p>PMR446 is a private mobile radio that operates between 446.0 &#8211; 446.2 MHz in India. It&#8217;s a license-free band, as per the <a target="_blank" href="https://thejeshgn.com/documents/delicensing-low-power-and-very-low-power-short-range-radio-frequency-devices-g-s-r-1047e/" rel="noreferrer noopener">Gazette Notification G.S.R. 1047(E)</a>. Since I am dealing with India, I will focus on it. The rules limit output power to 0.5 W. It doesn&#8217;t seem like much, but you can reach a kilometer or more in urban conditions with buildings and trees. A 5KM to 10KM in direct line of sight conditions. It&#8217;s much better than my helmet communication system. The rules allow channel spacing of 6.25 kHz (Digital) and 12.5 kHz (analog). The frequency range can accommodate 16 analog or 32 digital channels. I have a dedicated page about <a target="_blank" href="https://thejeshgn.com/wiki/notebook/pmr446-personal-mobile-radio-at-446-mhz/" rel="noreferrer noopener">the PMR446 band, channels, channel guarding</a>, etc. The most important thing to know is that we need to select walkie-talkies that operate PMR446 in India.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://thejeshgn.com/documents/delicensing-low-power-and-very-low-power-short-range-radio-frequency-devices-g-s-r-1047e/"><img loading="lazy" decoding="async" width="898" height="375" src="https://thejeshgn.com/wp-content/uploads/2024/11/GSR1047E-PMR446.png" alt="G.S.R. 1047(E) [PART II—SEC. 3(i)] Table V - Personal Mobile Radio at 446 MHz" class="wp-image-32962" srcset="https://thejeshgn.com/wp-content/uploads/2024/11/GSR1047E-PMR446.png 898w, https://thejeshgn.com/wp-content/uploads/2024/11/GSR1047E-PMR446-300x125.png 300w, https://thejeshgn.com/wp-content/uploads/2024/11/GSR1047E-PMR446-768x321.png 768w, https://thejeshgn.com/wp-content/uploads/2024/11/GSR1047E-PMR446-720x301.png 720w, https://thejeshgn.com/wp-content/uploads/2024/11/GSR1047E-PMR446-520x217.png 520w, https://thejeshgn.com/wp-content/uploads/2024/11/GSR1047E-PMR446-320x134.png 320w" sizes="auto, (max-width: 898px) 100vw, 898px" /></a><figcaption class="wp-element-caption">G.S.R. 1047(E) [PART II—SEC. 3(i)] Table V &#8211; Personal Mobile Radio at 446 MHz</figcaption></figure>
</div>


<p class="has-luminous-vivid-amber-background-color has-background">I have shown the snippet from the Gazette and given link to the rules for your reference. Please get your own legal advice. I am not a lawyer, and this is not legal advice.</p>



<p>The protocols are standardized, and most follow the ETSI Harmonised European Standard. Since this is something all <a target="_blank" href="https://en.wikipedia.org/wiki/PMR446#Technical_information" rel="noreferrer noopener">vendors use</a>, if someone wants to build, the protocol is not a secret sauce. This also makes the technology and walkies vendor-neutral.</p>



<h3 class="wp-block-heading">Privacy</h3>



<p>There is no encryption on PMR446. So you shouldn&#8217;t communicate anything that needs encryption on these bands. <mark style="background-color:#fcb900" class="has-inline-color">There is no privacy.</mark> There are ways to keep a set of people independent by using a specific channel and Squelch (CTSS or DCS). Squelch is called PL (Private Line) tone, CG (Channel Guard) tone, or QC (Quiet Channel) tone, depending on the vendor, but all refer to the same functionality. Basically, you use a channel and a specific CTSS or DCS code to keep your conversations independent from others. This setup doesn&#8217;t stop others from using the same combo and listening to you.</p>



<h3 class="wp-block-heading">Motorola T82 Extreme</h3>



<p>Armed with this information and requirement, I started my search, and my final list was</p>



<ul class="wp-block-list">
<li>Motorola Talkabout T82 Extreme &#8211; <a href="https://thejeshgn.com/documents/motorola-talkabout-t82-extreme-user-manual/">Manual</a></li>



<li>Kenwood TK 3501</li>



<li>Wavex PT100</li>



<li>Vertel Team Talkie Radio</li>



<li>Sanchar G3U</li>



<li>Baofeng GT-68 PMR Walkie Talkie</li>
</ul>



<p>If I had money, I would have tried each of them and then decided which to go with. Based on the information I found online, I went with the T82 by Motorola. If you have any other model listed here, let me know, and we can test them together.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-2 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><a href="https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-scaled.png" rel="lightbox[38059]"><img loading="lazy" decoding="async" width="724" height="1024" data-id="38062" src="https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-724x1024.png" alt="" class="wp-image-38062" srcset="https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-724x1024.png 724w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-212x300.png 212w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-768x1086.png 768w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-1086x1536.png 1086w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-1448x2048.png 1448w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-720x1018.png 720w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-520x736.png 520w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-320x453.png 320w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-freebrochure-1-scaled.png 1810w" sizes="auto, (max-width: 724px) 100vw, 724px" /></a></figure>



<figure class="wp-block-image size-large"><a href="https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-scaled.png" rel="lightbox[38059]"><img loading="lazy" decoding="async" width="724" height="1024" data-id="38063" src="https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-724x1024.png" alt="" class="wp-image-38063" srcset="https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-724x1024.png 724w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-212x300.png 212w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-768x1086.png 768w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-1086x1536.png 1086w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-1448x2048.png 1448w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-720x1018.png 720w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-520x736.png 520w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-320x453.png 320w, https://thejeshgn.com/wp-content/uploads/2026/02/motorola-t82-extreme-walkie-talkies-licence-free-brochure-2-scaled.png 1810w" sizes="auto, (max-width: 724px) 100vw, 724px" /></a></figure>
</figure>



<p></p>



<p>In terms of cost, it wasn&#8217;t cheap; a pair of them cost me INR 18,000. It came in a package with two walkies, a carrying case, a charger, NIMH rechargeable batteries, earpieces, belt clips, etc. It&#8217;s probably the most expensive pair. G3U or PT100 is half the price.  </p>



<h4 class="wp-block-heading">Things I like</h4>



<ol class="wp-block-list">
<li>Brand recognition. Everyone knows Motorola. It also looks colorful, playful, rugged, and harmless. The support in India is good, and the community around it is also good. I have had other Motorola phones (pre-smartphone era cell phones), and they have been very rugged and have had very positive experiences in general.</li>



<li>It&#8217;s rugged. It can be used in conditions where others can&#8217;t be used. I won&#8217;t be scared to put it on a motorcycle handlebar or hang it outside my backpack. I am also not worried about water splashing on it (IPX4), though I wouldn&#8217;t submerge it. Also, not worried about accidentally dropping them.</li>



<li>A replaceable, rechargeable, 800 mAh NIMH battery powers it. It can be recharged using a micro USB charger. That said, you can replace the NIMH battery with 3 x AA Alkaline batteries in an emergency. The promised battery life is around 18 hours. Even if it&#8217;s just half, I will be happy. One can also upgrade to a 1300 mAh NiMH battery if required.</li>



<li>It comes with a headset with a boom mic. That makes it usable inside the helmet if you want, or use it hands-free. It has VOX/iVOX (Internal Voice Operated / Voice Operated Transmission ), which lets you transmit without pressing a button. It has three levels of sensitivity for voice activation. By default, it is in PTT (Push To Talk) mode, where you need to press and hold a button to talk. In VOX mode, sound activates the transmission. So all you do is speak, and the radio will transmit for you. This is especially useful when you are riding or doing manual work.</li>



<li>It supports 8 PMR Channels. User expandable to 16 Channels in countries where it&#8217;s allowed by government authorities. And 121 Sub-Codes (38 CTCSS Codes &amp; 83 DCS codes). So, there are plenty of options to isolate your group from others.</li>



<li>It&#8217;s very easy to use. Manuals and settings are very easy. It probably takes less than 5 minutes to teach someone to start using it effectively.</li>



<li>It has an Emergency Alert Mode with a dedicated button that can signal members of your group for help.</li>



<li>It has dual-channel monitoring. It lets you listen to two channels and engage with the primary one.</li>



<li>There are other features, such as a flashlight, roger Tone, channel monitoring and scanning, easy pairing, etc.</li>



<li>The range has been good. I was able to get at least 1 KM in urban settings with buildings. I continue to do more range tests. I will write a separate post about it.</li>



<li>As such, there is no limit to the number of folks you can have on the same channel. This is true for most Walkie-Talkies.</li>
</ol>



<h4 class="wp-block-heading">Things to improve</h4>



<ol class="wp-block-list">
<li>Micro USB. It should have been USB-C even if it was just 5 watts.</li>



<li>Price. It is expensive. I am looking to try Wavex PT100 and Vertel, which are half the price. If you have, let me know. We can compare.</li>
</ol>



<h3 class="wp-block-heading">Conclusion</h3>



<p>Should you get it? Maybe. It depends on your need. But once you start using it, you will see how useful it becomes. Especially if you ride or drive a lot in teams, or have a farm or work in a place with poor or no network, trek, or run events, etc.  As far as me, I am still going to use <a href="https://thejeshgn.com/2024/10/30/review-bluarmor-c30-mesh-intercom-for-bikers/">BluArmor</a> while riding. But I will surely carry it along with me. </p>



<p>It&#8217;s also a good entry point into HAM radio. It&#8217;s good enough to generate interest among kids and adults about analog communications.</p>



<hr class="wp-block-separator has-text-color has-vivid-purple-color has-alpha-channel-opacity has-vivid-purple-background-color has-background is-style-dots"/>



<div class="wp-block-columns has-background is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex" style="background:linear-gradient(137deg,rgb(255,206,236) 0%,rgb(152,150,240) 62%)">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p></p>



<p>You can read this blog using <a href="https://feeds.thejeshgn.com/thejeshgn" target="_blank" rel="noreferrer noopener">RSS Feed</a>. But if you are the person who loves getting emails, then you can join my readers by <a href="https://thejeshgn.com/subscribe/" target="_blank" rel="noreferrer noopener">signing</a> up.</p>


<div class="wp-block-jetpack-subscriptions__supports-newline wp-block-jetpack-subscriptions__show-subs wp-block-jetpack-subscriptions">
		<div>
			<div>
				<div>
					<p style="width: 25%;max-width: 100%;">
						<a href="https://thejeshgn.com/?post_type=post&#038;p=38059" style="width: calc(100% - 10px);font-size: 16px;padding: 15px 23px 15px 23px;margin: 0; margin-left: 10px;border-color: black;border-radius: 6px;border-width: 2px; background-color: #000000; color: #FFFFFF; text-decoration: none; white-space: nowrap; margin-left: 0">Subscribe</a>
					</p>
				</div>
			</div>
		</div>
	</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>
</div>



<p> </p>
]]></content:encoded>
					
					<wfw:commentRss>https://thejeshgn.com/2026/02/09/motorola-t82-extreme-is-my-pmr446-walkie-talkie/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">38059</post-id>	</item>
		<item>
		<title>One Video Format</title>
		<link>https://thejeshgn.com/2026/01/19/one-video-format/</link>
					<comments>https://thejeshgn.com/2026/01/19/one-video-format/#respond</comments>
		
		<dc:creator><![CDATA[Thejesh GN]]></dc:creator>
		<pubDate>Mon, 19 Jan 2026 18:14:45 +0000</pubDate>
				<category><![CDATA[Podcast]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Free and Open Source]]></category>
		<category><![CDATA[Web 🌐]]></category>
		<guid isPermaLink="false">https://thejeshgn.com/?p=37947</guid>

					<description><![CDATA[I have a bunch of screen-casts that I want to release. I have not found a good PeerTube instance to host my videos. I am happy to pay a monthly fee, and I hope one day there will be a platform that provides this. For now, I will add them to my site,&#46;&#46;&#46;]]></description>
										<content:encoded><![CDATA[
<p>I have a bunch of screen-casts that I want to release. I have not found a good PeerTube instance to host my videos. I am happy to pay a monthly fee, and I hope one day there will be a platform that provides this.</p>



<p> For now, I will add them to my site, Archive.org, and YouTube. They are all CC-BY-SA. </p>



<p>I am thinking of uploading just one version of each video. No server side transcoding. Hence, I want to convert the videos into the most optimized format and size. I have been doing <a href="https://thejeshgn.com/wiki/styles/video-formatting-and-embedding/">some experiments</a> and seem to have found decent settings. Its 1080p or 720p with 24 frames. </p>



<p>If its mobile screencast then I will pad them with color, so it can be a proper FHD or HD.</p>



<p>I am going to use WebM as the container with AV1 as video codec and Opus as audio codec. Most screencasts could be just 720p.</p>



<figure class="wp-block-video aligncenter"><video height="720" style="aspect-ratio: 1280 / 720;" width="1280" controls src="https://thejeshgn.com/wp-content/uploads/2026/01/osmand_route_using_gpx_720p_24fps_bgcolor.webm"></video><figcaption class="wp-element-caption">OSMAnd routing using GPX file, 720p aka HD, <strong>2</strong> minutes, 17 seconds, 10MB</figcaption></figure>



<p></p>



<figure class="wp-block-video aligncenter"><video height="1080" style="aspect-ratio: 1920 / 1080;" width="1920" controls src="https://thejeshgn.com/wp-content/uploads/2026/01/Big_Buck_Bunny_1080_10s_1MB.webm"></video><figcaption class="wp-element-caption">Test video &#8211; Big Buck Bunny 1080p, 10 seconds, 1 MB file size</figcaption></figure>



<hr class="wp-block-separator has-text-color has-vivid-purple-color has-alpha-channel-opacity has-vivid-purple-background-color has-background is-style-dots"/>



<div class="wp-block-columns has-background is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex" style="background:linear-gradient(137deg,rgb(255,206,236) 0%,rgb(152,150,240) 62%)">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p></p>



<p>You can read this blog using <a href="https://feeds.thejeshgn.com/thejeshgn" target="_blank" rel="noreferrer noopener">RSS Feed</a>. But if you are the person who loves getting emails, then you can join my readers by <a href="https://thejeshgn.com/subscribe/" target="_blank" rel="noreferrer noopener">signing</a> up.</p>


<div class="wp-block-jetpack-subscriptions__supports-newline wp-block-jetpack-subscriptions__show-subs wp-block-jetpack-subscriptions">
		<div>
			<div>
				<div>
					<p style="width: 25%;max-width: 100%;">
						<a href="https://thejeshgn.com/?post_type=post&#038;p=37947" style="width: calc(100% - 10px);font-size: 16px;padding: 15px 23px 15px 23px;margin: 0; margin-left: 10px;border-color: black;border-radius: 6px;border-width: 2px; background-color: #000000; color: #FFFFFF; text-decoration: none; white-space: nowrap; margin-left: 0">Subscribe</a>
					</p>
				</div>
			</div>
		</div>
	</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:5px"></div>
</div>



<p> </p>
]]></content:encoded>
					
					<wfw:commentRss>https://thejeshgn.com/2026/01/19/one-video-format/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://thejeshgn.com/wp-content/uploads/2026/01/osmand_route_using_gpx_720p_24fps_bgcolor.webm" length="10511359" type="video/webm" />
<enclosure url="https://thejeshgn.com/wp-content/uploads/2026/01/Big_Buck_Bunny_1080_10s_1MB.webm" length="1059560" type="video/webm" />

		<post-id xmlns="com-wordpress:feed-additions:1">37947</post-id>	</item>
	</channel>
</rss>
