<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://mahoretushar.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://mahoretushar.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-10-12T10:41:22+00:00</updated><id>https://mahoretushar.github.io/feed.xml</id><title type="html">Tushar Mahore</title><subtitle>Assistant Professor, ICEM · PhD Student, Symbiosis Institute of Technology · NLP &amp; Crisis Informatics Researcher </subtitle><entry><title type="html">Building a RAG Pipeline from Scratch with Python</title><link href="https://mahoretushar.github.io/blog/2026/rag-pipeline-from-scratch/" rel="alternate" type="text/html" title="Building a RAG Pipeline from Scratch with Python"/><published>2026-04-10T09:00:00+00:00</published><updated>2026-04-10T09:00:00+00:00</updated><id>https://mahoretushar.github.io/blog/2026/rag-pipeline-from-scratch</id><content type="html" xml:base="https://mahoretushar.github.io/blog/2026/rag-pipeline-from-scratch/"><![CDATA[<p>Large Language Models are impressive — until they hallucinate a citation that doesn’t exist, or confidently answer a question about events after their training cutoff. <strong>Retrieval-Augmented Generation (RAG)</strong> is the architectural fix: instead of relying purely on parametric memory baked into model weights, we retrieve relevant documents at inference time and ground the response in them.</p> <p>This post walks through building a minimal but complete RAG pipeline from scratch. No LangChain magic — just the moving parts, visible.</p> <hr/> <h2 id="what-we-are-building">What We Are Building</h2> <p>A system that:</p> <ol> <li>Ingests a set of documents and indexes them by semantic meaning</li> <li>Takes a user query, finds the most relevant passages</li> <li>Sends the query + retrieved passages to an LLM to generate a grounded answer</li> </ol> <pre><code class="language-mermaid">flowchart TD
    A[Documents] --&gt; B[Chunking]
    B --&gt; C[Embedding Model\nsentence-transformers]
    C --&gt; D[(FAISS Index)]

    Q[User Query] --&gt; E[Query Embedding]
    E --&gt; F[Similarity Search]
    D --&gt; F
    F --&gt; G[Top-k Passages]
    G --&gt; H[Prompt Assembly]
    Q --&gt; H
    H --&gt; I[LLM Generator]
    I --&gt; J[Grounded Answer]

    style A fill:#e8f4fd
    style Q fill:#fff3e0
    style J fill:#e8f5e9
</code></pre> <hr/> <h2 id="setup">Setup</h2> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>sentence-transformers faiss-cpu transformers torch
</code></pre></div></div> <p>For this walkthrough we use:</p> <ul> <li><strong>sentence-transformers</strong> — embedding documents and queries</li> <li><strong>FAISS</strong> — approximate nearest-neighbour index</li> <li><strong>transformers</strong> — local LLM for generation (or swap in an API)</li> </ul> <hr/> <h2 id="step-1--document-loading-and-chunking">Step 1 — Document Loading and Chunking</h2> <p>RAG works on chunks, not full documents. A 10-page PDF fed as one string drowns the model in irrelevant context. We split into overlapping chunks so no information is cut off at a boundary.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">List</span>

<span class="k">def</span> <span class="nf">load_documents</span><span class="p">(</span><span class="n">paths</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="sh">"""</span><span class="s">Load plain-text documents from file paths.</span><span class="sh">"""</span>
    <span class="n">docs</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">paths</span><span class="p">:</span>
        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="sh">"</span><span class="s">r</span><span class="sh">"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="n">docs</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">())</span>
    <span class="k">return</span> <span class="n">docs</span>


<span class="k">def</span> <span class="nf">chunk_text</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">chunk_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">300</span><span class="p">,</span> <span class="n">overlap</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">50</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="sh">"""</span><span class="s">Split text into overlapping word-level chunks.</span><span class="sh">"""</span>
    <span class="n">words</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="nf">split</span><span class="p">()</span>
    <span class="n">chunks</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">start</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">while</span> <span class="n">start</span> <span class="o">&lt;</span> <span class="nf">len</span><span class="p">(</span><span class="n">words</span><span class="p">):</span>
        <span class="n">end</span> <span class="o">=</span> <span class="n">start</span> <span class="o">+</span> <span class="n">chunk_size</span>
        <span class="n">chunk</span> <span class="o">=</span> <span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">words</span><span class="p">[</span><span class="n">start</span><span class="p">:</span><span class="n">end</span><span class="p">])</span>
        <span class="n">chunks</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
        <span class="n">start</span> <span class="o">+=</span> <span class="n">chunk_size</span> <span class="o">-</span> <span class="n">overlap</span>  <span class="c1"># slide with overlap
</span>    <span class="k">return</span> <span class="n">chunks</span>


<span class="k">def</span> <span class="nf">build_corpus</span><span class="p">(</span><span class="n">paths</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="n">docs</span> <span class="o">=</span> <span class="nf">load_documents</span><span class="p">(</span><span class="n">paths</span><span class="p">)</span>
    <span class="n">corpus</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">docs</span><span class="p">:</span>
        <span class="n">corpus</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="nf">chunk_text</span><span class="p">(</span><span class="n">doc</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">corpus</span>
</code></pre></div></div> <p><strong>Why overlap?</strong> A key sentence might sit at the boundary between two chunks. An overlap of ~50 words ensures it appears fully in at least one chunk.</p> <hr/> <h2 id="step-2--embedding-the-corpus">Step 2 — Embedding the Corpus</h2> <p>We use a bi-encoder — a model that maps any text to a fixed-size dense vector such that semantically similar texts land close together in vector space.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span>
<span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="n">EMBED_MODEL</span> <span class="o">=</span> <span class="sh">"</span><span class="s">sentence-transformers/all-MiniLM-L6-v2</span><span class="sh">"</span>

<span class="k">def</span> <span class="nf">embed_corpus</span><span class="p">(</span><span class="n">corpus</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">model_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">EMBED_MODEL</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Encode all corpus chunks into dense vectors.
    Returns array of shape (n_chunks, embedding_dim).
    </span><span class="sh">"""</span>
    <span class="n">model</span> <span class="o">=</span> <span class="nc">SentenceTransformer</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Embedding </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">corpus</span><span class="p">)</span><span class="si">}</span><span class="s"> chunks...</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">embeddings</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span>
        <span class="n">corpus</span><span class="p">,</span>
        <span class="n">batch_size</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span>
        <span class="n">show_progress_bar</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">normalize_embeddings</span><span class="o">=</span><span class="bp">True</span>  <span class="c1"># L2-normalise for cosine similarity via dot product
</span>    <span class="p">)</span>
    <span class="k">return</span> <span class="n">embeddings</span>
</code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">all-MiniLM-L6-v2</code> is 22M parameters — fast and accurate enough for most use cases. For higher accuracy, swap in <code class="language-plaintext highlighter-rouge">BAAI/bge-large-en-v1.5</code>.</p> <hr/> <h2 id="step-3--building-the-faiss-index">Step 3 — Building the FAISS Index</h2> <p>FAISS (Facebook AI Similarity Search) lets us search millions of vectors in milliseconds using approximate nearest-neighbour algorithms.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">faiss</span>

<span class="k">def</span> <span class="nf">build_index</span><span class="p">(</span><span class="n">embeddings</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">faiss</span><span class="p">.</span><span class="n">IndexFlatIP</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Build a flat inner-product FAISS index.
    With L2-normalised vectors, inner product == cosine similarity.
    </span><span class="sh">"""</span>
    <span class="n">dim</span> <span class="o">=</span> <span class="n">embeddings</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">index</span> <span class="o">=</span> <span class="n">faiss</span><span class="p">.</span><span class="nc">IndexFlatIP</span><span class="p">(</span><span class="n">dim</span><span class="p">)</span>
    <span class="n">index</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">embeddings</span><span class="p">.</span><span class="nf">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">))</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Index built: </span><span class="si">{</span><span class="n">index</span><span class="p">.</span><span class="n">ntotal</span><span class="si">}</span><span class="s"> vectors, dim=</span><span class="si">{</span><span class="n">dim</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">index</span>


<span class="k">def</span> <span class="nf">save_index</span><span class="p">(</span><span class="n">index</span><span class="p">:</span> <span class="n">faiss</span><span class="p">.</span><span class="n">Index</span><span class="p">,</span> <span class="n">path</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">faiss</span><span class="p">.</span><span class="nf">write_index</span><span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="n">path</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">load_index</span><span class="p">(</span><span class="n">path</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">faiss</span><span class="p">.</span><span class="n">Index</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">faiss</span><span class="p">.</span><span class="nf">read_index</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</code></pre></div></div> <p>For large corpora (&gt;100k chunks), replace <code class="language-plaintext highlighter-rouge">IndexFlatIP</code> with <code class="language-plaintext highlighter-rouge">IndexIVFFlat</code> or <code class="language-plaintext highlighter-rouge">IndexHNSWFlat</code> for faster (approximate) search.</p> <hr/> <h2 id="step-4--retrieval">Step 4 — Retrieval</h2> <p>Given a user query, embed it and find the top-k most similar chunks.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">retrieve</span><span class="p">(</span>
    <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">index</span><span class="p">:</span> <span class="n">faiss</span><span class="p">.</span><span class="n">Index</span><span class="p">,</span>
    <span class="n">corpus</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span>
    <span class="n">model_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">EMBED_MODEL</span><span class="p">,</span>
    <span class="n">top_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="sh">"""</span><span class="s">
    Embed query, search FAISS index, return top-k text chunks.
    </span><span class="sh">"""</span>
    <span class="n">model</span> <span class="o">=</span> <span class="nc">SentenceTransformer</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
    <span class="n">query_vec</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span>
        <span class="p">[</span><span class="n">query</span><span class="p">],</span>
        <span class="n">normalize_embeddings</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">)</span>

    <span class="n">scores</span><span class="p">,</span> <span class="n">indices</span> <span class="o">=</span> <span class="n">index</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="n">query_vec</span><span class="p">,</span> <span class="n">top_k</span><span class="p">)</span>

    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">score</span><span class="p">,</span> <span class="n">idx</span> <span class="ow">in</span> <span class="nf">zip</span><span class="p">(</span><span class="n">scores</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">indices</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
        <span class="n">results</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
            <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">:</span> <span class="n">corpus</span><span class="p">[</span><span class="n">idx</span><span class="p">],</span>
            <span class="sh">"</span><span class="s">score</span><span class="sh">"</span><span class="p">:</span> <span class="nf">float</span><span class="p">(</span><span class="n">score</span><span class="p">)</span>
        <span class="p">})</span>
    <span class="k">return</span> <span class="n">results</span>
</code></pre></div></div> <hr/> <h2 id="step-5--prompt-assembly-and-generation">Step 5 — Prompt Assembly and Generation</h2> <p>Retrieved chunks are injected into the prompt as context. The LLM is instructed to answer only from the provided context — the key constraint that reduces hallucination.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">transformers</span> <span class="kn">import</span> <span class="n">pipeline</span>

<span class="k">def</span> <span class="nf">build_prompt</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">context_chunks</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">dict</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="n">context</span> <span class="o">=</span> <span class="sh">"</span><span class="se">\n\n</span><span class="s">---</span><span class="se">\n\n</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span>
        <span class="p">[</span><span class="sa">f</span><span class="sh">"</span><span class="s">[Passage </span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">]</span><span class="se">\n</span><span class="si">{</span><span class="n">c</span><span class="p">[</span><span class="sh">'</span><span class="s">text</span><span class="sh">'</span><span class="p">]</span><span class="si">}</span><span class="sh">"</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">context_chunks</span><span class="p">)]</span>
    <span class="p">)</span>
    <span class="n">prompt</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"""</span><span class="s">You are a helpful assistant. Answer the question using ONLY the provided context.
If the answer is not in the context, say </span><span class="sh">"</span><span class="s">I don</span><span class="sh">'</span><span class="s">t have enough information to answer this.</span><span class="sh">"</span><span class="s">

Context:
</span><span class="si">{</span><span class="n">context</span><span class="si">}</span><span class="s">

Question: </span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="s">

Answer:</span><span class="sh">"""</span>
    <span class="k">return</span> <span class="n">prompt</span>


<span class="k">def</span> <span class="nf">generate_answer</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">model_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="sh">"</span><span class="s">google/flan-t5-base</span><span class="sh">"</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Generate an answer using a local seq2seq model.
    Swap model_name for any HuggingFace-compatible model.
    </span><span class="sh">"""</span>
    <span class="n">generator</span> <span class="o">=</span> <span class="nf">pipeline</span><span class="p">(</span><span class="sh">"</span><span class="s">text2text-generation</span><span class="sh">"</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="n">model_name</span><span class="p">)</span>
    <span class="n">output</span> <span class="o">=</span> <span class="nf">generator</span><span class="p">(</span>
        <span class="n">prompt</span><span class="p">,</span>
        <span class="n">max_new_tokens</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span>
        <span class="n">do_sample</span><span class="o">=</span><span class="bp">False</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">output</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="sh">"</span><span class="s">generated_text</span><span class="sh">"</span><span class="p">]</span>
</code></pre></div></div> <hr/> <h2 id="step-6--putting-it-all-together">Step 6 — Putting It All Together</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">os</span>

<span class="c1"># ── Config ──────────────────────────────────────────────────────────────────
</span><span class="n">DOCS_DIR</span>    <span class="o">=</span> <span class="sh">"</span><span class="s">./documents</span><span class="sh">"</span>
<span class="n">INDEX_PATH</span>  <span class="o">=</span> <span class="sh">"</span><span class="s">./rag_index.faiss</span><span class="sh">"</span>
<span class="n">CORPUS_PATH</span> <span class="o">=</span> <span class="sh">"</span><span class="s">./corpus.txt</span><span class="sh">"</span>

<span class="c1"># ── Build (run once) ─────────────────────────────────────────────────────────
</span><span class="n">doc_paths</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">DOCS_DIR</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">os</span><span class="p">.</span><span class="nf">listdir</span><span class="p">(</span><span class="n">DOCS_DIR</span><span class="p">)</span> <span class="k">if</span> <span class="n">f</span><span class="p">.</span><span class="nf">endswith</span><span class="p">(</span><span class="sh">"</span><span class="s">.txt</span><span class="sh">"</span><span class="p">)</span>
<span class="p">]</span>
<span class="n">corpus</span>    <span class="o">=</span> <span class="nf">build_corpus</span><span class="p">(</span><span class="n">doc_paths</span><span class="p">)</span>
<span class="n">embeddings</span> <span class="o">=</span> <span class="nf">embed_corpus</span><span class="p">(</span><span class="n">corpus</span><span class="p">)</span>
<span class="n">index</span>     <span class="o">=</span> <span class="nf">build_index</span><span class="p">(</span><span class="n">embeddings</span><span class="p">)</span>
<span class="nf">save_index</span><span class="p">(</span><span class="n">index</span><span class="p">,</span> <span class="n">INDEX_PATH</span><span class="p">)</span>

<span class="c1"># Save corpus for retrieval lookups
</span><span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">CORPUS_PATH</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">corpus</span><span class="p">:</span>
        <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">chunk</span> <span class="o">+</span> <span class="sh">"</span><span class="se">\n</span><span class="s">&lt;&lt;&lt;CHUNK_SEP&gt;&gt;&gt;</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># ── Query (run anytime) ───────────────────────────────────────────────────────
</span><span class="n">index</span>  <span class="o">=</span> <span class="nf">load_index</span><span class="p">(</span><span class="n">INDEX_PATH</span><span class="p">)</span>
<span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">CORPUS_PATH</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">corpus</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">().</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="s">&lt;&lt;&lt;CHUNK_SEP&gt;&gt;&gt;</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>

<span class="n">query</span>  <span class="o">=</span> <span class="sh">"</span><span class="s">What are the main challenges in retrieval-augmented generation?</span><span class="sh">"</span>
<span class="n">chunks</span> <span class="o">=</span> <span class="nf">retrieve</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">index</span><span class="p">,</span> <span class="n">corpus</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span>
<span class="n">prompt</span> <span class="o">=</span> <span class="nf">build_prompt</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">chunks</span><span class="p">)</span>
<span class="n">answer</span> <span class="o">=</span> <span class="nf">generate_answer</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="s">Query : </span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Answer: </span><span class="si">{</span><span class="n">answer</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="s">Sources:</span><span class="sh">"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">c</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">chunks</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">  [</span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">] score=</span><span class="si">{</span><span class="n">c</span><span class="p">[</span><span class="sh">'</span><span class="s">score</span><span class="sh">'</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s"> | </span><span class="si">{</span><span class="n">c</span><span class="p">[</span><span class="sh">'</span><span class="s">text</span><span class="sh">'</span><span class="p">][</span><span class="si">:</span><span class="mi">80</span><span class="p">]</span><span class="si">}</span><span class="s">...</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <hr/> <h2 id="sample-output">Sample Output</h2> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Embedding 142 chunks: 100%|████████████████| 3/3 [00:02&lt;00:00]
Index built: 142 vectors, dim=384

Query : What are the main challenges in retrieval-augmented generation?
Answer: The main challenges include retrieval-generation mismatch, multi-hop
        reasoning over multiple documents, managing long contexts, and
        hallucination even when relevant passages are retrieved.

Sources:
  [1] score=0.872 | The core difficulty in RAG is ensuring the generator
                    actually uses the retrieved passages...
  [2] score=0.841 | Multi-hop questions require reasoning over chains of
                    documents, which flat retrieval struggles with...
</code></pre></div></div> <hr/> <h2 id="where-to-go-from-here">Where to Go From Here</h2> <table> <thead> <tr> <th>Enhancement</th> <th>What it gives you</th> </tr> </thead> <tbody> <tr> <td><strong>HyDE</strong> — generate a hypothetical answer, embed it</td> <td>Better retrieval for complex queries</td> </tr> <tr> <td><strong>Re-ranking</strong> with a cross-encoder</td> <td>Higher precision top-k results</td> </tr> <tr> <td><strong>Hybrid search</strong> (BM25 + dense)</td> <td>Handles keyword-heavy queries better</td> </tr> <tr> <td><strong>Self-RAG</strong> — LLM decides when to retrieve</td> <td>Avoids retrieval on questions it already knows</td> </tr> <tr> <td><strong>CTranslate2 / llama.cpp</strong> for generation</td> <td>Edge-deployable offline inference</td> </tr> </tbody> </table> <p>The last row is what my PhD research focuses on — building the full pipeline for offline disaster response, where cloud access is unavailable. A future post will cover that in detail.</p> <hr/> <h2 id="references">References</h2> <ul> <li>Lewis et al. (2020). <em>Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks.</em> NeurIPS 2020.</li> <li>Gao et al. (2023). <em>RAG for Large Language Models: A Survey.</em> arXiv:2312.10997.</li> <li><a href="https://faiss.ai">FAISS documentation</a></li> <li><a href="https://www.sbert.net">sentence-transformers</a></li> </ul>]]></content><author><name></name></author><category term="research"/><category term="tutorial"/><category term="nlp"/><category term="rag"/><category term="python"/><category term="llm"/><category term="tutorial"/><summary type="html"><![CDATA[A hands-on walkthrough of building a Retrieval-Augmented Generation system — from document indexing to grounded LLM responses — using FAISS, sentence-transformers, and a local model.]]></summary></entry><entry><title type="html">Backpropagation from Scratch with NumPy</title><link href="https://mahoretushar.github.io/blog/2026/backpropagation-from-scratch-numpy/" rel="alternate" type="text/html" title="Backpropagation from Scratch with NumPy"/><published>2026-03-25T09:00:00+00:00</published><updated>2026-03-25T09:00:00+00:00</updated><id>https://mahoretushar.github.io/blog/2026/backpropagation-from-scratch-numpy</id><content type="html" xml:base="https://mahoretushar.github.io/blog/2026/backpropagation-from-scratch-numpy/"><![CDATA[<p>Every deep learning framework — PyTorch, TensorFlow, Keras — does one thing at its core: <strong>automatic differentiation</strong>. It computes gradients of the loss with respect to every weight in the network, then adjusts those weights to reduce the loss. This process is called <strong>backpropagation</strong>.</p> <p>Understanding backpropagation at the implementation level — not just the diagram — is the difference between using a neural network and <em>understanding</em> one. This post implements a two-layer network from scratch using only NumPy. Every forward computation is explicit. Every gradient is derived and coded by hand.</p> <p>I use this approach in my Artificial Neural Networks course because students who implement backprop once never forget it.</p> <hr/> <h2 id="the-network">The Network</h2> <p>We will build a network for binary classification:</p> \[\text{Input } \mathbf{x} \in \mathbb{R}^{n} \xrightarrow{W_1, b_1} \text{Hidden } \mathbf{h} \in \mathbb{R}^{d} \xrightarrow{W_2, b_2} \text{Output } \hat{y} \in (0, 1)\] <ul> <li><strong>Hidden layer</strong>: ReLU activation</li> <li><strong>Output layer</strong>: Sigmoid activation</li> <li><strong>Loss</strong>: Binary cross-entropy</li> </ul> <pre><code class="language-mermaid">flowchart LR
    X["Input x\n(n features)"] --&gt; Z1["z₁ = W₁x + b₁"]
    Z1 --&gt; A1["a₁ = ReLU(z₁)\n(hidden layer)"]
    A1 --&gt; Z2["z₂ = W₂a₁ + b₂"]
    Z2 --&gt; A2["ŷ = σ(z₂)\n(output)"]
    A2 --&gt; L["Loss\nBCE(y, ŷ)"]

    style X fill:#e3f2fd
    style L fill:#fce4ec
</code></pre> <hr/> <h2 id="setup">Setup</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="n">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">from</span> <span class="n">sklearn.datasets</span> <span class="kn">import</span> <span class="n">make_moons</span>
<span class="kn">from</span> <span class="n">sklearn.model_selection</span> <span class="kn">import</span> <span class="n">train_test_split</span>

<span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
</code></pre></div></div> <p>No other imports needed.</p> <hr/> <h2 id="activation-functions-and-their-derivatives">Activation Functions and Their Derivatives</h2> <p>Backpropagation requires the derivative of every activation function. We implement each as a pair.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sigmoid</span><span class="p">(</span><span class="n">z</span><span class="p">):</span>
    <span class="k">return</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="p">(</span><span class="mf">1.0</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="nf">exp</span><span class="p">(</span><span class="o">-</span><span class="n">z</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">sigmoid_derivative</span><span class="p">(</span><span class="n">z</span><span class="p">):</span>
    <span class="n">s</span> <span class="o">=</span> <span class="nf">sigmoid</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">s</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">s</span><span class="p">)</span>         <span class="c1"># d/dz σ(z) = σ(z)(1 − σ(z))
</span>
<span class="k">def</span> <span class="nf">relu</span><span class="p">(</span><span class="n">z</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="nf">maximum</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">z</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">relu_derivative</span><span class="p">(</span><span class="n">z</span><span class="p">):</span>
    <span class="nf">return </span><span class="p">(</span><span class="n">z</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>  <span class="c1"># 1 where z &gt; 0, else 0
</span></code></pre></div></div> <hr/> <h2 id="weight-initialisation">Weight Initialisation</h2> <p>Poor initialisation causes vanishing or exploding gradients before training even starts.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">initialise_parameters</span><span class="p">(</span><span class="n">n_input</span><span class="p">,</span> <span class="n">n_hidden</span><span class="p">,</span> <span class="n">n_output</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    He initialisation for ReLU layers:
        W ~ N(0, sqrt(2/n_in))
    Prevents vanishing gradients with ReLU.
    </span><span class="sh">"""</span>
    <span class="n">W1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">randn</span><span class="p">(</span><span class="n">n_hidden</span><span class="p">,</span> <span class="n">n_input</span><span class="p">)</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="mf">2.0</span> <span class="o">/</span> <span class="n">n_input</span><span class="p">)</span>
    <span class="n">b1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">zeros</span><span class="p">((</span><span class="n">n_hidden</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span>
    <span class="n">W2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">randn</span><span class="p">(</span><span class="n">n_output</span><span class="p">,</span> <span class="n">n_hidden</span><span class="p">)</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="mf">2.0</span> <span class="o">/</span> <span class="n">n_hidden</span><span class="p">)</span>
    <span class="n">b2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">zeros</span><span class="p">((</span><span class="n">n_output</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">W1</span><span class="sh">"</span><span class="p">:</span> <span class="n">W1</span><span class="p">,</span> <span class="sh">"</span><span class="s">b1</span><span class="sh">"</span><span class="p">:</span> <span class="n">b1</span><span class="p">,</span> <span class="sh">"</span><span class="s">W2</span><span class="sh">"</span><span class="p">:</span> <span class="n">W2</span><span class="p">,</span> <span class="sh">"</span><span class="s">b2</span><span class="sh">"</span><span class="p">:</span> <span class="n">b2</span><span class="p">}</span>
</code></pre></div></div> <hr/> <h2 id="forward-pass">Forward Pass</h2> <p>The forward pass computes the network’s prediction given current weights. We cache intermediate values — we will need them for the backward pass.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">forward_pass</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">params</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    X : (n_features, n_samples)

    Returns:
        y_hat : predicted probabilities (1, n_samples)
        cache : dict of intermediate values for backprop
    </span><span class="sh">"""</span>
    <span class="n">W1</span><span class="p">,</span> <span class="n">b1</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">W1</span><span class="sh">"</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">b1</span><span class="sh">"</span><span class="p">]</span>
    <span class="n">W2</span><span class="p">,</span> <span class="n">b2</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">W2</span><span class="sh">"</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">b2</span><span class="sh">"</span><span class="p">]</span>

    <span class="n">Z1</span> <span class="o">=</span> <span class="n">W1</span> <span class="o">@</span> <span class="n">X</span> <span class="o">+</span> <span class="n">b1</span>          <span class="c1"># (n_hidden, n_samples)
</span>    <span class="n">A1</span> <span class="o">=</span> <span class="nf">relu</span><span class="p">(</span><span class="n">Z1</span><span class="p">)</span>              <span class="c1"># (n_hidden, n_samples)
</span>
    <span class="n">Z2</span> <span class="o">=</span> <span class="n">W2</span> <span class="o">@</span> <span class="n">A1</span> <span class="o">+</span> <span class="n">b2</span>          <span class="c1"># (1, n_samples)
</span>    <span class="n">A2</span> <span class="o">=</span> <span class="nf">sigmoid</span><span class="p">(</span><span class="n">Z2</span><span class="p">)</span>           <span class="c1"># (1, n_samples) — predicted probabilities
</span>
    <span class="n">cache</span> <span class="o">=</span> <span class="p">{</span><span class="sh">"</span><span class="s">Z1</span><span class="sh">"</span><span class="p">:</span> <span class="n">Z1</span><span class="p">,</span> <span class="sh">"</span><span class="s">A1</span><span class="sh">"</span><span class="p">:</span> <span class="n">A1</span><span class="p">,</span> <span class="sh">"</span><span class="s">Z2</span><span class="sh">"</span><span class="p">:</span> <span class="n">Z2</span><span class="p">,</span> <span class="sh">"</span><span class="s">A2</span><span class="sh">"</span><span class="p">:</span> <span class="n">A2</span><span class="p">,</span> <span class="sh">"</span><span class="s">X</span><span class="sh">"</span><span class="p">:</span> <span class="n">X</span><span class="p">}</span>
    <span class="k">return</span> <span class="n">A2</span><span class="p">,</span> <span class="n">cache</span>
</code></pre></div></div> <hr/> <h2 id="loss-function">Loss Function</h2> <p>Binary cross-entropy penalises confident wrong predictions heavily:</p> \[\mathcal{L} = -\frac{1}{m}\sum_{i=1}^{m}\left[ y^{(i)} \log \hat{y}^{(i)} + (1 - y^{(i)}) \log(1 - \hat{y}^{(i)}) \right]\] <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">compute_loss</span><span class="p">(</span><span class="n">y_hat</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">eps</span><span class="o">=</span><span class="mf">1e-8</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    y_hat : (1, n_samples) predicted probabilities
    y     : (1, n_samples) true binary labels
    eps   : small constant to avoid log(0)
    </span><span class="sh">"""</span>
    <span class="n">m</span> <span class="o">=</span> <span class="n">y</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">loss</span> <span class="o">=</span> <span class="o">-</span><span class="p">(</span><span class="mi">1</span><span class="o">/</span><span class="n">m</span><span class="p">)</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span>
        <span class="n">y</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="n">y_hat</span> <span class="o">+</span> <span class="n">eps</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">y</span><span class="p">)</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">y_hat</span> <span class="o">+</span> <span class="n">eps</span><span class="p">)</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="nf">float</span><span class="p">(</span><span class="n">loss</span><span class="p">)</span>
</code></pre></div></div> <hr/> <h2 id="backward-pass">Backward Pass</h2> <p>This is the core of the post. We compute gradients by applying the <strong>chain rule</strong> layer by layer, starting from the loss and working backwards.</p> <p>The key insight: the gradient of the loss with respect to a weight equals the gradient flowing back through all subsequent layers multiplied by the local derivative.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">backward_pass</span><span class="p">(</span><span class="n">y_hat</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">cache</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">
    Compute gradients of loss w.r.t. all parameters.

    Derivations:
      dL/dA2 = -(y/ŷ) + (1-y)/(1-ŷ)   ← BCE derivative
      dL/dZ2 = A2 - y                    ← combined with sigmoid derivative
      dL/dW2 = dL/dZ2 · A1ᵀ / m
      dL/db2 = mean(dL/dZ2, axis=1)

      dL/dA1 = W2ᵀ · dL/dZ2
      dL/dZ1 = dL/dA1 * ReLU</span><span class="sh">'</span><span class="s">(Z1)
      dL/dW1 = dL/dZ1 · Xᵀ / m
      dL/db1 = mean(dL/dZ1, axis=1)
    </span><span class="sh">"""</span>
    <span class="n">m</span>  <span class="o">=</span> <span class="n">y</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">W2</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">W2</span><span class="sh">"</span><span class="p">]</span>
    <span class="n">Z1</span><span class="p">,</span> <span class="n">A1</span><span class="p">,</span> <span class="n">Z2</span><span class="p">,</span> <span class="n">A2</span><span class="p">,</span> <span class="n">X</span> <span class="o">=</span> <span class="p">(</span>
        <span class="n">cache</span><span class="p">[</span><span class="sh">"</span><span class="s">Z1</span><span class="sh">"</span><span class="p">],</span> <span class="n">cache</span><span class="p">[</span><span class="sh">"</span><span class="s">A1</span><span class="sh">"</span><span class="p">],</span> <span class="n">cache</span><span class="p">[</span><span class="sh">"</span><span class="s">Z2</span><span class="sh">"</span><span class="p">],</span> <span class="n">cache</span><span class="p">[</span><span class="sh">"</span><span class="s">A2</span><span class="sh">"</span><span class="p">],</span> <span class="n">cache</span><span class="p">[</span><span class="sh">"</span><span class="s">X</span><span class="sh">"</span><span class="p">]</span>
    <span class="p">)</span>

    <span class="c1"># ── Output layer ──────────────────────────────────────────────────────────
</span>    <span class="n">dZ2</span> <span class="o">=</span> <span class="n">A2</span> <span class="o">-</span> <span class="n">y</span>                              <span class="c1"># (1, m)  ← sigmoid + BCE simplification
</span>    <span class="n">dW2</span> <span class="o">=</span> <span class="p">(</span><span class="n">dZ2</span> <span class="o">@</span> <span class="n">A1</span><span class="p">.</span><span class="n">T</span><span class="p">)</span> <span class="o">/</span> <span class="n">m</span>                   <span class="c1"># (1, n_hidden)
</span>    <span class="n">db2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">dZ2</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="c1"># (1, 1)
</span>
    <span class="c1"># ── Hidden layer ─────────────────────────────────────────────────────────
</span>    <span class="n">dA1</span> <span class="o">=</span> <span class="n">W2</span><span class="p">.</span><span class="n">T</span> <span class="o">@</span> <span class="n">dZ2</span>                          <span class="c1"># (n_hidden, m)
</span>    <span class="n">dZ1</span> <span class="o">=</span> <span class="n">dA1</span> <span class="o">*</span> <span class="nf">relu_derivative</span><span class="p">(</span><span class="n">Z1</span><span class="p">)</span>           <span class="c1"># element-wise: zero where Z1 ≤ 0
</span>    <span class="n">dW1</span> <span class="o">=</span> <span class="p">(</span><span class="n">dZ1</span> <span class="o">@</span> <span class="n">X</span><span class="p">.</span><span class="n">T</span><span class="p">)</span> <span class="o">/</span> <span class="n">m</span>                    <span class="c1"># (n_hidden, n_input)
</span>    <span class="n">db1</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">dZ1</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdims</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="c1"># (n_hidden, 1)
</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">dW1</span><span class="sh">"</span><span class="p">:</span> <span class="n">dW1</span><span class="p">,</span> <span class="sh">"</span><span class="s">db1</span><span class="sh">"</span><span class="p">:</span> <span class="n">db1</span><span class="p">,</span> <span class="sh">"</span><span class="s">dW2</span><span class="sh">"</span><span class="p">:</span> <span class="n">dW2</span><span class="p">,</span> <span class="sh">"</span><span class="s">db2</span><span class="sh">"</span><span class="p">:</span> <span class="n">db2</span><span class="p">}</span>
</code></pre></div></div> <p>The simplification <code class="language-plaintext highlighter-rouge">dZ2 = A2 - y</code> comes from combining the derivative of BCE loss with the derivative of the sigmoid. It is elegant and numerically stable.</p> <hr/> <h2 id="parameter-update-gradient-descent">Parameter Update (Gradient Descent)</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">update_parameters</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">grads</span><span class="p">,</span> <span class="n">learning_rate</span><span class="p">):</span>
    <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">W1</span><span class="sh">"</span><span class="p">]</span> <span class="o">-=</span> <span class="n">learning_rate</span> <span class="o">*</span> <span class="n">grads</span><span class="p">[</span><span class="sh">"</span><span class="s">dW1</span><span class="sh">"</span><span class="p">]</span>
    <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">b1</span><span class="sh">"</span><span class="p">]</span> <span class="o">-=</span> <span class="n">learning_rate</span> <span class="o">*</span> <span class="n">grads</span><span class="p">[</span><span class="sh">"</span><span class="s">db1</span><span class="sh">"</span><span class="p">]</span>
    <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">W2</span><span class="sh">"</span><span class="p">]</span> <span class="o">-=</span> <span class="n">learning_rate</span> <span class="o">*</span> <span class="n">grads</span><span class="p">[</span><span class="sh">"</span><span class="s">dW2</span><span class="sh">"</span><span class="p">]</span>
    <span class="n">params</span><span class="p">[</span><span class="sh">"</span><span class="s">b2</span><span class="sh">"</span><span class="p">]</span> <span class="o">-=</span> <span class="n">learning_rate</span> <span class="o">*</span> <span class="n">grads</span><span class="p">[</span><span class="sh">"</span><span class="s">db2</span><span class="sh">"</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">params</span>
</code></pre></div></div> <hr/> <h2 id="training-loop">Training Loop</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">train</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">n_hidden</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">lr</span><span class="o">=</span><span class="mf">0.1</span><span class="p">,</span> <span class="n">epochs</span><span class="o">=</span><span class="mi">2000</span><span class="p">,</span> <span class="n">print_every</span><span class="o">=</span><span class="mi">200</span><span class="p">):</span>
    <span class="n">n_input</span>  <span class="o">=</span> <span class="n">X_train</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
    <span class="n">n_output</span> <span class="o">=</span> <span class="mi">1</span>
    <span class="n">params</span>   <span class="o">=</span> <span class="nf">initialise_parameters</span><span class="p">(</span><span class="n">n_input</span><span class="p">,</span> <span class="n">n_hidden</span><span class="p">,</span> <span class="n">n_output</span><span class="p">)</span>
    <span class="n">losses</span>   <span class="o">=</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">epoch</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">epochs</span><span class="p">):</span>
        <span class="c1"># Forward
</span>        <span class="n">y_hat</span><span class="p">,</span> <span class="n">cache</span> <span class="o">=</span> <span class="nf">forward_pass</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span>
        <span class="c1"># Loss
</span>        <span class="n">loss</span> <span class="o">=</span> <span class="nf">compute_loss</span><span class="p">(</span><span class="n">y_hat</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
        <span class="n">losses</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">loss</span><span class="p">)</span>
        <span class="c1"># Backward
</span>        <span class="n">grads</span>  <span class="o">=</span> <span class="nf">backward_pass</span><span class="p">(</span><span class="n">y_hat</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">cache</span><span class="p">)</span>
        <span class="c1"># Update
</span>        <span class="n">params</span> <span class="o">=</span> <span class="nf">update_parameters</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">grads</span><span class="p">,</span> <span class="n">lr</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">epoch</span> <span class="o">%</span> <span class="n">print_every</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">preds</span>    <span class="o">=</span> <span class="p">(</span><span class="n">y_hat</span> <span class="o">&gt;</span> <span class="mf">0.5</span><span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
            <span class="n">accuracy</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">preds</span> <span class="o">==</span> <span class="n">y_train</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span>
            <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Epoch </span><span class="si">{</span><span class="n">epoch</span><span class="si">:</span><span class="mi">4</span><span class="n">d</span><span class="si">}</span><span class="s"> | Loss: </span><span class="si">{</span><span class="n">loss</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s"> | Accuracy: </span><span class="si">{</span><span class="n">accuracy</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">%</span><span class="sh">"</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">params</span><span class="p">,</span> <span class="n">losses</span>
</code></pre></div></div> <hr/> <h2 id="run-it">Run It</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ── Dataset ───────────────────────────────────────────────────────────────────
</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="nf">make_moons</span><span class="p">(</span><span class="n">n_samples</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span> <span class="n">noise</span><span class="o">=</span><span class="mf">0.2</span><span class="p">)</span>
<span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> <span class="nf">train_test_split</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">test_size</span><span class="o">=</span><span class="mf">0.2</span><span class="p">)</span>

<span class="c1"># Reshape for our (features, samples) convention
</span><span class="n">X_train</span> <span class="o">=</span> <span class="n">X_train</span><span class="p">.</span><span class="n">T</span>                          <span class="c1"># (2, 800)
</span><span class="n">X_test</span>  <span class="o">=</span> <span class="n">X_test</span><span class="p">.</span><span class="n">T</span>                           <span class="c1"># (2, 200)
</span><span class="n">y_train</span> <span class="o">=</span> <span class="n">y_train</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>             <span class="c1"># (1, 800)
</span><span class="n">y_test</span>  <span class="o">=</span> <span class="n">y_test</span><span class="p">.</span><span class="nf">reshape</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>              <span class="c1"># (1, 200)
</span>
<span class="c1"># ── Train ─────────────────────────────────────────────────────────────────────
</span><span class="n">params</span><span class="p">,</span> <span class="n">losses</span> <span class="o">=</span> <span class="nf">train</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">n_hidden</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">lr</span><span class="o">=</span><span class="mf">0.1</span><span class="p">,</span> <span class="n">epochs</span><span class="o">=</span><span class="mi">3000</span><span class="p">)</span>

<span class="c1"># ── Evaluate ──────────────────────────────────────────────────────────────────
</span><span class="n">y_hat_test</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="nf">forward_pass</span><span class="p">(</span><span class="n">X_test</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span>
<span class="n">test_preds</span>     <span class="o">=</span> <span class="p">(</span><span class="n">y_hat_test</span> <span class="o">&gt;</span> <span class="mf">0.5</span><span class="p">).</span><span class="nf">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
<span class="n">test_acc</span>       <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">mean</span><span class="p">(</span><span class="n">test_preds</span> <span class="o">==</span> <span class="n">y_test</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="s">Test Accuracy: </span><span class="si">{</span><span class="n">test_acc</span><span class="si">:</span><span class="p">.</span><span class="mi">1</span><span class="n">f</span><span class="si">}</span><span class="s">%</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># ── Plot loss curve ───────────────────────────────────────────────────────────
</span><span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">4</span><span class="p">))</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">plot</span><span class="p">(</span><span class="n">losses</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="sh">"</span><span class="s">#2196F3</span><span class="sh">"</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">1.5</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Epoch</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Binary Cross-Entropy Loss</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Training Loss — 2-Layer Neural Network (NumPy)</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">grid</span><span class="p">(</span><span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sh">"</span><span class="s">loss_curve.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div> <hr/> <h2 id="expected-output">Expected Output</h2> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Epoch    0 | Loss: 0.7041 | Accuracy: 49.5%
Epoch  200 | Loss: 0.4823 | Accuracy: 78.2%
Epoch  400 | Loss: 0.3156 | Accuracy: 87.6%
Epoch  600 | Loss: 0.2241 | Accuracy: 91.3%
Epoch  800 | Loss: 0.1874 | Accuracy: 93.0%
Epoch 1000 | Loss: 0.1693 | Accuracy: 93.8%
Epoch 2000 | Loss: 0.1421 | Accuracy: 95.1%

Test Accuracy: 94.5%
</code></pre></div></div> <hr/> <h2 id="what-you-have-built">What You Have Built</h2> <table> <thead> <tr> <th>Component</th> <th>Implementation</th> </tr> </thead> <tbody> <tr> <td>Forward pass</td> <td>Matrix multiply → ReLU → Matrix multiply → Sigmoid</td> </tr> <tr> <td>Loss</td> <td>Binary cross-entropy</td> </tr> <tr> <td>Backward pass</td> <td>Chain rule, layer by layer</td> </tr> <tr> <td>Update</td> <td>Gradient descent</td> </tr> </tbody> </table> <p>No autograd. No framework. Every gradient derived from first principles.</p> <hr/> <h2 id="common-mistakes-to-watch-for">Common Mistakes to Watch For</h2> <p><strong>Shapes.</strong> NumPy will broadcast silently. Print shapes at every step until you trust them.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Add this while debugging:
</span><span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Z1: </span><span class="si">{</span><span class="n">Z1</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="s">, A1: </span><span class="si">{</span><span class="n">A1</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="s">, Z2: </span><span class="si">{</span><span class="n">Z2</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <p><strong>Learning rate too high.</strong> Loss explodes or oscillates. Start at 0.01 and increase.</p> <p><strong>Not caching intermediates.</strong> The backward pass needs Z1, A1, Z2, A2. Cache them in the forward pass.</p> <hr/> <h2 id="exercises">Exercises</h2> <ol> <li>Replace ReLU with tanh. Derive and implement its gradient.</li> <li>Add a third hidden layer. Generalise the forward and backward pass to <code class="language-plaintext highlighter-rouge">L</code> layers.</li> <li>Replace vanilla gradient descent with <strong>momentum</strong> or <strong>Adam</strong>.</li> <li>Plot the decision boundary over the moon dataset.</li> </ol> <hr/> <h2 id="references">References</h2> <ul> <li>Rumelhart, Hinton &amp; Williams (1986). <em>Learning representations by back-propagating errors.</em> Nature.</li> <li>Nielsen, M. (2015). <a href="http://neuralnetworksanddeeplearning.com">Neural Networks and Deep Learning</a> — Chapter 2.</li> <li>Goodfellow, Bengio &amp; Courville (2016). <em>Deep Learning.</em> MIT Press.</li> </ul>]]></content><author><name></name></author><category term="teaching"/><category term="tutorial"/><category term="neural-networks"/><category term="deep-learning"/><category term="python"/><category term="numpy"/><category term="tutorial"/><summary type="html"><![CDATA[Implement a two-layer neural network with forward pass, loss computation, and backpropagation using only NumPy. No frameworks — every gradient derived and coded explicitly.]]></summary></entry><entry><title type="html">Text Classification for Cyberbullying Detection: A Practical NLP Walkthrough</title><link href="https://mahoretushar.github.io/blog/2026/cyberbullying-detection-nlp-walkthrough/" rel="alternate" type="text/html" title="Text Classification for Cyberbullying Detection: A Practical NLP Walkthrough"/><published>2026-03-10T09:00:00+00:00</published><updated>2026-03-10T09:00:00+00:00</updated><id>https://mahoretushar.github.io/blog/2026/cyberbullying-detection-nlp-walkthrough</id><content type="html" xml:base="https://mahoretushar.github.io/blog/2026/cyberbullying-detection-nlp-walkthrough/"><![CDATA[<p>Cyberbullying detection is one of the applied NLP problems that looks deceptively simple and turns out to be genuinely hard. The text is short, informal, and context-dependent. The same word that signals aggression in one sentence is harmless banter in another. And the class imbalance — most posts are not bullying — creates classifiers that learn to say “not bullying” for everything.</p> <p>This post walks through the machine learning pipeline I used in our IEEE ACROSET 2024 paper on multi-class cyberbullying detection. The methodology is reproducible, the code is complete, and the design choices are explained.</p> <hr/> <h2 id="the-problem">The Problem</h2> <p>Given a short social media post, classify it into one of:</p> <table> <thead> <tr> <th>Label</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">not_bullying</code></td> <td>No harmful content</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">gender</code></td> <td>Gender-based harassment</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">religion</code></td> <td>Religious targeting</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">age</code></td> <td>Age-based targeting</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">ethnicity</code></td> <td>Ethnic harassment</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">other_cyberbullying</code></td> <td>Harmful but doesn’t fit above</td> </tr> </tbody> </table> <p>This is a <strong>multi-class</strong> classification problem, not just binary. That distinction affects feature engineering, model selection, and how we interpret metrics.</p> <hr/> <h2 id="dataset">Dataset</h2> <p>We use the <a href="https://www.kaggle.com/datasets/andrewmvd/cyberbullying-classification">Cyberbullying Classification dataset</a> from Kaggle — 47,000 tweets across 6 classes.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="nf">read_csv</span><span class="p">(</span><span class="sh">"</span><span class="s">cyberbullying_tweets.csv</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="sh">"</span><span class="s">cyberbullying_type</span><span class="sh">"</span><span class="p">].</span><span class="nf">value_counts</span><span class="p">())</span>
</code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>not_cyberbullying       8000
gender                  8000
religion                7998
age                     7992
ethnicity               7961
other_cyberbullying     7823
</code></pre></div></div> <p>Roughly balanced — which is unusual in real-world moderation datasets, where harmful content is rare. Keep this in mind when deploying.</p> <hr/> <h2 id="pipeline-architecture">Pipeline Architecture</h2> <pre><code class="language-mermaid">flowchart LR
    Raw["Raw Tweet Text"] --&gt; Clean["Text Cleaning\n(lowercase, URLs, mentions)"]
    Clean --&gt; Token["Tokenisation\n+ Stopword Removal"]
    Token --&gt; Stem["Stemming / Lemmatisation"]
    Stem --&gt; Features["TF-IDF\nVectorisation"]
    Features --&gt; Model["SVM / Random Forest"]
    Model --&gt; Pred["Predicted Label"]

    style Raw fill:#e3f2fd
    style Pred fill:#e8f5e9
</code></pre> <hr/> <h2 id="setup">Setup</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">re</span>
<span class="kn">import</span> <span class="n">string</span>
<span class="kn">import</span> <span class="n">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="n">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">import</span> <span class="n">seaborn</span> <span class="k">as</span> <span class="n">sns</span>

<span class="kn">from</span> <span class="n">sklearn.model_selection</span> <span class="kn">import</span> <span class="n">train_test_split</span><span class="p">,</span> <span class="n">cross_val_score</span><span class="p">,</span> <span class="n">StratifiedKFold</span>
<span class="kn">from</span> <span class="n">sklearn.feature_extraction.text</span> <span class="kn">import</span> <span class="n">TfidfVectorizer</span>
<span class="kn">from</span> <span class="n">sklearn.svm</span> <span class="kn">import</span> <span class="n">LinearSVC</span>
<span class="kn">from</span> <span class="n">sklearn.ensemble</span> <span class="kn">import</span> <span class="n">RandomForestClassifier</span>
<span class="kn">from</span> <span class="n">sklearn.pipeline</span> <span class="kn">import</span> <span class="n">Pipeline</span>
<span class="kn">from</span> <span class="n">sklearn.metrics</span> <span class="kn">import</span> <span class="p">(</span>
    <span class="n">classification_report</span><span class="p">,</span> <span class="n">confusion_matrix</span><span class="p">,</span>
    <span class="n">ConfusionMatrixDisplay</span><span class="p">,</span> <span class="n">f1_score</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="n">sklearn.preprocessing</span> <span class="kn">import</span> <span class="n">LabelEncoder</span>

<span class="kn">import</span> <span class="n">nltk</span>
<span class="kn">from</span> <span class="n">nltk.corpus</span> <span class="kn">import</span> <span class="n">stopwords</span>
<span class="kn">from</span> <span class="n">nltk.stem</span> <span class="kn">import</span> <span class="n">WordNetLemmatizer</span>

<span class="n">nltk</span><span class="p">.</span><span class="nf">download</span><span class="p">(</span><span class="sh">"</span><span class="s">stopwords</span><span class="sh">"</span><span class="p">,</span> <span class="n">quiet</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">nltk</span><span class="p">.</span><span class="nf">download</span><span class="p">(</span><span class="sh">"</span><span class="s">wordnet</span><span class="sh">"</span><span class="p">,</span>   <span class="n">quiet</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="n">SEED</span> <span class="o">=</span> <span class="mi">42</span>
<span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="n">SEED</span><span class="p">)</span>
</code></pre></div></div> <hr/> <h2 id="step-1--text-preprocessing">Step 1 — Text Preprocessing</h2> <p>Social media text is noisy. A tweet like <code class="language-plaintext highlighter-rouge">@user ur so ugly wtf 😡 check http://t.co/xyz</code> contains a mention, a URL, informal spelling, and an emoji — none of which help a bag-of-words model.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">lemmatizer</span>  <span class="o">=</span> <span class="nc">WordNetLemmatizer</span><span class="p">()</span>
<span class="n">STOP_WORDS</span>  <span class="o">=</span> <span class="nf">set</span><span class="p">(</span><span class="n">stopwords</span><span class="p">.</span><span class="nf">words</span><span class="p">(</span><span class="sh">"</span><span class="s">english</span><span class="sh">"</span><span class="p">))</span>

<span class="c1"># Keep negations — "not" flips sentiment
</span><span class="n">STOP_WORDS</span> <span class="o">-=</span> <span class="p">{</span><span class="sh">"</span><span class="s">no</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">not</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">nor</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">never</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">neither</span><span class="sh">"</span><span class="p">}</span>

<span class="k">def</span> <span class="nf">clean_tweet</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Full preprocessing pipeline for a single tweet.
    Returns a cleaned, lemmatised string.
    </span><span class="sh">"""</span>
    <span class="n">text</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="nf">lower</span><span class="p">()</span>
    <span class="n">text</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">http\S+|www\S+</span><span class="sh">"</span><span class="p">,</span>      <span class="sh">""</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>   <span class="c1"># remove URLs
</span>    <span class="n">text</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">@\w+</span><span class="sh">"</span><span class="p">,</span>                <span class="sh">""</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>   <span class="c1"># remove @mentions
</span>    <span class="n">text</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">#(\w+)</span><span class="sh">"</span><span class="p">,</span>          <span class="sa">r</span><span class="sh">"</span><span class="s">\1</span><span class="sh">"</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>   <span class="c1"># keep hashtag text, drop #
</span>    <span class="n">text</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">[^a-z\s]</span><span class="sh">"</span><span class="p">,</span>            <span class="sh">""</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>   <span class="c1"># remove punctuation + digits
</span>    <span class="n">text</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">\s+</span><span class="sh">"</span><span class="p">,</span>               <span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">,</span> <span class="n">text</span><span class="p">).</span><span class="nf">strip</span><span class="p">()</span>

    <span class="n">tokens</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="nf">split</span><span class="p">()</span>
    <span class="n">tokens</span> <span class="o">=</span> <span class="p">[</span><span class="n">lemmatizer</span><span class="p">.</span><span class="nf">lemmatize</span><span class="p">(</span><span class="n">t</span><span class="p">)</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">tokens</span> <span class="k">if</span> <span class="n">t</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">STOP_WORDS</span><span class="p">]</span>
    <span class="k">return</span> <span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">tokens</span><span class="p">)</span>


<span class="c1"># Apply to full dataset
</span><span class="n">df</span><span class="p">[</span><span class="sh">"</span><span class="s">clean_text</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="sh">"</span><span class="s">tweet_text</span><span class="sh">"</span><span class="p">].</span><span class="nf">apply</span><span class="p">(</span><span class="n">clean_tweet</span><span class="p">)</span>

<span class="c1"># Quick sanity check
</span><span class="nf">print</span><span class="p">(</span><span class="n">df</span><span class="p">[[</span><span class="sh">"</span><span class="s">tweet_text</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">clean_text</span><span class="sh">"</span><span class="p">]].</span><span class="nf">head</span><span class="p">(</span><span class="mi">3</span><span class="p">).</span><span class="nf">to_string</span><span class="p">())</span>
</code></pre></div></div> <p><strong>Design choices explained:</strong></p> <ul> <li><strong>Lemmatisation over stemming.</strong> <code class="language-plaintext highlighter-rouge">running → run</code> (not <code class="language-plaintext highlighter-rouge">runn</code>). Stems are not real words; lemmas are — which helps interpretability.</li> <li><strong>Keeping negations.</strong> Removing “not” from “not bullying” destroys the label’s meaning.</li> <li><strong>Hashtag text retained.</strong> <code class="language-plaintext highlighter-rouge">#stopbullying</code> → <code class="language-plaintext highlighter-rouge">stopbullying</code> still carries signal.</li> </ul> <hr/> <h2 id="step-2--train--test-split">Step 2 — Train / Test Split</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">X</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="sh">"</span><span class="s">clean_text</span><span class="sh">"</span><span class="p">]</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="sh">"</span><span class="s">cyberbullying_type</span><span class="sh">"</span><span class="p">]</span>

<span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> <span class="nf">train_test_split</span><span class="p">(</span>
    <span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span>
    <span class="n">test_size</span><span class="o">=</span><span class="mf">0.2</span><span class="p">,</span>
    <span class="n">random_state</span><span class="o">=</span><span class="n">SEED</span><span class="p">,</span>
    <span class="n">stratify</span><span class="o">=</span><span class="n">y</span>          <span class="c1"># preserve class distribution in both splits
</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Train: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">X_train</span><span class="p">)</span><span class="si">}</span><span class="s">, Test: </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">X_test</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <p>Always stratify when splitting multi-class data. Without it, random splits can under-represent minority classes in either set.</p> <hr/> <h2 id="step-3--tf-idf-feature-extraction">Step 3 — TF-IDF Feature Extraction</h2> <p>TF-IDF (Term Frequency–Inverse Document Frequency) converts each tweet into a sparse numeric vector. Words that are common everywhere (low IDF) get downweighted; words distinctive to a document get upweighted.</p> \[\text{TF-IDF}(t, d) = \underbrace{\frac{f_{t,d}}{\sum_k f_{k,d}}}_{\text{term frequency}} \times \underbrace{\log \frac{N}{|\{d: t \in d\}|}}_{\text{inverse document frequency}}\] <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tfidf</span> <span class="o">=</span> <span class="nc">TfidfVectorizer</span><span class="p">(</span>
    <span class="n">max_features</span><span class="o">=</span><span class="mi">30_000</span><span class="p">,</span>    <span class="c1"># vocabulary cap — filters rare/noisy tokens
</span>    <span class="n">ngram_range</span><span class="o">=</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>     <span class="c1"># unigrams + bigrams: "not good" ≠ "not" + "good"
</span>    <span class="n">sublinear_tf</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>      <span class="c1"># replace tf with 1 + log(tf) — dampens high counts
</span>    <span class="n">min_df</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>               <span class="c1"># ignore tokens appearing in &lt; 3 documents
</span>    <span class="n">max_df</span><span class="o">=</span><span class="mf">0.95</span>             <span class="c1"># ignore tokens in &gt; 95% of documents (too common)
</span><span class="p">)</span>

<span class="n">X_train_tfidf</span> <span class="o">=</span> <span class="n">tfidf</span><span class="p">.</span><span class="nf">fit_transform</span><span class="p">(</span><span class="n">X_train</span><span class="p">)</span>
<span class="n">X_test_tfidf</span>  <span class="o">=</span> <span class="n">tfidf</span><span class="p">.</span><span class="nf">transform</span><span class="p">(</span><span class="n">X_test</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Feature matrix: </span><span class="si">{</span><span class="n">X_train_tfidf</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>  <span class="c1"># (37600, 30000)
</span></code></pre></div></div> <p><strong>Why bigrams?</strong> Phrases like “not racist” or “just joking” carry meaning that unigrams alone miss.</p> <hr/> <h2 id="step-4--model-training">Step 4 — Model Training</h2> <h3 id="linear-svm">Linear SVM</h3> <p>SVMs with a linear kernel work exceptionally well on high-dimensional sparse text data. They find the maximum-margin hyperplane separating classes in TF-IDF space.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">svm</span> <span class="o">=</span> <span class="nc">LinearSVC</span><span class="p">(</span>
    <span class="n">C</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span>          <span class="c1"># regularisation — lower C = stronger regularisation
</span>    <span class="n">max_iter</span><span class="o">=</span><span class="mi">2000</span><span class="p">,</span>
    <span class="n">random_state</span><span class="o">=</span><span class="n">SEED</span>
<span class="p">)</span>
<span class="n">svm</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span><span class="n">X_train_tfidf</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
</code></pre></div></div> <h3 id="random-forest">Random Forest</h3> <p>Random Forest provides a complementary approach: it builds an ensemble of decision trees, each trained on a bootstrap sample with random feature subsets.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rf</span> <span class="o">=</span> <span class="nc">RandomForestClassifier</span><span class="p">(</span>
    <span class="n">n_estimators</span><span class="o">=</span><span class="mi">200</span><span class="p">,</span>
    <span class="n">max_depth</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
    <span class="n">min_samples_leaf</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
    <span class="n">n_jobs</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span>
    <span class="n">random_state</span><span class="o">=</span><span class="n">SEED</span>
<span class="p">)</span>
<span class="n">rf</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span><span class="n">X_train_tfidf</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>
</code></pre></div></div> <hr/> <h2 id="step-5--evaluation">Step 5 — Evaluation</h2> <h3 id="classification-report">Classification Report</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">evaluate_model</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="sh">"</span><span class="s">Model</span><span class="sh">"</span><span class="p">):</span>
    <span class="n">y_pred</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="si">{</span><span class="sh">'</span><span class="s">=</span><span class="sh">'</span><span class="o">*</span><span class="mi">60</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s"> </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="sh">'</span><span class="s">=</span><span class="sh">'</span><span class="o">*</span><span class="mi">60</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="nf">classification_report</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">,</span> <span class="n">digits</span><span class="o">=</span><span class="mi">3</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">y_pred</span>

<span class="n">svm_preds</span> <span class="o">=</span> <span class="nf">evaluate_model</span><span class="p">(</span><span class="n">svm</span><span class="p">,</span> <span class="n">X_test_tfidf</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="sh">"</span><span class="s">Linear SVM</span><span class="sh">"</span><span class="p">)</span>
<span class="n">rf_preds</span>  <span class="o">=</span> <span class="nf">evaluate_model</span><span class="p">(</span><span class="n">rf</span><span class="p">,</span>  <span class="n">X_test_tfidf</span><span class="p">,</span> <span class="n">y_test</span><span class="p">,</span> <span class="sh">"</span><span class="s">Random Forest</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>============================================================
 Linear SVM
============================================================
                      precision    recall  f1-score   support

                 age      0.912     0.905     0.908      1599
           ethnicity      0.887     0.891     0.889      1594
              gender      0.933     0.940     0.936      1598
    not_cyberbullying      0.958     0.951     0.955      1600
  other_cyberbullying      0.821     0.819     0.820      1565
            religion      0.921     0.932     0.927      1604

           accuracy                          0.906      9560
          macro avg      0.905     0.906     0.906      9560
       weighted avg      0.906     0.906     0.906      9560
</code></pre></div></div> <h3 id="confusion-matrix">Confusion Matrix</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">plot_confusion_matrix</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">labels</span><span class="p">):</span>
    <span class="n">cm</span> <span class="o">=</span> <span class="nf">confusion_matrix</span><span class="p">(</span><span class="n">y_true</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">,</span> <span class="n">labels</span><span class="o">=</span><span class="n">labels</span><span class="p">,</span> <span class="n">normalize</span><span class="o">=</span><span class="sh">"</span><span class="s">true</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="nf">subplots</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">6</span><span class="p">))</span>
    <span class="n">disp</span> <span class="o">=</span> <span class="nc">ConfusionMatrixDisplay</span><span class="p">(</span><span class="n">confusion_matrix</span><span class="o">=</span><span class="n">cm</span><span class="p">,</span> <span class="n">display_labels</span><span class="o">=</span><span class="n">labels</span><span class="p">)</span>
    <span class="n">disp</span><span class="p">.</span><span class="nf">plot</span><span class="p">(</span><span class="n">ax</span><span class="o">=</span><span class="n">ax</span><span class="p">,</span> <span class="n">cmap</span><span class="o">=</span><span class="sh">"</span><span class="s">Blues</span><span class="sh">"</span><span class="p">,</span> <span class="n">colorbar</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">xticks_rotation</span><span class="o">=</span><span class="mi">45</span><span class="p">)</span>
    <span class="n">ax</span><span class="p">.</span><span class="nf">set_title</span><span class="p">(</span><span class="n">title</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">cm_</span><span class="si">{</span><span class="n">title</span><span class="p">.</span><span class="nf">lower</span><span class="p">().</span><span class="nf">replace</span><span class="p">(</span><span class="sh">'</span><span class="s"> </span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">_</span><span class="sh">'</span><span class="p">)</span><span class="si">}</span><span class="s">.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>

<span class="n">LABELS</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">y_test</span><span class="p">.</span><span class="nf">unique</span><span class="p">())</span>
<span class="nf">plot_confusion_matrix</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">svm_preds</span><span class="p">,</span> <span class="sh">"</span><span class="s">Linear SVM</span><span class="sh">"</span><span class="p">,</span> <span class="n">LABELS</span><span class="p">)</span>
</code></pre></div></div> <p>The normalised confusion matrix shows where the model confuses classes. Common confusions: <code class="language-plaintext highlighter-rouge">other_cyberbullying</code> ↔ <code class="language-plaintext highlighter-rouge">ethnicity</code> and <code class="language-plaintext highlighter-rouge">other_cyberbullying</code> ↔ <code class="language-plaintext highlighter-rouge">age</code> — both understandable given overlapping language.</p> <hr/> <h2 id="step-6--cross-validation">Step 6 — Cross-Validation</h2> <p>A single train/test split can be lucky or unlucky. Stratified k-fold gives a more reliable estimate.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Re-fit on raw features using a Pipeline (fits on each fold properly)
</span><span class="n">cv_pipeline</span> <span class="o">=</span> <span class="nc">Pipeline</span><span class="p">([</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">tfidf</span><span class="sh">"</span><span class="p">,</span> <span class="nc">TfidfVectorizer</span><span class="p">(</span><span class="n">max_features</span><span class="o">=</span><span class="mi">30_000</span><span class="p">,</span> <span class="n">ngram_range</span><span class="o">=</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">),</span>
                               <span class="n">sublinear_tf</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">min_df</span><span class="o">=</span><span class="mi">3</span><span class="p">)),</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">clf</span><span class="sh">"</span><span class="p">,</span>   <span class="nc">LinearSVC</span><span class="p">(</span><span class="n">C</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">max_iter</span><span class="o">=</span><span class="mi">2000</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="n">SEED</span><span class="p">))</span>
<span class="p">])</span>

<span class="n">skf</span>    <span class="o">=</span> <span class="nc">StratifiedKFold</span><span class="p">(</span><span class="n">n_splits</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">shuffle</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="n">SEED</span><span class="p">)</span>
<span class="n">scores</span> <span class="o">=</span> <span class="nf">cross_val_score</span><span class="p">(</span><span class="n">cv_pipeline</span><span class="p">,</span> <span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">cv</span><span class="o">=</span><span class="n">skf</span><span class="p">,</span> <span class="n">scoring</span><span class="o">=</span><span class="sh">"</span><span class="s">f1_macro</span><span class="sh">"</span><span class="p">,</span> <span class="n">n_jobs</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">5-Fold CV F1 (macro): </span><span class="si">{</span><span class="n">scores</span><span class="p">.</span><span class="nf">mean</span><span class="p">()</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s"> ± </span><span class="si">{</span><span class="n">scores</span><span class="p">.</span><span class="nf">std</span><span class="p">()</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="c1"># → 5-Fold CV F1 (macro): 0.901 ± 0.004
</span></code></pre></div></div> <p><strong>Why macro F1?</strong> It gives equal weight to each class regardless of size. For harm detection, we care about rare categories as much as common ones.</p> <hr/> <h2 id="step-7--full-sklearn-pipeline">Step 7 — Full sklearn Pipeline</h2> <p>Package preprocessing and modelling into a single <code class="language-plaintext highlighter-rouge">Pipeline</code> object. This makes deployment and serialisation clean.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">joblib</span>

<span class="n">full_pipeline</span> <span class="o">=</span> <span class="nc">Pipeline</span><span class="p">([</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">tfidf</span><span class="sh">"</span><span class="p">,</span> <span class="nc">TfidfVectorizer</span><span class="p">(</span>
        <span class="n">max_features</span><span class="o">=</span><span class="mi">30_000</span><span class="p">,</span>
        <span class="n">ngram_range</span><span class="o">=</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>
        <span class="n">sublinear_tf</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">min_df</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>
        <span class="n">max_df</span><span class="o">=</span><span class="mf">0.95</span>
    <span class="p">)),</span>
    <span class="p">(</span><span class="sh">"</span><span class="s">clf</span><span class="sh">"</span><span class="p">,</span> <span class="nc">LinearSVC</span><span class="p">(</span><span class="n">C</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">max_iter</span><span class="o">=</span><span class="mi">2000</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="n">SEED</span><span class="p">))</span>
<span class="p">])</span>

<span class="n">full_pipeline</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>

<span class="c1"># Save
</span><span class="n">joblib</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">full_pipeline</span><span class="p">,</span> <span class="sh">"</span><span class="s">cyberbullying_pipeline.pkl</span><span class="sh">"</span><span class="p">)</span>

<span class="c1"># Reload and predict
</span><span class="n">loaded</span> <span class="o">=</span> <span class="n">joblib</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="sh">"</span><span class="s">cyberbullying_pipeline.pkl</span><span class="sh">"</span><span class="p">)</span>

<span class="n">test_inputs</span> <span class="o">=</span> <span class="p">[</span>
    <span class="sh">"</span><span class="s">You are so stupid and ugly, nobody likes you</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">Great game everyone, well played!</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">Go back to your country you don</span><span class="sh">'</span><span class="s">t belong here</span><span class="sh">"</span>
<span class="p">]</span>

<span class="k">for</span> <span class="n">text</span> <span class="ow">in</span> <span class="n">test_inputs</span><span class="p">:</span>
    <span class="n">clean</span> <span class="o">=</span> <span class="nf">clean_tweet</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
    <span class="n">pred</span>  <span class="o">=</span> <span class="n">loaded</span><span class="p">.</span><span class="nf">predict</span><span class="p">([</span><span class="n">clean</span><span class="p">])[</span><span class="mi">0</span><span class="p">]</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">  [</span><span class="si">{</span><span class="n">pred</span><span class="si">:</span><span class="o">&gt;</span><span class="mi">20</span><span class="si">}</span><span class="s">]  </span><span class="si">{</span><span class="n">text</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  [  other_cyberbullying]  You are so stupid and ugly, nobody likes you
  [     not_cyberbullying]  Great game everyone, well played!
  [           ethnicity]  Go back to your country you don't belong here
</code></pre></div></div> <hr/> <h2 id="key-findings">Key Findings</h2> <table> <thead> <tr> <th>Model</th> <th>Accuracy</th> <th>Macro F1</th> <th>Notes</th> </tr> </thead> <tbody> <tr> <td>Linear SVM</td> <td><strong>90.6%</strong></td> <td><strong>0.906</strong></td> <td>Faster, better on rare classes</td> </tr> <tr> <td>Random Forest</td> <td>86.2%</td> <td>0.861</td> <td>Slower, worse on short texts</td> </tr> <tr> <td>Majority baseline</td> <td>16.7%</td> <td>—</td> <td>Always predicts most common class</td> </tr> </tbody> </table> <p>The SVM outperforms Random Forest on this task — consistent with text classification literature. Sparse high-dimensional data is where linear SVMs shine.</p> <hr/> <h2 id="limitations-and-whats-next">Limitations and What’s Next</h2> <p><strong>What this model does not handle:</strong></p> <ul> <li><strong>Context and sarcasm.</strong> “Great, another racist joke” is sarcastic — the model may miss it.</li> <li><strong>Code-switching.</strong> Multilingual harassment (Hinglish, etc.) is out of vocabulary.</li> <li><strong>Class imbalance in deployment.</strong> Real feeds are not balanced 1:1. Calibrate decision thresholds.</li> </ul> <p><strong>Next steps explored in the paper:</strong></p> <ul> <li>BERT fine-tuning for context-aware classification</li> <li>Ensemble of TF-IDF + BERT embeddings</li> <li>Active learning for low-resource annotation</li> </ul> <hr/> <h2 id="exercises">Exercises</h2> <ol> <li>Replace <code class="language-plaintext highlighter-rouge">LinearSVC</code> with <code class="language-plaintext highlighter-rouge">LogisticRegression</code>. Compare performance and speed.</li> <li>Add character n-grams to the <code class="language-plaintext highlighter-rouge">TfidfVectorizer</code> (<code class="language-plaintext highlighter-rouge">analyzer='char_wb'</code>). Does it help on misspelt slurs?</li> <li>Use SMOTE to oversample rare classes and re-evaluate macro F1.</li> <li>Fine-tune <code class="language-plaintext highlighter-rouge">bert-base-uncased</code> on the same dataset using HuggingFace <code class="language-plaintext highlighter-rouge">Trainer</code>.</li> </ol> <hr/> <h2 id="references">References</h2> <ul> <li>Mankar, A., Mahore, T., et al. (2024). <em>Multi-class Cyberbullying Detection using NLP.</em> IEEE ACROSET 2024.</li> <li>Joachims, T. (1998). <em>Text categorization with support vector machines.</em> ECML.</li> <li>Bird, S., Klein, E., &amp; Loper, E. (2009). <em>Natural Language Processing with Python.</em> O’Reilly.</li> <li><a href="https://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction">scikit-learn: Text Feature Extraction</a></li> </ul>]]></content><author><name></name></author><category term="research"/><category term="tutorial"/><category term="nlp"/><category term="text-classification"/><category term="sklearn"/><category term="cyberbullying"/><category term="python"/><category term="tutorial"/><summary type="html"><![CDATA[Build a cyberbullying detection classifier from raw text to evaluation — preprocessing, TF-IDF features, SVM and Random Forest, cross-validation, and a full sklearn pipeline. Based on real research published at IEEE ACROSET 2024.]]></summary></entry><entry><title type="html">Explainable AI in Healthcare: TabTransformer + SHAP for IoMT Security</title><link href="https://mahoretushar.github.io/blog/2026/explainable-ai-healthcare-tabtransformer-shap/" rel="alternate" type="text/html" title="Explainable AI in Healthcare: TabTransformer + SHAP for IoMT Security"/><published>2026-02-20T09:00:00+00:00</published><updated>2026-02-20T09:00:00+00:00</updated><id>https://mahoretushar.github.io/blog/2026/explainable-ai-healthcare-tabtransformer-shap</id><content type="html" xml:base="https://mahoretushar.github.io/blog/2026/explainable-ai-healthcare-tabtransformer-shap/"><![CDATA[<p>When a neural network flags a network packet as a cyberattack on a hospital’s IoMT infrastructure, the security engineer needs to know <em>why</em> — not just that it did. In healthcare settings, black-box decisions carry real consequences: a false positive may shut down a patient monitor; a false negative may allow ransomware onto a ventilator network.</p> <p>This post covers the research I contributed to our 2026 Academic Press book chapter on Explainable AI for IoMT security. We apply a <strong>TabTransformer</strong> — a Transformer architecture adapted for tabular data — and explain its predictions with <strong>SHAP</strong> (SHapley Additive exPlanations). The result is a classifier that achieves strong detection accuracy <em>and</em> provides human-readable explanations.</p> <hr/> <h2 id="the-problem">The Problem</h2> <p><strong>IoMT (Internet of Medical Things)</strong> devices — infusion pumps, wearables, imaging equipment — generate tabular network traffic logs. Detecting anomalies in this traffic is a classic tabular classification problem, but with unusually high stakes.</p> <p>Two failure modes matter:</p> <table> <thead> <tr> <th>Failure</th> <th>Consequence</th> </tr> </thead> <tbody> <tr> <td>False Positive</td> <td>Alert fatigue; legitimate devices shut down</td> </tr> <tr> <td>False Negative</td> <td>Intrusion missed; patient safety at risk</td> </tr> </tbody> </table> <p>Standard neural networks achieve high accuracy but provide no explanation. Clinicians and security auditors need to see which features drove a prediction — this is where SHAP comes in.</p> <hr/> <h2 id="architecture-tabtransformer">Architecture: TabTransformer</h2> <p>Standard feedforward networks treat all tabular features identically. <strong>TabTransformer</strong> (Huang et al., 2020) applies self-attention to <em>categorical</em> embeddings, learning contextual relationships between features, while processing numerical features separately through a layer normalisation branch.</p> <pre><code class="language-mermaid">flowchart TD
    subgraph Input
        Cat["Categorical Features\n(protocol, port, flag)"]
        Num["Numerical Features\n(packet size, duration, rate)"]
    end

    Cat --&gt; Emb["Column Embeddings"]
    Emb --&gt; TF["Transformer Encoder\n(Multi-Head Self-Attention × L)"]
    TF --&gt; Flat1["Flatten"]

    Num --&gt; LN["Layer Normalisation"]
    LN --&gt; Flat2["Pass-through"]

    Flat1 --&gt; Concat["Concatenate"]
    Flat2 --&gt; Concat
    Concat --&gt; MLP["MLP Head\n(FC → ReLU → Dropout → FC)"]
    MLP --&gt; Out["Attack Class Prediction"]

    style Input fill:#e3f2fd
    style Out fill:#e8f5e9
</code></pre> <p>The key innovation: self-attention allows the model to learn that <code class="language-plaintext highlighter-rouge">protocol=TCP + port=22 + flag=SYN</code> together signal an SSH brute-force, even if each feature individually is unremarkable.</p> <hr/> <h2 id="setup">Setup</h2> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>torch torchvision pytorch-tabnet shap scikit-learn pandas matplotlib
</code></pre></div></div> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="n">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.nn</span> <span class="k">as</span> <span class="n">nn</span>
<span class="kn">import</span> <span class="n">shap</span>
<span class="kn">import</span> <span class="n">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="kn">from</span> <span class="n">sklearn.model_selection</span> <span class="kn">import</span> <span class="n">train_test_split</span>
<span class="kn">from</span> <span class="n">sklearn.preprocessing</span> <span class="kn">import</span> <span class="n">LabelEncoder</span><span class="p">,</span> <span class="n">StandardScaler</span>
<span class="kn">from</span> <span class="n">sklearn.metrics</span> <span class="kn">import</span> <span class="n">classification_report</span><span class="p">,</span> <span class="n">f1_score</span>
<span class="kn">from</span> <span class="n">pytorch_tabnet.tab_model</span> <span class="kn">import</span> <span class="n">TabNetClassifier</span>   <span class="c1"># TabNet as baseline
</span>
<span class="n">SEED</span> <span class="o">=</span> <span class="mi">42</span>
<span class="n">torch</span><span class="p">.</span><span class="nf">manual_seed</span><span class="p">(</span><span class="n">SEED</span><span class="p">)</span>
<span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="nf">seed</span><span class="p">(</span><span class="n">SEED</span><span class="p">)</span>
</code></pre></div></div> <p>We use <strong>PyTorch-TabNet</strong> as a practical implementation. While TabTransformer and TabNet differ architecturally, both apply attention to tabular data and support SHAP-based explainability — TabNet is available as a maintained open-source library.</p> <hr/> <h2 id="dataset">Dataset</h2> <p>We use the <strong>CICIDS-2017</strong> dataset — a widely used benchmark for network intrusion detection with 14 attack types plus benign traffic.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Load a sample (full dataset is ~2.8M rows)
</span><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="nf">read_csv</span><span class="p">(</span><span class="sh">"</span><span class="s">CICIDS2017_sample.csv</span><span class="sh">"</span><span class="p">)</span>
<span class="n">df</span><span class="p">.</span><span class="n">columns</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">columns</span><span class="p">.</span><span class="nb">str</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span>           <span class="c1"># dataset has trailing whitespace in headers
</span><span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="nf">replace</span><span class="p">([</span><span class="n">np</span><span class="p">.</span><span class="n">inf</span><span class="p">,</span> <span class="o">-</span><span class="n">np</span><span class="p">.</span><span class="n">inf</span><span class="p">],</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">).</span><span class="nf">dropna</span><span class="p">()</span>

<span class="nf">print</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="sh">"</span><span class="s"> Label</span><span class="sh">"</span><span class="p">].</span><span class="nf">value_counts</span><span class="p">().</span><span class="nf">head</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span>
</code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BENIGN             67343
DoS Hulk           46158
PortScan           15890
DDoS               12832
DoS GoldenEye       2303
FTP-Patator         1296
SSH-Patator         1177
DoS slowloris        750
...
</code></pre></div></div> <p><strong>Class imbalance</strong> is severe. We address this through stratified sampling and the model’s class weights.</p> <hr/> <h2 id="preprocessing">Preprocessing</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">TARGET</span> <span class="o">=</span> <span class="sh">"</span><span class="s"> Label</span><span class="sh">"</span>
<span class="n">DROP_COLS</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">Flow ID</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s"> Source IP</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s"> Destination IP</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s"> Timestamp</span><span class="sh">"</span><span class="p">]</span>

<span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="nf">drop</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="n">c</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">DROP_COLS</span> <span class="k">if</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="n">columns</span><span class="p">])</span>

<span class="c1"># Encode target
</span><span class="n">le</span> <span class="o">=</span> <span class="nc">LabelEncoder</span><span class="p">()</span>
<span class="n">y</span>  <span class="o">=</span> <span class="n">le</span><span class="p">.</span><span class="nf">fit_transform</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="n">TARGET</span><span class="p">])</span>
<span class="n">X</span>  <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="nf">drop</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="n">TARGET</span><span class="p">])</span>

<span class="c1"># Numerical features only (CICIDS is all-numerical after drop)
</span><span class="n">scaler</span> <span class="o">=</span> <span class="nc">StandardScaler</span><span class="p">()</span>
<span class="n">X_scaled</span> <span class="o">=</span> <span class="n">scaler</span><span class="p">.</span><span class="nf">fit_transform</span><span class="p">(</span><span class="n">X</span><span class="p">)</span>

<span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> <span class="nf">train_test_split</span><span class="p">(</span>
    <span class="n">X_scaled</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span>
    <span class="n">test_size</span><span class="o">=</span><span class="mf">0.2</span><span class="p">,</span>
    <span class="n">random_state</span><span class="o">=</span><span class="n">SEED</span><span class="p">,</span>
    <span class="n">stratify</span><span class="o">=</span><span class="n">y</span>
<span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Train: </span><span class="si">{</span><span class="n">X_train</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="s">, Test: </span><span class="si">{</span><span class="n">X_test</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Classes: </span><span class="si">{</span><span class="n">le</span><span class="p">.</span><span class="n">classes_</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div> <hr/> <h2 id="training-the-tabnet-classifier">Training the TabNet Classifier</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">clf</span> <span class="o">=</span> <span class="nc">TabNetClassifier</span><span class="p">(</span>
    <span class="n">n_d</span><span class="o">=</span><span class="mi">32</span><span class="p">,</span>               <span class="c1"># embedding dimension per step
</span>    <span class="n">n_a</span><span class="o">=</span><span class="mi">32</span><span class="p">,</span>               <span class="c1"># attention embedding dimension
</span>    <span class="n">n_steps</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span>            <span class="c1"># number of sequential attention steps
</span>    <span class="n">gamma</span><span class="o">=</span><span class="mf">1.3</span><span class="p">,</span>            <span class="c1"># coefficient for feature reusage in steps
</span>    <span class="n">n_independent</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
    <span class="n">n_shared</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
    <span class="n">lambda_sparse</span><span class="o">=</span><span class="mf">1e-3</span><span class="p">,</span>   <span class="c1"># sparsity regularisation — forces feature selection
</span>    <span class="n">optimizer_fn</span><span class="o">=</span><span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="n">Adam</span><span class="p">,</span>
    <span class="n">optimizer_params</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">lr</span><span class="sh">"</span><span class="p">:</span> <span class="mf">2e-3</span><span class="p">},</span>
    <span class="n">mask_type</span><span class="o">=</span><span class="sh">"</span><span class="s">sparsemax</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">verbose</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
    <span class="n">seed</span><span class="o">=</span><span class="n">SEED</span>
<span class="p">)</span>

<span class="n">clf</span><span class="p">.</span><span class="nf">fit</span><span class="p">(</span>
    <span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span>
    <span class="n">eval_set</span><span class="o">=</span><span class="p">[(</span><span class="n">X_test</span><span class="p">,</span> <span class="n">y_test</span><span class="p">)],</span>
    <span class="n">eval_metric</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">accuracy</span><span class="sh">"</span><span class="p">],</span>
    <span class="n">max_epochs</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span>
    <span class="n">patience</span><span class="o">=</span><span class="mi">15</span><span class="p">,</span>          <span class="c1"># early stopping
</span>    <span class="n">batch_size</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span>
    <span class="n">virtual_batch_size</span><span class="o">=</span><span class="mi">128</span>
<span class="p">)</span>
</code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>epoch 10  | loss: 0.2841 | val_accuracy: 0.9512
epoch 20  | loss: 0.1763 | val_accuracy: 0.9698
epoch 40  | loss: 0.1124 | val_accuracy: 0.9801
epoch 60  | loss: 0.0932 | val_accuracy: 0.9847
Early stopping at epoch 71 | best val_accuracy: 0.9851
</code></pre></div></div> <hr/> <h2 id="evaluation">Evaluation</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">y_pred</span> <span class="o">=</span> <span class="n">clf</span><span class="p">.</span><span class="nf">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="nf">classification_report</span><span class="p">(</span>
    <span class="n">y_test</span><span class="p">,</span> <span class="n">y_pred</span><span class="p">,</span>
    <span class="n">target_names</span><span class="o">=</span><span class="n">le</span><span class="p">.</span><span class="n">classes_</span><span class="p">,</span>
    <span class="n">digits</span><span class="o">=</span><span class="mi">3</span>
<span class="p">))</span>
</code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                   precision    recall  f1-score   support

           BENIGN      0.992     0.987     0.990     13469
          DDoS           0.981     0.994     0.987      2566
     DoS GoldenEye      0.963     0.949     0.956       461
         DoS Hulk       0.978     0.983     0.981      9232
    DoS Slowloris       0.971     0.958     0.965       150
     FTP-Patator       0.994     0.991     0.993       259
          PortScan      0.979     0.982     0.981      3178
      SSH-Patator       0.989     0.986     0.988       235

         accuracy                          0.985     29550
        macro avg      0.981     0.979     0.980     29550
     weighted avg      0.985     0.985     0.985     29550
</code></pre></div></div> <p>98.5% accuracy, 0.980 macro F1. Strong — but accuracy alone is not enough for a security system. We need to understand <em>what</em> the model learned.</p> <hr/> <h2 id="explainability-with-shap">Explainability with SHAP</h2> <p><strong>SHAP</strong> computes Shapley values: for each prediction, how much did each feature <em>contribute</em> to pushing the output above or below the baseline?</p> \[\phi_i = \sum_{S \subseteq F \setminus \{i\}} \frac{|S|!(|F|-|S|-1)!}{|F|!} \left[ f(S \cup \{i\}) - f(S) \right]\] <p>Where \(\phi_i\) is the Shapley value for feature \(i\), \(F\) is the full feature set, and \(f(S)\) is the model output using only the feature subset \(S\).</p> <p>TabNet has a built-in feature importance mechanism (via the sparse attention masks). We combine it with SHAP’s model-agnostic <code class="language-plaintext highlighter-rouge">KernelExplainer</code> for sample-level explanations.</p> <h3 id="global-feature-importance-tabnet-built-in">Global Feature Importance (TabNet built-in)</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">feature_importances</span> <span class="o">=</span> <span class="n">clf</span><span class="p">.</span><span class="n">feature_importances_</span>
<span class="n">feature_names</span>       <span class="o">=</span> <span class="n">X</span><span class="p">.</span><span class="n">columns</span><span class="p">.</span><span class="nf">tolist</span><span class="p">()</span>

<span class="c1"># Top 15 features
</span><span class="n">fi_df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="nc">DataFrame</span><span class="p">({</span>
    <span class="sh">"</span><span class="s">feature</span><span class="sh">"</span><span class="p">:</span>    <span class="n">feature_names</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">importance</span><span class="sh">"</span><span class="p">:</span> <span class="n">feature_importances</span>
<span class="p">}).</span><span class="nf">sort_values</span><span class="p">(</span><span class="sh">"</span><span class="s">importance</span><span class="sh">"</span><span class="p">,</span> <span class="n">ascending</span><span class="o">=</span><span class="bp">False</span><span class="p">).</span><span class="nf">head</span><span class="p">(</span><span class="mi">15</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="nf">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">5</span><span class="p">))</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">barh</span><span class="p">(</span><span class="n">fi_df</span><span class="p">[</span><span class="sh">"</span><span class="s">feature</span><span class="sh">"</span><span class="p">][::</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="n">fi_df</span><span class="p">[</span><span class="sh">"</span><span class="s">importance</span><span class="sh">"</span><span class="p">][::</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span> <span class="n">color</span><span class="o">=</span><span class="sh">"</span><span class="s">#1976D2</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Mean Attention Weight</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">TabNet Global Feature Importance — IoMT Intrusion Detection</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sh">"</span><span class="s">tabnet_global_importance.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div> <p>Top features (representative): <code class="language-plaintext highlighter-rouge">Flow Duration</code>, <code class="language-plaintext highlighter-rouge">Total Fwd Packets</code>, <code class="language-plaintext highlighter-rouge">Bwd Packet Length Max</code>, <code class="language-plaintext highlighter-rouge">Flow Bytes/s</code>, <code class="language-plaintext highlighter-rouge">Fwd IAT Total</code>.</p> <h3 id="shap-summary-plot-sample-level">SHAP Summary Plot (Sample-Level)</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Use a background sample for SHAP KernelExplainer
</span><span class="n">background</span> <span class="o">=</span> <span class="n">shap</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="mi">100</span><span class="p">)</span>

<span class="c1"># Predict probabilities (needed for KernelExplainer)
</span><span class="k">def</span> <span class="nf">predict_proba</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">clf</span><span class="p">.</span><span class="nf">predict_proba</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="n">explainer</span>   <span class="o">=</span> <span class="n">shap</span><span class="p">.</span><span class="nc">KernelExplainer</span><span class="p">(</span><span class="n">predict_proba</span><span class="p">,</span> <span class="n">background</span><span class="p">)</span>

<span class="c1"># Explain 200 test samples (KernelExplainer is slow — sample wisely)
</span><span class="n">X_explain</span>   <span class="o">=</span> <span class="n">X_test</span><span class="p">[:</span><span class="mi">200</span><span class="p">]</span>
<span class="n">shap_values</span> <span class="o">=</span> <span class="n">explainer</span><span class="p">.</span><span class="nf">shap_values</span><span class="p">(</span><span class="n">X_explain</span><span class="p">,</span> <span class="n">nsamples</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>

<span class="c1"># Summary plot for the DDoS class (index 1)
</span><span class="n">shap</span><span class="p">.</span><span class="nf">summary_plot</span><span class="p">(</span>
    <span class="n">shap_values</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
    <span class="n">X_explain</span><span class="p">,</span>
    <span class="n">feature_names</span><span class="o">=</span><span class="n">feature_names</span><span class="p">,</span>
    <span class="n">max_display</span><span class="o">=</span><span class="mi">15</span><span class="p">,</span>
    <span class="n">show</span><span class="o">=</span><span class="bp">False</span>
<span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">SHAP Summary — DDoS Class</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sh">"</span><span class="s">shap_summary_ddos.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">,</span> <span class="n">bbox_inches</span><span class="o">=</span><span class="sh">"</span><span class="s">tight</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div> <p>The SHAP summary plot shows:</p> <ul> <li><strong>Each dot</strong> = one sample</li> <li><strong>Position on X</strong> = SHAP value (positive → pushes toward DDoS prediction)</li> <li><strong>Colour</strong> = feature value (red = high, blue = low)</li> </ul> <p>For DDoS: high <code class="language-plaintext highlighter-rouge">Flow Bytes/s</code> and high <code class="language-plaintext highlighter-rouge">Total Fwd Packets</code> with short <code class="language-plaintext highlighter-rouge">Flow Duration</code> strongly push toward DDoS — consistent with volumetric attack behaviour.</p> <h3 id="single-prediction-explanation">Single Prediction Explanation</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Pick a DDoS sample
</span><span class="n">ddos_indices</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="n">y_test</span> <span class="o">==</span> <span class="n">le</span><span class="p">.</span><span class="nf">transform</span><span class="p">([</span><span class="sh">"</span><span class="s">DDoS</span><span class="sh">"</span><span class="p">])[</span><span class="mi">0</span><span class="p">])[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">sample_idx</span>   <span class="o">=</span> <span class="n">ddos_indices</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">sample</span>       <span class="o">=</span> <span class="n">X_test</span><span class="p">[</span><span class="n">sample_idx</span><span class="p">:</span><span class="n">sample_idx</span><span class="o">+</span><span class="mi">1</span><span class="p">]</span>

<span class="n">shap_single</span> <span class="o">=</span> <span class="n">explainer</span><span class="p">.</span><span class="nf">shap_values</span><span class="p">(</span><span class="n">sample</span><span class="p">,</span> <span class="n">nsamples</span><span class="o">=</span><span class="mi">200</span><span class="p">)</span>

<span class="n">shap</span><span class="p">.</span><span class="nf">force_plot</span><span class="p">(</span>
    <span class="n">explainer</span><span class="p">.</span><span class="n">expected_value</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
    <span class="n">shap_single</span><span class="p">[</span><span class="mi">1</span><span class="p">][</span><span class="mi">0</span><span class="p">],</span>
    <span class="n">sample</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
    <span class="n">feature_names</span><span class="o">=</span><span class="n">feature_names</span><span class="p">,</span>
    <span class="n">matplotlib</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">show</span><span class="o">=</span><span class="bp">False</span>
<span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Force Plot — DDoS prediction (true label: DDoS)</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">savefig</span><span class="p">(</span><span class="sh">"</span><span class="s">shap_force_ddos.png</span><span class="sh">"</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">,</span> <span class="n">bbox_inches</span><span class="o">=</span><span class="sh">"</span><span class="s">tight</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div> <p>The force plot shows which features pushed the prediction from the base rate toward “DDoS”: a high packet rate, uniform packet sizes, and near-zero inter-arrival time are the primary drivers — exactly what a DDoS flood looks like at the network level.</p> <hr/> <h2 id="why-this-matters-for-healthcare">Why This Matters for Healthcare</h2> <table> <thead> <tr> <th>XAI Benefit</th> <th>Clinical Security Context</th> </tr> </thead> <tbody> <tr> <td><strong>Auditability</strong></td> <td>Regulatory bodies (HIPAA, MDR) require documented decision rationale</td> </tr> <tr> <td><strong>Trust calibration</strong></td> <td>Engineers can sanity-check: “Does the model flag DDoS for the right reasons?”</td> </tr> <tr> <td><strong>Bias detection</strong></td> <td>SHAP reveals if model relies on artefacts (e.g., specific IP ranges from lab collection)</td> </tr> <tr> <td><strong>Incident reporting</strong></td> <td>Force plots translate directly into incident log entries</td> </tr> </tbody> </table> <p>A model that correctly detects 98.5% of attacks but cannot explain a single prediction is not deployable in a hospital environment. SHAP bridges that gap.</p> <hr/> <h2 id="limitations">Limitations</h2> <ul> <li><strong>KernelExplainer</strong> is slow. For production, use TreeSHAP (tree-based models) or DeepSHAP (PyTorch models with hooks).</li> <li><strong>SHAP ≠ causality.</strong> High SHAP value for <code class="language-plaintext highlighter-rouge">Flow Bytes/s</code> means it correlates with the prediction — not that it causes the attack.</li> <li><strong>Distribution shift.</strong> IoMT devices in real hospitals produce different traffic patterns than CICIDS lab captures. Always validate on data from the deployment environment.</li> </ul> <hr/> <h2 id="exercises">Exercises</h2> <ol> <li>Replace TabNet with a standard gradient-boosted tree (XGBoost). Compare SHAP TreeExplainer speed vs. KernelExplainer.</li> <li>Use <code class="language-plaintext highlighter-rouge">shap.dependence_plot</code> to visualise how <code class="language-plaintext highlighter-rouge">Flow Duration</code> interacts with <code class="language-plaintext highlighter-rouge">Fwd Packet Length Mean</code> for the DDoS class.</li> <li>Apply SHAP to identify and remove features that are artefacts of the data collection setup rather than real attack signals.</li> <li>Implement LIME as an alternative explainability method. Compare its local explanations with SHAP on the same samples.</li> </ol> <hr/> <h2 id="references">References</h2> <ul> <li>Pande, S., Mahore, T., et al. (2026). <em>TabTransformer for Explainable IoMT Intrusion Detection.</em> Academic Press Book Chapter.</li> <li>Huang, X., et al. (2020). <em>TabTransformer: Tabular Data Modeling Using Contextual Embeddings.</em> arXiv:2012.06678.</li> <li>Lundberg, S. M. &amp; Lee, S.-I. (2017). <em>A Unified Approach to Interpreting Model Predictions.</em> NeurIPS 2017.</li> <li>Arik, S. Ö. &amp; Pfister, T. (2021). <em>TabNet: Attentive Interpretable Tabular Learning.</em> AAAI 2021.</li> <li>Sharafaldin, I., et al. (2018). <em>Toward Generating a New Intrusion Detection Dataset.</em> ICISSP 2018.</li> </ul>]]></content><author><name></name></author><category term="research"/><category term="tutorial"/><category term="xai"/><category term="transformer"/><category term="tabular-data"/><category term="shap"/><category term="healthcare"/><category term="python"/><category term="tutorial"/><summary type="html"><![CDATA[Apply a TabTransformer — a Transformer architecture for tabular data — to IoMT intrusion detection, then explain its predictions using SHAP. A deep dive into XAI for high-stakes healthcare systems, based on our Academic Press 2026 book chapter.]]></summary></entry><entry><title type="html">Google Gemini updates: Flash 1.5, Gemma 2 and Project Astra</title><link href="https://mahoretushar.github.io/blog/2024/google-gemini-updates-flash-15-gemma-2-and-project-astra/" rel="alternate" type="text/html" title="Google Gemini updates: Flash 1.5, Gemma 2 and Project Astra"/><published>2024-05-14T00:00:00+00:00</published><updated>2024-05-14T00:00:00+00:00</updated><id>https://mahoretushar.github.io/blog/2024/google-gemini-updates-flash-15-gemma-2-and-project-astra</id><content type="html" xml:base="https://mahoretushar.github.io/blog/2024/google-gemini-updates-flash-15-gemma-2-and-project-astra/"><![CDATA[<p>Gemini breaks new ground with a faster model, longer context, AI agents and moreLearn more:Learn more:Learn more:Models &amp; ResearchProductsInfrastructure &amp; cloudTechnology Learn more: ProductsPlatformsDevices Learn more: Outreach &amp; initiativesLeadershipInside Google Learn more: May 14, 2024 We’re introducing a series of updates across the Gemini family of models, including the new 1.5 Flash, our lightweight model for speed and efficiency, and Project Astra, our vision for the future of AI assistants. Demis HassabisCEO of Google DeepMind, on behalf of the Gemini teamIn December, we launched our first natively multimodal model Gemini 1.0 in three sizes: Ultra, Pro and Nano. Just a few months later we released 1.5 Pro, with enhanced performance and a breakthrough long context window of 1 million tokens.Developers and enterprise customers have been putting 1.5 Pro to use in incredible ways and finding its long context window, multimodal reasoning capabilities and impressive overall performance incredibly useful.We know from user feedback that some applications need lower latency and a lower cost to serve. This inspired us to keep innovating, so today, we’re introducing Gemini 1.5 Flash: a model that’s lighter-weight than 1.5 Pro, and designed to be fast and efficient to serve at scale.Both 1.5 Pro and 1.5 Flash are available in public preview with a 1 million token context window in Google AI Studio and Vertex AI. And now, 1.5 Pro is also available with a 2 million token context window via waitlist to developers using the API and to Google Cloud customers.We’re also introducing updates across the Gemini family of models, announcing our next generation of open models, Gemma 2, and sharing progress on the future of AI assistants, with Project Astra.Context lengths of leading foundation models compared with Gemini 1.5’s 2 million token capability1.5 Flash is the newest addition to the Gemini model family and the fastest Gemini model served in the API. It’s optimized for high-volume, high-frequency tasks at scale, is more cost-efficient to serve and features our breakthrough long context window.While it’s a lighter weight model than 1.5 Pro, it’s highly capable of multimodal reasoning across vast amounts of information and delivers impressive quality for its size.The new Gemini 1.5 Flash model is optimized for speed and efficiency, is highly capable of multimodal reasoning and features our breakthrough long context window.1.5 Flash excels at summarization, chat applications, image and video captioning, data extraction from long documents and tables, and more. This is because it’s been trained by 1.5 Pro through a process called “distillation,” where the most essential knowledge and skills from a larger model are transferred to a smaller, more efficient model.Read more about 1.5 Flash in our updated Gemini 1.5 technical report, on the Gemini technology page, and learn about 1.5 Flash’s availability and pricing.Over the last few months, we’ve significantly improved 1.5 Pro, our best model for general performance across a wide range of tasks.Beyond extending its context window to 2 million tokens, we’ve enhanced its code generation, logical reasoning and planning, multi-turn conversation, and audio and image understanding through data and algorithmic advances. We see strong improvements on public and internal benchmarks for each of these tasks.1.5 Pro can now follow increasingly complex and nuanced instructions, including ones that specify product-level behavior involving role, format and style. We’ve improved control over the model’s responses for specific use cases, like crafting the persona and response style of a chat agent or automating workflows through multiple function calls. And we’ve enabled users to steer model behavior by setting system instructions.We added audio understanding in the Gemini API and Google AI Studio, so 1.5 Pro can now reason across image and audio for videos uploaded in Google AI Studio. And we’re now integrating 1.5 Pro into Google products, including Gemini Advanced and in Workspace apps.Read more about 1.5 Pro in our updated Gemini 1.5 technical report and on the Gemini technology page.Gemini Nano is expanding beyond text-only inputs to include images as well. Starting with Pixel, applications using Gemini Nano with Multimodality will be able to understand the world the way people do — not just through text, but also through sight, sound and spoken language.Read more about Gemini 1.0 Nano on Android.Today, we’re also sharing a series of updates to Gemma, our family of open models built from the same research and technology used to create the Gemini models.We’re announcing Gemma 2, our next generation of open models for responsible AI innovation. Gemma 2 has a new architecture designed for breakthrough performance and efficiency, and will be available in new sizes.The Gemma family is also expanding with PaliGemma, our first vision-language model inspired by PaLI-3. And we’ve upgraded our Responsible Generative AI Toolkit with LLM Comparator for evaluating the quality of model responses.Read more on the Developer blog.As part of Google DeepMind’s mission to build AI responsibly to benefit humanity, we’ve always wanted to develop universal AI agents that can be helpful in everyday life. That’s why today, we’re sharing our progress in building the future of AI assistants with Project Astra (advanced seeing and talking responsive agent).To be truly useful, an agent needs to understand and respond to the complex and dynamic world just like people do — and take in and remember what it sees and hears to understand context and take action. It also needs to be proactive, teachable and personal, so users can talk to it naturally and without lag or delay.While we’ve made incredible progress developing AI systems that can understand multimodal information, getting response time down to something conversational is a difficult engineering challenge. Over the past few years, we’ve been working to improve how our models perceive, reason and converse to make the pace and quality of interaction feel more natural.Building on Gemini, we’ve developed prototype agents that can process information faster by continuously encoding video frames, combining the video and speech input into a timeline of events, and caching this information for efficient recall.By leveraging our leading speech models, we also enhanced how they sound, giving the agents a wider range of intonations. These agents can better understand the context they’re being used in, and respond quickly, in conversation.With technology like this, it’s easy to envision a future where people could have an expert AI assistant by their side, through a phone or glasses. And some of these capabilities are coming to Google products, like the Gemini app and web experience, later this year.We’ve made incredible progress so far with our family of Gemini models, and we’re always striving to advance the state-of-the-art even further. By investing in a relentless production line of innovation, we’re able to explore new ideas at the frontier, while also unlocking the possibility of new and exciting Gemini use cases.Learn more about Gemini and its capabilities.Collection Sign up for our newsletters with product updates, event information, special offers, and more. Done. Just one step more. Check your inbox to confirm your subscription.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>You can also subscribe with a different email address.
  
        Your information will be used in accordance with Google's privacy policy. You may opt out at any time.
</code></pre></div></div>]]></content><author><name></name></author><category term="external-posts"/><category term="google"/><summary type="html"><![CDATA[We’re sharing updates across our Gemini family of models and a glimpse of Project Astra, our vision for the future of AI assistants.]]></summary></entry><entry><title type="html">Displaying External Posts on Your al-folio Blog</title><link href="https://mahoretushar.github.io/blog/2022/displaying-external-posts-on-your-al-folio-blog/" rel="alternate" type="text/html" title="Displaying External Posts on Your al-folio Blog"/><published>2022-04-23T23:20:09+00:00</published><updated>2022-04-23T23:20:09+00:00</updated><id>https://mahoretushar.github.io/blog/2022/displaying-external-posts-on-your-al-folio-blog</id><content type="html" xml:base="https://mahoretushar.github.io/blog/2022/displaying-external-posts-on-your-al-folio-blog/"><![CDATA[<h3>External Posts on Your al-folio Blog</h3> <p>If you prefer publishing blog posts on medium.com or other external sources, starting version v0.5.0, <a href="https://github.com/alshedivat/al-folio">al-folio</a> lets you to display your external posts in the blog feed of your website! 🎉🎉</p> <p>Configuring external sources of super simple. After upgrading to v0.5.0, just add the following section to your _config.yml:</p> <pre>external_sources:<br />  - name: medium.com  # name of the source (arbitrary string)<br />    rss_url: <a href="https://medium.com/@al-folio/feed">https://medium.com/@&lt;your-medium-username&gt;/feed</a></pre> <p>The example above adds your medium.com blog post feed as an external source. But you can add arbitrary RSS feeds as sources.</p> <p>Any questions or suggestions? 👉 Start <a href="https://github.com/alshedivat/al-folio/discussions">a discussion on GitHub</a>!</p> <p><img src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=b60a1d241a0a" width="1" height="1" alt=""/></p>]]></content><author><name></name></author><category term="external-posts"/><category term="medium"/></entry></feed>