<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Biki Kalita</title>
    <description>The latest articles on DEV Community by Biki Kalita (@bikidev).</description>
    <link>https://dev.to/bikidev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810004%2Fd7834cae-1471-40db-a3fc-024b28d39140.jpeg</url>
      <title>DEV Community: Biki Kalita</title>
      <link>https://dev.to/bikidev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bikidev"/>
    <language>en</language>
    <item>
      <title>The Auto-DJ Case Files: Building a Self-Learning Music Brain From Your YouTube Habit</title>
      <dc:creator>Biki Kalita</dc:creator>
      <pubDate>Sun, 26 Jul 2026 13:22:39 +0000</pubDate>
      <link>https://dev.to/bikidev/the-auto-dj-case-files-building-a-self-learning-music-brain-from-your-youtube-habit-1ndl</link>
      <guid>https://dev.to/bikidev/the-auto-dj-case-files-building-a-self-learning-music-brain-from-your-youtube-habit-1ndl</guid>
      <description>&lt;h3&gt;
  
  
  A 3–5 month field guide for turning a browsing habit into an AI orchestra
&lt;/h3&gt;




&lt;h2&gt;
  
  
  Prologue: The Case That Started With a Skip Button
&lt;/h2&gt;

&lt;p&gt;Every great investigation starts with something small and irritating. Yours starts on a Tuesday night. You're three hours deep into a YouTube rabbit hole — lo-fi beats, a Vietnamese city-pop deep cut, a live session you didn't mean to watch twice — and you think, &lt;em&gt;someone should be paying attention to this.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That someone is about to be you. Not as a listener. As a detective.&lt;/p&gt;

&lt;p&gt;Here's the case file: somewhere in your YouTube history is a &lt;em&gt;pattern&lt;/em&gt;. A shape. You skip certain songs after four seconds. You replay others three times in a row without noticing. You always end up on synthwave at 11pm and never before 6pm. Right now, that pattern is invisible — smeared across a history page that YouTube barely lets you search, let alone reason about. Your job over the next few months is to build the instrument that makes it visible: a system that watches what you actually do, figures out what it means, and hands you back playlists that feel like they were made by someone who &lt;em&gt;knows&lt;/em&gt; you. Because they were. That someone is a machine you built, trained on a dataset of one: you.&lt;/p&gt;

&lt;p&gt;This won't be a weekend project. It's closer to building a small orchestra — a browser extension as your field agent, a backend as your evidence locker, a couple of ML models as your forensic lab, and a frontend as the stage where the final performance happens. Every piece depends on every other piece. That's what makes it hard, and it's also what makes it worth doing slowly, in public, with your own hands on every layer.&lt;/p&gt;

&lt;p&gt;Let's start where every good documentary starts: by following one song from the crime scene to the credits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 1: The Case of the Repeating Refrain
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Following one song's journey
&lt;/h3&gt;

&lt;p&gt;It's 9:47pm. You click a YouTube video: "Kiss of Life – Midas Touch (Live Performance)." You watch 41 seconds, skip to the chorus, watch another 90 seconds, then close the tab. Here's what needs to happen for that unremarkable moment to eventually shape a playlist called "Monday Chill":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Witness (Browser Extension).&lt;/strong&gt; A content script sitting quietly inside the YouTube tab notices the video element load. It reads the video ID, title, channel name, and duration from the page. It watches the &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element's &lt;code&gt;timeupdate&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;seeked&lt;/code&gt;, and &lt;code&gt;ended&lt;/code&gt; events like a stenographer taking notes: &lt;em&gt;play at 0:00, seek to 1:32, pause at 3:04, total watch time 41s + 90s = 131s out of 210s (62%).&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Tip-Off (Event API).&lt;/strong&gt; Every so often — batched, not on every tick, because nobody wants to be &lt;em&gt;that&lt;/em&gt; extension hammering a server — the extension bundles these observations into a small JSON payload and POSTs it to your backend: &lt;code&gt;{video_id, title, channel, watch_segments, timestamp, session_id}&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Evidence Locker (Database).&lt;/strong&gt; Your FastAPI backend receives the tip-off, validates it, and writes a raw event row into PostgreSQL. Nothing clever happens yet — this is just chain-of-custody. You never want to lose the raw signal, because every clever thing you do later will be &lt;em&gt;derived&lt;/em&gt; from it, and derived things need to be recomputable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Lab Work (ML Pipelines, offline).&lt;/strong&gt; On a schedule — say, once a night — a pipeline wakes up, looks for new raw events, and does the actual detective work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Is this even a music video?&lt;/em&gt; (classification)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;What song is this, really?&lt;/em&gt; (metadata resolution via title parsing + external lookups)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;What does it sound/feel like?&lt;/em&gt; (audio embedding + mood tagging)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;How does this fit with everything else you've watched?&lt;/em&gt; (behavioral signal: implicit rating from watch %, skip pattern, replay count, time of day)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Case Board (Vector store + relational tables).&lt;/strong&gt; The song's audio embedding gets written to a vector column (via &lt;code&gt;pgvector&lt;/code&gt;), its metadata to a normal relational table, and its behavioral score gets updated incrementally — like a detective updating a suspect's file every time new evidence comes in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Reveal (Playlist Generator).&lt;/strong&gt; Periodically, a clustering job looks at the whole case board — all your songs, their embeddings, their mood tags, their time-of-day patterns — and groups them. A cluster of moody, low-energy, evening-heavy tracks with "chill," "lofi," or "acoustic" tags gets surfaced and, because most of its listens happened on Mondays, gets named "Monday Chill."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Broadcast (Frontend + Player).&lt;/strong&gt; Your React app queries the backend, gets back a list of playlists with their tracks, and renders them. When you hit play, it doesn't awkwardly embed a YouTube iframe — it either streams audio you've legitimately extracted for personal use, or falls back to a controlled YouTube player, and it logs &lt;em&gt;that&lt;/em&gt; playback too, feeding the loop again.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole organism, end to end. Six weeks from now this will feel obvious. Right now, notice the shape: &lt;strong&gt;observe → transmit → store → understand → organize → present&lt;/strong&gt;, with a feedback loop closing the circle back to step 1. Every phase of this guide builds one of these organs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The architecture, explained like you're new here
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Browser: YouTube tab]
        |
   content script  (watches video element)
        |
   background script (batches + sends events)
        |
        v
   Event Ingestion API  (FastAPI, /events endpoint)
        |
        v
   Raw Events Table  (PostgreSQL)
        |
   +----+----------------------------+
   |                                 |
   v                                 v
Metadata &amp;amp; Classification      Audio &amp;amp; Behavior Pipelines
(is it music? what song?)      (embeddings, mood, implicit score)
   |                                 |
   +----------------+----------------+
                    v
        Enriched Track Store
   (PostgreSQL relational + pgvector embeddings)
                    |
                    v
          Clustering / Playlist Engine
        (HDBSCAN, labeling, scheduling)
                    |
                    v
             Playlist API (FastAPI)
                    |
                    v
           React Frontend + Player
     (renders playlists, plays audio, logs playback)
                    |
                    +--&amp;gt; feeds back into Raw Events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain English: a browser extension is the only part of this system that can see raw YouTube behavior, so it's your sensor. Everything after that is you progressively distilling raw noise into structured meaning — first "what is this," then "what does it sound like," then "what does it mean that you watched it," then "what group of songs does this belong to." A pipeline is just a name for "a sequence of these distillation steps that runs on a schedule instead of live," which matters because audio analysis and ML inference are too slow to run inline while you're browsing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why these particular tools (and not the flashier alternatives)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FastAPI, not Django or raw Flask.&lt;/strong&gt; You'll be exposing a mix of simple CRUD endpoints and ML inference endpoints. FastAPI's automatic request validation via Pydantic means fewer "why did my extension send a malformed payload and silently corrupt my data" nights, and its async support matters once you're streaming audio or calling external APIs (metadata lookups) without blocking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PostgreSQL + pgvector, not a separate vector database.&lt;/strong&gt; You could stand up Pinecone or Weaviate. For a personal-scale project (thousands, maybe tens of thousands of tracks — not billions), that's solving a problem you don't have yet, at the cost of one more service to run, secure, and back up. &lt;code&gt;pgvector&lt;/code&gt; lets you keep relational metadata (title, channel, mood tags) and embeddings in the &lt;em&gt;same&lt;/em&gt; database, so a query like "find songs similar to this one, but only ones tagged 'study' and watched after 9pm" is one SQL query with a vector distance operator, not a join across two systems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A CNN (or a pretrained audio model) for audio classification, not a giant end-to-end transformer.&lt;/strong&gt; Audio classification at the "is this speech, music, or noise" and "what does this sound like" level has been solved well by relatively small convolutional architectures operating on spectrograms (think YAMNet, VGGish). You don't need GPT-scale compute to tell that a track is high-energy electronic versus a mellow acoustic set. This is a deliberate choice to keep your compute budget human-sized — you'll likely run this on a laptop or a modest cloud instance, not a GPU cluster.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transformers for tagging, not for audio itself.&lt;/strong&gt; Where transformers genuinely earn their complexity is &lt;em&gt;text&lt;/em&gt; — parsing messy YouTube titles ("Artist - Song (Official Music Video) [4K]"), zero-shot classifying mood from lyrics or descriptions, and understanding channel/description context. Hugging Face's zero-shot classification pipelines let you throw a candidate label set ("chill," "energetic," "sad," "study," "workout") at a piece of text and get calibrated-ish scores back without training anything — perfect for a first pass, with fine-tuning as a stretch goal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A browser extension, not scraping Google Takeout alone.&lt;/strong&gt; Takeout gives you history, but it's after the fact, coarse, and doesn't include watch percentage, skips, or replays — exactly the implicit-feedback signal your recommendation logic needs. The extension is how you get &lt;em&gt;behavioral&lt;/em&gt; data, not just &lt;em&gt;occurrence&lt;/em&gt; data. Takeout is still useful — as bootstrap data and backfill, which Chapter 4 covers.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the pattern in every choice above: pick the tool that matches the actual scale and shape of &lt;em&gt;your&lt;/em&gt; problem, not the tool with the best conference talk. You're building a personal system for one very well-instrumented user (you), not a production service for millions. Keep saying that sentence to yourself whenever you're tempted to add Kubernetes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The living organism
&lt;/h3&gt;

&lt;p&gt;Here's the mental model to carry through the rest of this guide: this system has a &lt;strong&gt;nervous system&lt;/strong&gt; (the extension + event API, always sensing), a &lt;strong&gt;subconscious&lt;/strong&gt; (the nightly pipelines, working while you sleep, turning raw sensation into structured understanding), and a &lt;strong&gt;conscious voice&lt;/strong&gt; (the frontend, presenting a curated, explainable result). If you break the nervous system, nothing gets sensed. If you break the subconscious, sensations pile up unprocessed. If you break the conscious voice, all that understanding has nowhere to be expressed. You'll spend the next several months building each of these organs, and — this is the important part — you'll build them so that each one can be tested and demoed &lt;em&gt;on its own&lt;/em&gt;, before they're wired together. That's the difference between a project that has a working prototype at every milestone and a project that only "works" the week before a deadline, if you're lucky.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 2: The Learning Roadmap — Five Months, Six Phases
&lt;/h2&gt;

&lt;p&gt;You know basic Python and JavaScript. You don't yet know how these pieces talk to each other, or how to think about audio and behavior as data. So the roadmap is designed to &lt;strong&gt;stack skills like sediment layers&lt;/strong&gt; — each phase leaves you with a thin, ugly, but genuinely working slice of the system, and each phase's slice becomes the foundation the next phase builds on. You will never be staring at a blank canvas with six unconnected concepts in your head. You will always be extending something that already runs.&lt;/p&gt;

&lt;p&gt;Treat each phase as 2–4 weeks depending on how much time you have per week. If you have 10+ hours/week, aim for the shorter end; if you're doing this around a full-time job, take the longer end. Five months is the "comfortable, learn deeply" pace. Three months is achievable if you already move fast and cut a few of the "stretch" items.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 0: Recon (3–5 days, before Week 1 really starts)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Set up the skeleton of every future piece, even if each piece does almost nothing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create the repo structure: &lt;code&gt;extension/&lt;/code&gt;, &lt;code&gt;backend/&lt;/code&gt;, &lt;code&gt;ml/&lt;/code&gt;, &lt;code&gt;frontend/&lt;/code&gt;, &lt;code&gt;infra/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Get a FastAPI "hello world" running locally with one route, &lt;code&gt;GET /ping&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Get a bare Chrome extension loaded in developer mode that does nothing but log &lt;code&gt;"extension loaded"&lt;/code&gt; to the console on any youtube.com page.&lt;/li&gt;
&lt;li&gt;Spin up a local PostgreSQL instance (Docker is your friend here — &lt;code&gt;docker run postgres&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learning pit stop:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 min: FastAPI's official "First Steps" tutorial — just enough to run a server.&lt;/li&gt;
&lt;li&gt;20 min: Chrome's Extension "Getting Started" tutorial — focus on the manifest and content scripts, skip the rest for now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You know you're ready when:&lt;/strong&gt; you can open a YouTube tab, see &lt;code&gt;"extension loaded"&lt;/code&gt; in the console, and hit &lt;code&gt;http://localhost:8000/ping&lt;/code&gt; in your browser and get &lt;code&gt;{"status": "ok"}&lt;/code&gt;. That's it. Two independent heartbeats.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 1: The Witness — Event Tracking (Weeks 1–3)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; A Chrome extension that logs real YouTube watch events, and a backend endpoint that stores them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills gained:&lt;/strong&gt; DOM event listeners, Chrome extension messaging (content script &amp;lt;-&amp;gt; background script), REST API design basics, relational schema design for event logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A content script that attaches listeners to the YouTube &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element (&lt;code&gt;play&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;seeked&lt;/code&gt;, &lt;code&gt;ended&lt;/code&gt;, &lt;code&gt;timeupdate&lt;/code&gt; sampled every few seconds — not every frame, that's overkill and will flood your database).&lt;/li&gt;
&lt;li&gt;A background script that batches events (e.g., flush every 30 seconds or every 10 events) and sends them via &lt;code&gt;fetch&lt;/code&gt; to your backend.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;POST /events&lt;/code&gt; FastAPI endpoint with a Pydantic model validating the payload, writing rows into a &lt;code&gt;raw_events&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;A minimal schema: &lt;code&gt;raw_events(id, video_id, title, channel, event_type, position_seconds, video_duration_seconds, session_id, created_at)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Architecture decision point:&lt;/strong&gt; Do you send every micro-event (every &lt;code&gt;timeupdate&lt;/code&gt; tick) to the server, or do you compute a per-video &lt;em&gt;summary&lt;/em&gt; (total watch time, max position reached, number of seeks) in the extension and send one summary per video? Sending raw ticks gives you more analytical flexibility later (you could reconstruct exact skip behavior) at the cost of way more data and server load. Summarizing in-extension is simpler and cheaper but throws away granularity. &lt;strong&gt;Recommendation for a learning project:&lt;/strong&gt; send raw ticks for now — you're optimizing for "what can I learn from this," not production efficiency, and you can always summarize later. You can throttle it (sample every 5 seconds) to keep volume sane.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning pit stops:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 min video: search "Chrome extension message passing background content script" — get comfortable with &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Skim MDN's HTMLMediaElement events page — you need &lt;code&gt;timeupdate&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;seeked&lt;/code&gt;, &lt;code&gt;ended&lt;/code&gt;, &lt;code&gt;play&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;10 min: Pydantic's "Models" quickstart — you'll lean on this for every endpoint from here forward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skeleton to work from (not a finished file):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// content_script.js -- the witness's notebook&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;attachListeners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoEl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;videoEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeupdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO: throttle this -- don't fire on every tick&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO: send {event_type: 'progress', position: videoEl.currentTime}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;videoEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;seeked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO: compare videoEl.currentTime to lastPosition&lt;/span&gt;
    &lt;span class="c1"&gt;// to detect skip-forward vs rewind&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;videoEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pause&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO: send pause event with position&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// TODO: figure out how to get videoId from the page URL&lt;/span&gt;
&lt;span class="c1"&gt;// TODO: figure out how to detect when YouTube's SPA navigation&lt;/span&gt;
&lt;span class="c1"&gt;//       changes video without a full page reload (hint: MutationObserver&lt;/span&gt;
&lt;span class="c1"&gt;//       on the title element, or listen for yt-navigate-finish)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# backend/main.py -- the evidence intake desk
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RawEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# 'play' | 'pause' | 'seeked' | 'ended' | 'progress'
&lt;/span&gt;    &lt;span class="n"&gt;position_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;video_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RawEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: open a DB connection (consider SQLAlchemy or raw psycopg)
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: INSERT into raw_events
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: return a lightweight ack, don't do heavy work here --
&lt;/span&gt;    &lt;span class="c1"&gt;#       this endpoint must stay fast, it's on the hot path
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Milestone / "you know you're ready when":&lt;/strong&gt; you watch three different YouTube videos for real, then query your database and see a believable, timestamped trail of what you actually did -- including at least one skip you remember making. Take a screenshot. This is the first time the system has &lt;em&gt;seen&lt;/em&gt; you.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 2: The Classifier -- Is This Even Music? (Weeks 4-6)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Given a raw event's title/channel/duration, decide whether it's a music video at all (versus a podcast, tutorial, vlog) -- and if it is, extract a clean artist/song guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills gained:&lt;/strong&gt; Text classification basics, regex-as-first-resort vs ML-as-fallback thinking, working with the YouTube Data API, zero-shot classification with Hugging Face.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A nightly (or on-demand) script that reads unprocessed rows from &lt;code&gt;raw_events&lt;/code&gt;, groups by &lt;code&gt;video_id&lt;/code&gt;, and for each unique video, runs a classification step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First pass -- heuristics, not ML.&lt;/strong&gt; Channel name contains "Official," "VEVO," "Music"? Title matches patterns like &lt;code&gt;Artist - Title (Official ...)&lt;/code&gt;? These heuristics alone will correctly classify a surprising majority of real YouTube music content. Write these first. Resist the urge to reach for a model before you've squeezed out what regex and metadata can do -- this is a core "how to think like an engineer" lesson: cheap and boring beats fancy and unnecessary, every time it's available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second pass -- zero-shot text classification&lt;/strong&gt; for the ambiguous remainder, using Hugging Face's &lt;code&gt;zero-shot-classification&lt;/code&gt; pipeline with candidate labels like &lt;code&gt;["music video", "podcast", "tutorial", "vlog", "gaming"]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;tracks&lt;/code&gt; table that stores the resolved artist/title guess, separate from the raw event log -- this is your first "enriched" table, the first sign of the subconscious turning sensation into understanding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Architecture decision point:&lt;/strong&gt; Regex-first-then-ML, or ML-first-then-regex-cleanup? Going regex-first means your model only has to handle genuinely ambiguous cases, which is both faster (you skip inference on 70%+ of videos) and more accurate (you're not asking a general-purpose model to outperform a rule you could write in one line for "channel name ends in VEVO"). This is a recurring theme: &lt;strong&gt;use ML for the residual, not the whole problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning pit stop:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 min: Hugging Face's zero-shot classification pipeline docs -- run their example locally before writing your own.&lt;/li&gt;
&lt;li&gt;Skim: the YouTube Data API "Videos: list" reference -- you'll want &lt;code&gt;snippet.description&lt;/code&gt; and &lt;code&gt;snippet.tags&lt;/code&gt; for extra signal, and eventually &lt;code&gt;contentDetails.duration&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skeleton:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ml/classify.py -- the detective's first read of the case file
&lt;/span&gt;&lt;span class="n"&gt;MUSIC_CHANNEL_HINTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vevo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;official&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;music&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;heuristic_is_music&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Return True/False if confident, None if unsure (defer to ML)
&lt;/span&gt;    &lt;span class="n"&gt;channel_lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;channel_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MUSIC_CHANNEL_HINTS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: add a regex for "Artist - Title" pattern
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: add a denylist for obvious non-music (podcast, vlog, tutorial keywords)
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ml_classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: load huggingface zero-shot pipeline once, reuse across calls
&lt;/span&gt;    &lt;span class="c1"&gt;#       (loading it per-call will make this painfully slow)
&lt;/span&gt;    &lt;span class="c1"&gt;# candidate_labels = ["music video", "podcast", "tutorial", "vlog", "other"]
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_artist_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: try a regex like r"^(.*?)\s*-\s*(.*?)(\(|\[|$)"
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: fall back to None, None if it doesn't match -- don't force a bad guess
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Milestone:&lt;/strong&gt; run the classifier over a week's worth of real watch history. Print a summary: "Classified 42 videos: 31 music, 11 other." Manually eyeball 10 of them -- if your accuracy feels roughly 80%+, move on; ML classifiers don't need to be perfect, because Phase 5's clustering step will naturally down-weight noise. Perfectionism here is a trap -- resist polishing this past "good enough to build on."&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 3: The Forensic Lab -- Audio &amp;amp; Metadata Enrichment (Weeks 7-10)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; For every confirmed music video, extract audio and metadata rich enough to power mood tagging and similarity search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills gained:&lt;/strong&gt; Working with &lt;code&gt;yt-dlp&lt;/code&gt;, basic digital signal processing (spectrograms, mel scales), running a pretrained audio embedding model, external metadata APIs (MusicBrainz or similar), and your first taste of "architecture decision with a real trade-off."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pipeline step that, given a &lt;code&gt;video_id&lt;/code&gt;, downloads (or streams) audio using &lt;code&gt;yt-dlp&lt;/code&gt;, for &lt;strong&gt;local processing only&lt;/strong&gt; -- not redistribution. This is the moment to have your privacy/ethics conversation with yourself; more in Chapter 4.&lt;/li&gt;
&lt;li&gt;A function that converts raw audio into a &lt;strong&gt;mel spectrogram&lt;/strong&gt; -- the standard "image-like" representation that lets you use image-flavored models (CNNs) on sound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture decision point, spelled out for you:&lt;/strong&gt; you could (a) use a pretrained audio embedding model like YAMNet or OpenL3 to get a general-purpose embedding vector per track with zero training, or (b) train a small CNN yourself on your own labeled data (mood-tagged tracks) to get embeddings tuned to &lt;em&gt;your&lt;/em&gt; taste categories. (a) is faster, works immediately, and is what you should ship first. (b) is the better learning experience and gives you a genuinely custom system -- a great Phase 3.5 stretch goal once (a) is working end to end. Don't skip straight to (b); you'll spend three weeks on a training loop for a payoff you could've had in three days.&lt;/li&gt;
&lt;li&gt;Store the resulting embedding vector in a &lt;code&gt;pgvector&lt;/code&gt; column on your &lt;code&gt;tracks&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;A metadata enrichment step: look up the resolved artist/title against MusicBrainz's API (free, no key needed for light use) to pull canonical artist name, genre tags if available, and release year.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learning pit stops:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 min video: search "mel spectrogram explained simply" -- you need the intuition (frequency content over time, perceptually weighted), not the full DSP math.&lt;/li&gt;
&lt;li&gt;Read the &lt;code&gt;yt-dlp&lt;/code&gt; README's "Usage and Options" section, specifically audio extraction flags (&lt;code&gt;-x --audio-format&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Skim: YAMNet's model page on TensorFlow Hub -- its usage example is short and you can adapt it directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skeleton:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ml/audio_features.py -- the forensic lab bench
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yt_dlp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;librosa&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;download_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: configure yt_dlp options for audio-only extraction
&lt;/span&gt;    &lt;span class="c1"&gt;# ydl_opts = {'format': 'bestaudio/best', 'outtmpl': f'{out_dir}/%(id)s.%(ext)s', ...}
&lt;/span&gt;    &lt;span class="c1"&gt;# NOTE: this is for local, personal analysis only -- see Chapter 4
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_mel_spectrogram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: y, sr = librosa.load(audio_path)
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: convert to log scale (librosa.power_to_db) -- raw mel values
&lt;/span&gt;    &lt;span class="c1"&gt;#       span orders of magnitude, log scale is what models expect
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: load a pretrained model (YAMNet via tensorflow_hub, or
&lt;/span&gt;    &lt;span class="c1"&gt;#       openl3.get_audio_embedding) and run inference
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: pool frame-level embeddings into a single fixed-length vector
&lt;/span&gt;    &lt;span class="c1"&gt;#       (mean pooling is a fine first choice)
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Architecture decision point (data storage):&lt;/strong&gt; store raw audio permanently, or delete it after extracting the embedding? Storing it burns disk and raises copyright/privacy questions for no real benefit -- you only need the embedding, not the waveform, for everything downstream. &lt;strong&gt;Recommendation:&lt;/strong&gt; extract-then-delete, keep only the derived vector. Write this down as a design principle now; it'll save you a difficult conversation with yourself in Month 4 when your disk is full of MP3s you don't need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Milestone:&lt;/strong&gt; pick five songs you know well, run them through the pipeline, and eyeball their embeddings' pairwise cosine similarity. Two songs by the same artist in the same genre should be &lt;em&gt;closer&lt;/em&gt; to each other than to a wildly different genre. If your embeddings pass this smell test, you have a forensic lab that actually works. Screenshot the similarity matrix -- this is the moment the system starts to have &lt;em&gt;taste&lt;/em&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 4: The Behavioral Profiler -- Learning What You Actually Like (Weeks 11-14)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Turn raw watch behavior into an implicit "how much did you like this" score, and build your first learned model of taste.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills gained:&lt;/strong&gt; Implicit feedback theory, feature engineering from event logs, neural collaborative filtering, thinking in terms of "signal vs noise" in behavioral data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A feature engineering step that, per track, computes: percent watched, replay count within a session, skip-within-first-10-seconds flag, time-of-day distribution, day-of-week distribution.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;implicit score&lt;/strong&gt; -- not a 1-5 star rating (you never gave one), but a derived number. A simple, honest starting formula:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;implicit_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;completion_ratio&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;replay_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;early_skip_penalty&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;completion_ratio = watched_seconds / video_duration&lt;/code&gt;, capped at 1.0, &lt;code&gt;early_skip_penalty&lt;/code&gt; is 1 if you skipped within the first 10 seconds and never returned, else 0, and &lt;code&gt;w1, w2, w3&lt;/code&gt; are weights you pick by hand at first (try &lt;code&gt;w1=1.0, w2=0.5, w3=0.8&lt;/code&gt;) and refine once you have enough data to eyeball whether the resulting rankings feel right. This is implicit feedback in a nutshell: you're inferring preference from &lt;em&gt;behavior&lt;/em&gt;, not asking for it directly, and every implicit signal is a proxy, not ground truth -- a song you love but skip because you're not in the mood right now looks identical, in raw data, to a song you dislike. Time-of-day and mood context (Phase 5) is what starts to disambiguate the two.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The math of implicit feedback, briefly:&lt;/strong&gt; classic explicit recommender systems (star ratings) treat missing data as "unknown." Implicit feedback systems (like this one) treat &lt;em&gt;everything&lt;/em&gt; as a signal -- even the absence of a play is information (you had the chance to watch it and didn't). The seminal framing here, worth reading about, is "confidence-weighted preference": you're not just modeling &lt;em&gt;whether&lt;/em&gt; you like something, you're modeling how &lt;em&gt;confident&lt;/em&gt; the system should be in that preference, where confidence scales with how many times you've interacted with a track.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A first neural collaborative filtering sketch&lt;/strong&gt;, once you have a few hundred (track, implicit_score, context) rows. Neural CF replaces the classic matrix-factorization approach (learn a vector per user, a vector per item, dot-product them to predict preference) with small embedding layers you can extend to include context features (time of day, mood tag) alongside the learned latent vectors.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learning pit stops:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;25 min: search "implicit feedback recommender systems explained" -- get comfortable with the idea that "no interaction" is still data.&lt;/li&gt;
&lt;li&gt;Skim: Keras's &lt;a href="https://keras.io/api/layers/core_layers/embedding/" rel="noopener noreferrer"&gt;Embedding layer docs&lt;/a&gt; -- this is the core building block of neural CF.&lt;/li&gt;
&lt;li&gt;Optional deeper dive (30 min): the original "Collaborative Filtering for Implicit Feedback Datasets" paper's abstract and intro -- you don't need the full math, just the framing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skeleton (Keras):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ml/behavior_model.py -- teaching the system your taste, one skip at a time
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tensorflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorflow.keras&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_neural_cf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_tracks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding_dim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;track_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context_features&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# e.g. hour_of_day, day_of_week, recent_genre_onehot...
&lt;/span&gt;
    &lt;span class="n"&gt;track_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_tracks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding_dim&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;track_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;track_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Flatten&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;&lt;span class="n"&gt;track_embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# TODO: concatenate track_vec with context_input
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: pass through 1-2 Dense layers with ReLU
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: final Dense(1, activation='sigmoid') to predict a
&lt;/span&gt;    &lt;span class="c1"&gt;#       normalized implicit_score in [0, 1]
&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;track_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_input&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neural_cf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_implicit_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;completion_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watched_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;replay_bonus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# diminishing returns
&lt;/span&gt;    &lt;span class="n"&gt;early_skip_penalty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;early_skip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completion_ratio&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;replay_bonus&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;early_skip_penalty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Architecture decision point:&lt;/strong&gt; do you train one global model, or treat this as pure feature engineering with no trained model at all (just weighted heuristics)? With a personal dataset of maybe a few thousand tracks and a few weeks of history, you may not have enough data for a neural model to meaningfully outperform a well-tuned heuristic score. &lt;strong&gt;Recommendation:&lt;/strong&gt; ship the heuristic first (it's basically free), and treat the neural CF model as a learning exercise you compare &lt;em&gt;against&lt;/em&gt; the heuristic -- a great "does my fancy model actually beat my simple baseline?" experiment, which is one of the most important habits in applied ML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Milestone:&lt;/strong&gt; for a handful of tracks you have strong feelings about, check whether your implicit score ranks them the way you'd expect -- your most-replayed song should sit near the top, something you skipped every time near the bottom. If the ranking basically matches your gut, you've built a taste profile that didn't exist an hour ago in any explicit form.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 5: The Case Board -- Clustering &amp;amp; Playlist Generation (Weeks 15-18)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Group tracks into meaningful clusters and turn each cluster into a named, explainable playlist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills gained:&lt;/strong&gt; Unsupervised clustering (HDBSCAN, K-Means), cluster labeling strategies, combining embeddings from different modalities (audio + behavior + text mood tags), scheduling recurring jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A combined feature vector per track: concatenate (or weighted-sum) the audio embedding, a mood-tag one-hot/multi-hot vector (from zero-shot classification against labels like "chill," "energetic," "sad," "focus," "romantic," "aggressive"), and behavioral context features (average time-of-day, day-of-week distribution).&lt;/li&gt;
&lt;li&gt;A clustering pass using &lt;strong&gt;HDBSCAN&lt;/strong&gt; over that combined vector space. Why HDBSCAN over plain K-Means: K-Means forces every point into a cluster and requires you to pre-specify the number of clusters -- a bad fit here, because you genuinely don't know how many "moods" exist in your listening history, and forcing an odd one-off track into a cluster it doesn't belong in produces bad playlists. HDBSCAN discovers the number of clusters itself and explicitly labels outliers as noise (cluster = -1) rather than cramming them somewhere.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;cluster-labeling step&lt;/strong&gt;: for each cluster, look at the most common mood tags among its members, the most common time-of-day, and generate a human name via simple rules ("if dominant mood is 'chill' and dominant time is evening -&amp;gt; name candidates: 'Evening Chill', 'Wind Down'"). This is a fun place to eventually swap in an LLM call ("given these five representative songs and these mood tags, suggest a playlist name") as a stretch goal.&lt;/li&gt;
&lt;li&gt;Specialized playlist types layered on top of the general clustering:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most-played:&lt;/strong&gt; pure sort by implicit_score / play_count, no clustering needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mood playlists:&lt;/strong&gt; direct output of the HDBSCAN clusters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Study / focus:&lt;/strong&gt; filter tracks where the zero-shot classifier's "instrumental" or "low-vocal-density" signal is high and mood tag includes "calm" or "focus" -- this is a good spot to add a simple vocal-presence heuristic (spectral features like zero-crossing rate can proxy for vocal presence without a dedicated model).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Travel:&lt;/strong&gt; filter by tracks played during sessions your extension can tag as "away from home" if you choose to add coarse geolocation (optional, privacy-sensitive -- see Chapter 4) -- or more simply, tracks played during longer, uninterrupted sessions (a proxy for commutes or trips).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; filter using a language-detection pass over the (translated or original) lyrics/title text, using a lightweight library like &lt;code&gt;langdetect&lt;/code&gt; or a Hugging Face language-ID model.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learning pit stops:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 min: read HDBSCAN's own documentation "How HDBSCAN Works" page -- it's written for exactly this level of newcomer and has excellent visuals.&lt;/li&gt;
&lt;li&gt;Skim: scikit-learn's clustering comparison page to see K-Means vs DBSCAN vs HDBSCAN side by side on toy datasets -- seeing the failure modes visually will save you hours of confusion later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skeleton:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ml/cluster.py -- the case board, where scattered evidence becomes a story
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hdbscan&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_combined_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_emb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mood_vec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;behavior_features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: consider normalizing each component before concatenating --
&lt;/span&gt;    &lt;span class="c1"&gt;#       otherwise a component with larger raw magnitude (e.g. audio_emb)
&lt;/span&gt;    &lt;span class="c1"&gt;#       will dominate the distance metric HDBSCAN uses
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cluster_tracks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_cluster_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;clusterer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hdbscan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HDBSCAN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_cluster_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;min_cluster_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;euclidean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clusterer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit_predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# labels == -1 means "noise" -- HDBSCAN is telling you this track
&lt;/span&gt;    &lt;span class="c1"&gt;# doesn't fit cleanly anywhere yet. That's useful information, not a bug.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;name_cluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track_rows_in_cluster&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: find most common mood tag, most common time-of-day bucket
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: map (mood, time_bucket) -&amp;gt; candidate name via a lookup table
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: fall back to "Mix #&amp;lt;id&amp;gt;" if nothing matches cleanly
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Architecture decision point:&lt;/strong&gt; re-cluster from scratch every time, or incrementally assign new tracks to existing clusters? Full re-clustering is simpler to reason about and is what you should build first -- run it nightly over your whole track history. Incremental assignment (using each cluster's centroid or a trained classifier) is faster and preserves playlist identity over time (your "Monday Chill" playlist doesn't get renamed every week), and is a great optimization once the naive approach is working and you notice it's slow or that playlist churn is annoying you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Milestone:&lt;/strong&gt; run the full pipeline end to end and get back a JSON structure of named playlists with real tracks in them. Take a screenshot of the raw output -- you now have an AI DJ's set list, even before there's a frontend to look at it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 6: The Broadcast -- Web Player &amp;amp; Full Integration (Weeks 19-22)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; A React frontend that displays your playlists and actually plays music, wired end-to-end to everything you've built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills gained:&lt;/strong&gt; React component architecture, working with an audio player (HTML5 &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; or the YouTube IFrame API), API integration patterns, and the specific discipline of wiring together five already-working pieces without breaking any of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;GET /playlists&lt;/code&gt; endpoint that returns your generated playlists with track metadata.&lt;/li&gt;
&lt;li&gt;A React app with a playlist grid, a track list per playlist, and a persistent bottom player bar (the "Spotify-like" feel).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playback, decision point spelled out:&lt;/strong&gt; you have two real options. (a) Embed the YouTube IFrame Player API and drive it programmatically -- simplest, fully compliant with YouTube's terms, but visually and functionally you're still "a YouTube player with a nicer wrapper," and you can't easily do gapless playback or true background audio. (b) Serve audio you've extracted yourself, from your own backend, for genuinely personal, non-redistributed use -- gives you a true from-scratch audio player experience (waveforms, gapless transitions, real &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element control) but carries real copyright and terms-of-service responsibilities you must take seriously and keep strictly personal/local, never exposed publicly. &lt;strong&gt;Recommendation:&lt;/strong&gt; build the IFrame API version first, since it's unambiguously fine to build and ship as a personal tool, and treat the self-hosted audio player as an explicitly-labeled "local-only, for-my-eyes-only experiment" if you choose to explore it, never something you deploy publicly.&lt;/li&gt;
&lt;li&gt;A "similar tracks" widget: given the currently playing track's embedding, query &lt;code&gt;pgvector&lt;/code&gt; for nearest neighbors (&lt;code&gt;ORDER BY embedding &amp;lt;-&amp;gt; :current_embedding LIMIT 5&lt;/code&gt;) and render them as suggestions.&lt;/li&gt;
&lt;li&gt;Wire the frontend's play events back into the extension's event pipeline (Chapter 1's feedback loop closes here) -- plays that originate in your own app should count as behavioral signal too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learning pit stops:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 min: the YouTube IFrame Player API "Getting Started" guide -- focus on &lt;code&gt;loadVideoById&lt;/code&gt;, &lt;code&gt;playVideo&lt;/code&gt;, and the &lt;code&gt;onStateChange&lt;/code&gt; event.&lt;/li&gt;
&lt;li&gt;Skim: any short React hooks refresher if it's been a while (&lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useContext&lt;/code&gt; for a simple global "currently playing" state).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skeleton:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontend/src/components/Player.jsx -- the stage where the case gets solved&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;currentTrack&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playerRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;currentTrack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO: if using YouTube IFrame API:&lt;/span&gt;
    &lt;span class="c1"&gt;//   playerRef.current.loadVideoById(currentTrack.video_id);&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO: log a 'play' event back to your /events endpoint here,&lt;/span&gt;
    &lt;span class="c1"&gt;//       reusing the same schema Phase 1 established&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentTrack&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"player-bar"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* TODO: track title, artist, progress bar */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* TODO: mount the YouTube IFrame player (hidden or small) here */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# backend/main.py (addition) -- serving the case board to the courtroom
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/playlists&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_playlists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: query your clusters/playlists table
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: for each playlist, include a handful of representative tracks
&lt;/span&gt;    &lt;span class="c1"&gt;#       and the playlist's dominant mood tags for display
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tracks/{track_id}/similar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;similar_tracks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: SELECT * FROM tracks ORDER BY embedding &amp;lt;-&amp;gt; (
&lt;/span&gt;    &lt;span class="c1"&gt;#         SELECT embedding FROM tracks WHERE id = :track_id
&lt;/span&gt;    &lt;span class="c1"&gt;#       ) LIMIT :limit
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Milestone -- the big one:&lt;/strong&gt; open your app, see real named playlists built from your real YouTube behavior, click play, and hear music. Take a screenshot of "Monday Chill" (or whatever your system named it) with real tracks in it. This is the moment described in the opening story finally closes the loop -- you are, quite literally, looking at the output of a detective story you wrote about yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 3: The Brain of the System — Meet Your Neural Concierge
&lt;/h2&gt;

&lt;p&gt;Phases give you a build order. This chapter zooms into the &lt;em&gt;thinking tools&lt;/em&gt; you'll reach for repeatedly — the habits of mind that separate "I copied a tutorial" from "I understand what I built."&lt;/p&gt;

&lt;h3&gt;
  
  
  How to search, read docs, and debug like you've done this before
&lt;/h3&gt;

&lt;p&gt;You haven't done this before, and that's fine — nobody starts out knowing how to google effectively for a niche integration. Here's the actual process, made explicit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name the smallest unit of your confusion.&lt;/strong&gt; Not "how do I do audio classification" — that's a whole field. Instead: "how do I get a fixed-length vector out of a variable-length audio file using a pretrained model." That sentence is close to a good search query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search for the library name plus the verb, not the concept plus the verb.&lt;/strong&gt; "yt-dlp extract audio python" beats "how to download youtube audio." Library-specific queries land you on docs and GitHub issues where someone already hit your exact wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the &lt;em&gt;signature&lt;/em&gt; before the prose.&lt;/strong&gt; When you land on a function's docs, look at its parameters and return type first. Half the time that alone answers your question faster than the paragraph above it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you're stuck for more than 20-30 minutes, write down what you expected to happen and what actually happened, in one sentence each.&lt;/strong&gt; This is the single highest-leverage debugging habit there is — most of the time, writing that sentence reveals the bug before you've even searched anything, because it forces you to notice the gap between your mental model and reality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce in isolation.&lt;/strong&gt; If your pipeline breaks on the audio-embedding step, don't debug it inside the full nightly job — copy the failing call into a throwaway script or a notebook cell with one hardcoded input. You want the shortest possible loop between "change something" and "see the result."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll use this loop dozens of times across the next five months. It's worth more than any specific fact in this guide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trade-off thinking, one more time with feeling
&lt;/h3&gt;

&lt;p&gt;Every phase above included at least one "architecture decision point," and that's not decoration — it's the actual skill this project is teaching you. Real systems are built from a sequence of decisions where there's no objectively correct answer, only a &lt;em&gt;right answer for your constraints&lt;/em&gt;. The questions to ask yourself every time are the same four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What does each option cost me right now&lt;/strong&gt; (time, complexity, new concepts to learn)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What does each option cost me later&lt;/strong&gt; (technical debt, rework, scaling pain)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I actually have the problem this option solves,&lt;/strong&gt; or am I solving a problem I imagine having?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can I try the cheap option first and upgrade later&lt;/strong&gt;, or is this a decision that's expensive to reverse?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice that almost every recommendation in this guide follows the same shape: start with the boring, cheap, well-understood option; treat the fancy option as a deliberate, isolated upgrade once you've felt the limits of the simple one. That's not a lack of ambition — it's how you make sure the ambition is spent on the parts of the project that are genuinely novel (your specific taste model, your specific clustering logic) instead of getting burned on generic infrastructure choices that a thousand tutorials have already solved for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  NLP for mood: zero-shot first, fine-tuning as a deliberate upgrade
&lt;/h3&gt;

&lt;p&gt;You'll use Hugging Face's &lt;code&gt;zero-shot-classification&lt;/code&gt; pipeline in two places: initial content classification (Phase 2) and mood tagging (Phase 5). It's worth understanding &lt;em&gt;why&lt;/em&gt; zero-shot works at all — the underlying model (typically an NLI, natural-language-inference, model like BART-MNLI) was trained to judge whether one sentence entails another. Zero-shot classification is a clever repurposing: it turns "does this song feel energetic?" into "does the premise 'this text describes energetic music' entail the hypothesis built from your candidate label," and ranks labels by entailment confidence. You get calibrated-&lt;em&gt;ish&lt;/em&gt; multi-label scores over any label set you dream up, with zero training.&lt;/p&gt;

&lt;p&gt;The catch: zero-shot models are working from &lt;em&gt;text&lt;/em&gt; — titles, descriptions, maybe lyrics if you fetch them — not from the audio itself. A song titled "Sunny Day Vibes" will zero-shot-classify as upbeat even if the actual track is a somber ballad using the title ironically. This is exactly why Phase 5's combined vector blends the zero-shot mood tags &lt;em&gt;with&lt;/em&gt; the audio embedding — text tells you what the creator &lt;em&gt;labeled&lt;/em&gt; the song as, audio tells you what it actually &lt;em&gt;sounds&lt;/em&gt; like, and behavior tells you how &lt;em&gt;you&lt;/em&gt; actually responded to it. None of the three signals is trustworthy alone; together they triangulate something closer to the truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to fine-tune instead of relying on zero-shot:&lt;/strong&gt; once you've hand-corrected mood tags on maybe 200-300 tracks (a natural byproduct of using your own app and noticing "this is tagged wrong"), you have enough labeled data to fine-tune a small classifier — even a lightweight one trained on top of frozen text embeddings — specifically on &lt;em&gt;your&lt;/em&gt; vocabulary of moods and &lt;em&gt;your&lt;/em&gt; taste in how songs get labeled. This is a genuinely great Month 4-5 stretch goal: it's the moment your mood tagger stops being "a generic pretrained model" and starts being "a model that knows your ears."&lt;/p&gt;

&lt;h3&gt;
  
  
  The math of implicit feedback, one level deeper
&lt;/h3&gt;

&lt;p&gt;Section from Phase 4 gave you a working formula. Here's the conceptual core worth sitting with: in explicit feedback (star ratings), the model's job is to predict a rating, and the loss function punishes wrong ratings directly. In implicit feedback, there's no rating to predict — only binary or continuous signals of &lt;em&gt;engagement&lt;/em&gt;. The standard reframe (going back to Hu, Koren &amp;amp; Volinsky's foundational work) is to treat every (user, item) pair as having a binary preference (did they like it, roughly, yes/no) plus a &lt;em&gt;confidence&lt;/em&gt; in that preference, where confidence grows with how much engagement evidence you've observed. A song you've watched once for 10 seconds contributes a weak, low-confidence data point. A song you've replayed eleven times contributes a strong, high-confidence one. Your loss function (if you get to training a real model) should weight errors on high-confidence pairs more heavily than errors on low-confidence ones — getting a well-established favorite's ranking wrong is a bigger problem than getting an ambiguous one-play track wrong.&lt;/p&gt;

&lt;p&gt;Practically, for a personal-scale project, you don't need to implement the full weighted matrix factorization machinery to benefit from this idea — just make sure your implicit_score formula and your neural CF's context features both account for "how many times have I actually interacted with this" as a first-class signal, not an afterthought.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neural collaborative filtering, sketched a little further
&lt;/h3&gt;

&lt;p&gt;The Phase 4 skeleton gave you the shape: an embedding layer per track, concatenated with hand-engineered context features, passed through dense layers to a final prediction. A few things worth understanding about &lt;em&gt;why&lt;/em&gt; this works before you write more of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The embedding layer is learning a compressed "taste coordinate"&lt;/strong&gt; for each track — not from any explicit attribute you gave it, but purely from patterns in which tracks tend to get high implicit scores together, in similar contexts. Two tracks can end up with similar embeddings even if their audio sounds nothing alike, if you consistently listen to them in the same moods/times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is a genuinely different signal from the audio embedding.&lt;/strong&gt; Audio embeddings capture "sounds like." CF embeddings capture "gets consumed like." A great expansion once both exist: concatenate them (as Phase 5 does) so your clusters reflect both what things sound like &lt;em&gt;and&lt;/em&gt; how you actually use them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With a small personal dataset, expect this model to overfit fast.&lt;/strong&gt; That's not a failure — it's expected, and it's a good opportunity to learn regularization basics (dropout between dense layers, L2 weight decay on the embedding) and to practice train/validation splitting even when your dataset is uncomfortably small. Keep a held-out slice of your most recent week's data and check whether the model's predicted scores actually track your real subsequent behavior — that's your only honest signal that any of this is working.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Chapter 4: The Data Story — Where Evidence Lives
&lt;/h2&gt;

&lt;p&gt;Every detective needs an evidence locker with a sane filing system. Here's yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  The schema, roughly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;raw_events&lt;/code&gt;&lt;/strong&gt; — the unfiltered sensor feed. Every &lt;code&gt;play&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;seeked&lt;/code&gt;, &lt;code&gt;progress&lt;/code&gt;, &lt;code&gt;ended&lt;/code&gt; tick from the extension, tied to a &lt;code&gt;video_id&lt;/code&gt; and &lt;code&gt;session_id&lt;/code&gt;. This table only grows; you never edit it, only append. Treat it the way you'd treat a security camera's footage — the raw record, kept intact, that every later interpretation can be checked against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tracks&lt;/code&gt;&lt;/strong&gt; — one row per resolved song: &lt;code&gt;video_id&lt;/code&gt;, &lt;code&gt;artist&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;is_music&lt;/code&gt; (bool), &lt;code&gt;duration_seconds&lt;/code&gt;, &lt;code&gt;audio_embedding&lt;/code&gt; (a &lt;code&gt;pgvector&lt;/code&gt; column), &lt;code&gt;mood_tags&lt;/code&gt; (array or jsonb), &lt;code&gt;language&lt;/code&gt;, &lt;code&gt;musicbrainz_id&lt;/code&gt; (nullable), &lt;code&gt;implicit_score&lt;/code&gt; (recomputed periodically), &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sessions&lt;/code&gt;&lt;/strong&gt; — optional but genuinely useful: groups of events with a shared &lt;code&gt;session_id&lt;/code&gt;, with derived fields like &lt;code&gt;session_start&lt;/code&gt;, &lt;code&gt;session_length&lt;/code&gt;, &lt;code&gt;dominant_time_bucket&lt;/code&gt; (morning/afternoon/evening/night). This is where "travel" and "study" playlist heuristics get their signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;playlists&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;playlist_tracks&lt;/code&gt;&lt;/strong&gt; — the output of the clustering step: a playlist has a name, a generation timestamp, dominant mood tags, and a join table linking it to track rows, ordered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key design idea: &lt;strong&gt;raw data is immutable, derived data is regenerable.&lt;/strong&gt; If your classification logic improves in Month 4, you should be able to delete and recompute the &lt;code&gt;tracks&lt;/code&gt; table's &lt;code&gt;is_music&lt;/code&gt; and &lt;code&gt;mood_tags&lt;/code&gt; columns from &lt;code&gt;raw_events&lt;/code&gt; without losing anything real. This is the same principle behind "extract-then-delete" for audio from Phase 3 — keep the cheap-to-regenerate stuff disposable, keep the expensive-to-recreate stuff (your actual watch history) sacred.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bootstrapping with Google Takeout
&lt;/h3&gt;

&lt;p&gt;Your extension only sees behavior from the moment you install it forward. If you want historical depth on day one, Google Takeout's YouTube history export gives you a JSON/HTML dump of your watch history going back as far as YouTube has recorded it. It's coarser than extension data — no watch percentage, no skip detection, just "you watched this, at this timestamp" — but it's real, and it's enough to seed your &lt;code&gt;tracks&lt;/code&gt; table and get Phase 2's classifier something to chew on in week one, instead of waiting three weeks for organic data to accumulate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to use it:&lt;/strong&gt; request a Takeout export (JSON format), write a one-off import script that reads the export and inserts synthetic &lt;code&gt;raw_events&lt;/code&gt; rows with &lt;code&gt;event_type = 'legacy_watch'&lt;/code&gt; and no position data, then let Phase 2's classifier run over them exactly as it would over live data. Treat legacy rows as lower-confidence in your implicit-score formula — you don't know how much of the video was actually watched, so don't let a legacy "watched" event count as strongly as a fully-instrumented one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simulating data for development
&lt;/h3&gt;

&lt;p&gt;You don't want to wait around watching YouTube for hours just to test whether your clustering code handles 500 tracks reasonably. Write a small synthetic data generator early — even a rough one — that creates plausible-looking &lt;code&gt;raw_events&lt;/code&gt; rows: pick from a pool of real song titles (you can hand-curate 100-200 from your own taste, spanning a few genres/moods), assign random-but-plausible watch percentages and timestamps clustered around a few "mood personas" (a synthetic "you" who watches chill music at night and energetic music in the morning). This does two things: it lets you develop and test the ML/clustering code without waiting on real data, and it gives you a &lt;em&gt;known-answer&lt;/em&gt; test — since you control the synthetic personas, you can check whether your clustering actually recovers something close to them, which is a much stronger validation than "the code ran without crashing."&lt;/p&gt;

&lt;h3&gt;
  
  
  Privacy and local-first design
&lt;/h3&gt;

&lt;p&gt;This system is, by design, an intimate profile of your habits — what you listen to, when, how obsessively. A few commitments worth making explicit before you write a line of storage code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep it local by default.&lt;/strong&gt; Run Postgres on your own machine or a private instance you control, not a shared cloud service, unless you've deliberately decided to and secured it (auth, no public ports, encrypted connections).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never redistribute extracted audio.&lt;/strong&gt; Phase 3's "extract-then-delete" isn't just an engineering nicety — it's the difference between "a personal analysis tool" and "a tool with a copyright problem." Keep audio files ephemeral, process-and-discard, never served to anyone but the pipeline that needs them for a few seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be deliberate about optional signals like geolocation.&lt;/strong&gt; The "Travel" playlist idea in Phase 5 floated coarse geolocation as one possible signal. If you add it, make it opt-in, store it at low resolution (city-level, not GPS-precise), and be honest with yourself about whether the marginal playlist quality is worth the marginal privacy cost. The session-length proxy (no geolocation needed) gets you most of the value for none of the risk — a good default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are both the engineer and the subject.&lt;/strong&gt; That's unusual and worth sitting with: normally, privacy-by-design is about protecting &lt;em&gt;other people's&lt;/em&gt; data from &lt;em&gt;your&lt;/em&gt; system. Here, you're the one whose intimate behavioral data is on the line, which makes it a genuinely good, low-stakes place to practice privacy-conscious engineering habits before you ever build something that touches anyone else's data.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Chapter 5: The Galaxy of Songs — Playlist Generation Magic
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Embeddings, explained like you're teaching a friend at a bar
&lt;/h3&gt;

&lt;p&gt;Imagine every song as a point of light in a vast dark space. Songs that sound alike, or that you tend to listen to in similar moods, drift close together — not because anyone placed them there by hand, but because the math that produced their coordinates was trained (or, for pretrained models, trained by someone else on a huge amount of audio) to put "things that behave similarly" near each other and "things that behave differently" far apart. That's an embedding: a long list of numbers (say, 128 or 512 of them) that represents a song not as an ID, but as a &lt;em&gt;position in a meaningful space&lt;/em&gt;. Two songs' embeddings being close together, measured by something like cosine similarity, is a proxy for "these songs are alike in whatever sense the model was trained to notice."&lt;/p&gt;

&lt;p&gt;You have two galaxies in this project: the &lt;strong&gt;audio galaxy&lt;/strong&gt; (positions determined by what things sound like) and the &lt;strong&gt;behavioral galaxy&lt;/strong&gt; (positions determined by how you actually consume them, learned via the collaborative-filtering embeddings from Chapter 3). A track's true "meaning" in your system lives at the intersection — which is exactly why Phase 5 concatenates both into one combined vector before clustering. A cluster in that combined space isn't just "songs that sound similar," and it isn't just "songs you listen to in similar contexts" — it's songs that are similar on &lt;em&gt;both&lt;/em&gt; axes simultaneously, which is a much stronger, more personally meaningful notion of "these belong on the same playlist" than either signal alone could produce.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clustering and automatic labeling, tied together
&lt;/h3&gt;

&lt;p&gt;HDBSCAN doesn't know what a "mood" is. It only knows distances. The intelligence in the system comes from &lt;em&gt;what you fed it&lt;/em&gt; (the combined embedding) and &lt;em&gt;how you interpret its output&lt;/em&gt; (the labeling heuristic). This division of labor is worth internalizing as a general ML pattern: &lt;strong&gt;unsupervised algorithms are excellent at finding structure, and terrible at explaining what the structure means&lt;/strong&gt; — that translation step is where your domain knowledge (you know what "chill" sounds like to you) has to do the work, usually through a simple, inspectable rule rather than another opaque model. Keep that labeling step simple and debuggable for as long as possible; it's the layer you'll be staring at and tweaking the most, because it's the layer that decides whether a playlist gets a name you'd actually want to click.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering for activity-based playlists
&lt;/h3&gt;

&lt;p&gt;"Study" and "Travel" playlists aren't clusters in the audio/behavior space so much as they're &lt;em&gt;filters&lt;/em&gt; layered on top of it — a study playlist is really "the calm, low-vocal-density cluster, further filtered to sessions where you historically kept the same track playing for a long uninterrupted stretch (a proxy for focus, since skip-heavy sessions suggest browsing, not working)." Treat these as compositions of your existing signals rather than new things to model from scratch — it's both less work and more explainable, which matters a lot when you're debugging why a jazz track ended up in "Study" and a moment's thought about "oh, right, low vocal density plus long uninterrupted plays" tells you exactly why.&lt;/p&gt;

&lt;h3&gt;
  
  
  A future reinforcement learning layer
&lt;/h3&gt;

&lt;p&gt;Once the system exists and you're using it daily, a natural next question emerges: instead of only &lt;em&gt;observing&lt;/em&gt; your behavior passively, could the system &lt;em&gt;learn from how you react to its own suggestions&lt;/em&gt;? This is where a lightweight RL framing becomes interesting — treat playlist generation as a policy that chooses which tracks to include, treat your subsequent behavior (did you skip the second track in "Monday Chill," did you let the whole playlist play through) as a reward signal, and use something like a contextual bandit (a simpler, more tractable cousin of full RL, well suited to problems where you're repeatedly choosing from a set of options and observing an immediate reward) to nudge future playlist composition toward what you actually engage with. This is explicitly a &lt;em&gt;later&lt;/em&gt; idea — Chapter 9 revisits it as a genuine "beyond the guide" project, because it depends on having a stable, working system generating playlists you actually use regularly first. RL on top of a system that doesn't have real usage data yet is RL on noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 6: The Web Dev Spine — Extension, Backend, Frontend, Tied Together
&lt;/h2&gt;

&lt;p&gt;The ML chapters get the glamour, but this is the skeleton everything else hangs on. A quick tour of the parts you haven't already built phase-by-phase, plus the connective tissue between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The extension, structurally
&lt;/h3&gt;

&lt;p&gt;A Chrome extension (Manifest V3) has three moving pieces you need to hold in your head as separate execution contexts that can't directly touch each other's variables — they only talk via message passing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;manifest.json&lt;/code&gt;&lt;/strong&gt; — declares permissions (&lt;code&gt;activeTab&lt;/code&gt;, host permissions for &lt;code&gt;*.youtube.com&lt;/code&gt;), which scripts run where, and whether you have a background service worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The content script&lt;/strong&gt; — runs &lt;em&gt;inside&lt;/em&gt; the YouTube page's context, so it can read the DOM and attach listeners to the &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element, but it's sandboxed from the extension's other state and can't make cross-origin requests as freely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The background service worker&lt;/strong&gt; — has broader permissions (can make network requests to your backend without CORS headaches, can persist state across tab navigations) but can't touch the page DOM directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The message-passing pattern you'll lean on constantly: content script calls &lt;code&gt;chrome.runtime.sendMessage({type: 'EVENT', payload: {...}})&lt;/code&gt;, background script listens via &lt;code&gt;chrome.runtime.onMessage.addListener&lt;/code&gt;, batches, and does the actual &lt;code&gt;fetch&lt;/code&gt; to your API. Get comfortable with this pattern early — Phase 1 depends on it, and every later extension feature (detecting SPA navigation between videos, badge icon updates showing tracking status) reuses it.&lt;/p&gt;

&lt;h3&gt;
  
  
  REST API design that won't fight you later
&lt;/h3&gt;

&lt;p&gt;A few conventions worth committing to from Phase 1 onward, because retrofitting them later is annoying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version your API from day one&lt;/strong&gt; (&lt;code&gt;/api/v1/events&lt;/code&gt;), even though you're the only consumer. Future-you, three months in, adding a mobile client or a second extension version, will thank present-you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep ingestion endpoints (&lt;code&gt;POST /events&lt;/code&gt;) fast and dumb.&lt;/strong&gt; They should do validation and a write, nothing else. Any classification, embedding, or clustering work belongs in the offline pipelines, not inline in the request path — mixing them means a slow ML call can make your extension feel laggy or drop events under load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design read endpoints (&lt;code&gt;GET /playlists&lt;/code&gt;, &lt;code&gt;GET /tracks/{id}/similar&lt;/code&gt;) around what the frontend actually needs to render&lt;/strong&gt;, not around your database schema. It's fine, even good, for an endpoint to join and reshape data server-side so the frontend doesn't have to stitch together three separate calls just to draw one screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Streaming audio, and the ethical note this section owes you
&lt;/h3&gt;

&lt;p&gt;If you go the self-hosted-audio route from Phase 6's decision point, you're technically capable of building a &lt;code&gt;/stream/{track_id}&lt;/code&gt; endpoint that reads an extracted audio file and streams it with proper &lt;code&gt;Range&lt;/code&gt; header support so the browser's &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element can seek. Build it if you want the learning experience of implementing HTTP range requests (a genuinely useful, transferable skill — it's the same mechanism video platforms use). But hold the line firmly: this stays a &lt;strong&gt;local, single-user, never-publicly-deployed&lt;/strong&gt; feature. The moment "personal tool running on my own machine for my own previously-watched content" turns into "a service anyone else can reach," you've crossed from a personal fair-use-adjacent experiment into redistribution, which is a different, real legal category. If you ever want to share this project publicly (a portfolio demo, a hackathon submission), disable or strip the self-hosted audio path and demo with the YouTube IFrame API version instead — it's not a downgrade, it's the responsible default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend architecture, briefly
&lt;/h3&gt;

&lt;p&gt;Keep state simple: a top-level &lt;code&gt;currentTrack&lt;/code&gt; and &lt;code&gt;currentPlaylist&lt;/code&gt; in React context (or a lightweight state library if you want the practice), a &lt;code&gt;PlaylistGrid&lt;/code&gt; component that fetches from &lt;code&gt;/playlists&lt;/code&gt; on mount, a &lt;code&gt;Player&lt;/code&gt; component that owns the actual playback element and logs playback events. The "similar tracks" widget is a nice small component to practice component composition on: it takes a &lt;code&gt;trackId&lt;/code&gt; prop, fetches &lt;code&gt;/tracks/{id}/similar&lt;/code&gt; on prop change, and renders a small horizontal list — self-contained enough to build and test in isolation before wiring it into the main player view.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tying the vector database to the frontend
&lt;/h3&gt;

&lt;p&gt;The satisfying part of &lt;code&gt;pgvector&lt;/code&gt; is that "find similar tracks" is &lt;em&gt;one query&lt;/em&gt;, not a separate service call to a vector database plus a join back to your relational metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tracks&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;current_track_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tracks&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;current_track_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single query is doing something that would otherwise require standing up and syncing two separate systems. It's a small thing, but it's the kind of small thing that, once you've felt the alternative, you start actively choosing simpler architectures for the rest of your career.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 7: The Detective's Logbook — Milestones Across the Whole Case
&lt;/h2&gt;

&lt;p&gt;You've hit a concrete milestone at the end of every phase in Chapter 2, but it's worth stepping back and seeing the whole arc as one continuous story, because that's genuinely what it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 0:&lt;/strong&gt; two independent heartbeats — an extension that logs, a server that responds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1:&lt;/strong&gt; the system sees you for the first time. A real, timestamped trail of your own behavior, sitting in a database, that didn't exist yesterday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2:&lt;/strong&gt; the system starts telling music apart from everything else — its first act of judgment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3:&lt;/strong&gt; the system develops senses beyond text — it can now "hear," in the limited but real sense of turning sound into a comparable, structured signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4:&lt;/strong&gt; the system develops a memory of your taste that isn't just "what you told it" but "what you actually did," which is a fundamentally more honest signal than any explicit rating system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 5:&lt;/strong&gt; scattered evidence becomes a story — songs organize themselves into groups you didn't manually create, with names that feel earned rather than assigned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 6:&lt;/strong&gt; the system finds its voice, and hands the story back to you in a form you can actually use, day to day, the way you'd use Spotify — except every playlist in it is an artifact of your own engineering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those is worth a screenshot, a commit message that actually describes what changed and why, and — genuinely — a moment of letting yourself feel like this is a big deal. It is. Most people who use recommendation systems every day have no idea how the black box works. You're about to have built one, from the sensor all the way to the speaker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 8: Beyond the Guide — Where the Case Reopens
&lt;/h2&gt;

&lt;p&gt;Five months from now, you'll have a working, personal AI DJ. Here's where the story can keep going, roughly in order of "natural next step" to "ambitious departure":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploy it somewhere always-on&lt;/strong&gt;, like a Raspberry Pi or a small home server, so the nightly pipeline runs whether or not your laptop is open. This is a good, contained systems-administration project on its own — cron jobs, systemd services, basic monitoring so you notice if the pipeline silently stops working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add voice control&lt;/strong&gt; — "play something chill" parsed via a small intent-classification step (zero-shot classification again, this time over your playlist names and mood tags) wired to a wake-word library or a simple push-to-talk button in the frontend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate with the real Spotify API&lt;/strong&gt; as a second data/playback source, letting your behavioral model learn from both platforms at once, and letting your generated playlists optionally get pushed &lt;em&gt;into&lt;/em&gt; actual Spotify playlists you can use anywhere, on any device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the reinforcement learning layer&lt;/strong&gt; sketched in Chapter 5 — once you have real usage data (skips, replays, completions) on the &lt;em&gt;system's own suggestions&lt;/em&gt;, not just your organic YouTube behavior, you have the reward signal that makes a bandit-style approach genuinely meaningful instead of speculative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn it into a SaaS&lt;/strong&gt;, if you want the entirely different (and genuinely valuable) experience of learning multi-tenant architecture, per-user data isolation, OAuth flows for other people's YouTube accounts, and all the "now it's not just your data anymore" privacy engineering that Chapter 4 flagged as a different category of problem. Don't take this step lightly or quickly — it's a good six-month project in its own right, layered on top of the five months you'll have already spent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are required to call the project "done." The five-month build is a complete story with a real ending. Everything in this chapter is a sequel hook, not an unfinished obligation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 9: The Final Note
&lt;/h2&gt;

&lt;p&gt;You started this case with an irritating, wonderful little observation: somewhere in your YouTube history was a pattern nobody, not even you, could see clearly. Over the next five months, you built the instrument to see it — a browser extension that learned to watch the way you watch, a pipeline that learned to hear the way songs sound, a model that learned your taste from what you actually did instead of what you said, and a clustering engine that turned all of that into playlists with names that feel like they know you, because in the most literal sense, they do.&lt;/p&gt;

&lt;p&gt;Along the way you picked up a working fluency in things that sound intimidating from the outside and turn out to be learnable in exactly the incremental way this guide walked you through: DOM events and extension messaging, REST API design, mel spectrograms and audio embeddings, zero-shot NLP classification, the actual math behind implicit feedback, neural collaborative filtering, density-based clustering, vector similarity search, and a real frontend audio player. Each of those, on its own, is a legitimate skill line on a resume. Together, wired into one living system, they're something rarer: a complete, working thing you built end to end, that solves a problem only you have, in a way that only you could have tuned it.&lt;/p&gt;

&lt;p&gt;You started with a YouTube habit. You're ending with a machine learning orchestra — one that watches, listens, remembers, and plays, quietly, in the background of your life, getting a little more &lt;em&gt;you&lt;/em&gt; every single day it runs.&lt;/p&gt;

&lt;p&gt;Case closed. Time to hit play.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>ChronoWeave: The Documentary</title>
      <dc:creator>Biki Kalita</dc:creator>
      <pubDate>Sun, 26 Jul 2026 06:53:09 +0000</pubDate>
      <link>https://dev.to/bikidev/chronoweave-the-documentary-3c31</link>
      <guid>https://dev.to/bikidev/chronoweave-the-documentary-3c31</guid>
      <description>&lt;h3&gt;
  
  
  &lt;em&gt;From Framework User to Framework Understander — A 4-Month Journey Through Gradient Descent, Transformers, and the Physics of History&lt;/em&gt;
&lt;/h3&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Every timeline you've ever seen is a lie of omission. It shows you WHEN things happened. It never shows you WHY. ChronoWeave is our attempt to fix that — and along the way, you're going to learn what actually happens inside a neural network when you call &lt;code&gt;.backward()&lt;/code&gt;."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How to Use This Guide
&lt;/h2&gt;

&lt;p&gt;This is not a tutorial you skim. It's a companion for a 3–5 month build. Read one section, do the exercise, hit the wall, climb over it, then come back. Every chapter follows the same rhythm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Concept&lt;/strong&gt; — the idea, explained with an analogy before a single line of code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Code Challenge&lt;/strong&gt; — a skeleton you fill in, never a finished solution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Aha Moment&lt;/strong&gt; — what you should &lt;em&gt;see&lt;/em&gt; when it clicks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Extension&lt;/strong&gt; — how this piece bolts onto the larger machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt; — exact docs, not "just Google it"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And threaded throughout:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧭 &lt;strong&gt;The Mentor Says&lt;/strong&gt; — the advice I wish someone had given me&lt;/li&gt;
&lt;li&gt;🕳️ &lt;strong&gt;The Rabbit Hole&lt;/strong&gt; — optional, for when curiosity gets the better of you&lt;/li&gt;
&lt;li&gt;😤 &lt;strong&gt;The Struggle&lt;/strong&gt; — the wall you will hit, named in advance&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Checkpoint&lt;/strong&gt; — stop and verify before you go further&lt;/li&gt;
&lt;li&gt;💡 &lt;strong&gt;The Innovation&lt;/strong&gt; — why this piece of the project is worth talking about in an interview&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's begin.&lt;/p&gt;




&lt;h1&gt;
  
  
  CHAPTER 0: The Grand Vision — Seeing the Complete Picture
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Problem: Why Historical Timelines Are Broken
&lt;/h2&gt;

&lt;p&gt;Open any history textbook's timeline. You'll see a horizontal line, some dots, some dates. 1914. 1917. 1929. 1939. It tells you &lt;em&gt;when&lt;/em&gt;. It is silent on &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The assassination of Archduke Franz Ferdinand didn't cause World War I in a vacuum — it was the spark that landed on a decade of alliance treaties, arms races, and colonial tension that had already turned the continent into dry kindling. A flat timeline shows you the spark. It hides the kindling.&lt;/p&gt;

&lt;p&gt;What historians actually think in is a &lt;strong&gt;causal graph&lt;/strong&gt;: event A enabled event B, which, combined with condition C, triggered event D. That graph is tangled, non-linear, and multi-dimensional — which is exactly why nobody draws it by hand. It's too much cognitive load.&lt;/p&gt;

&lt;p&gt;ChronoWeave's bet: if a machine can &lt;em&gt;extract&lt;/em&gt; the causal graph from raw text, and then &lt;em&gt;lay it out&lt;/em&gt; so that causally-connected events cluster together and pull each other into readable arrangement, you get something a static timeline can never give you — a map you can explore, drag, and reorganize, where the layout itself is meaningful. Events that influence each other stay near each other. That's not a UI nicety. That's the entire value proposition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: ChronoWeave's Architecture
&lt;/h2&gt;

&lt;p&gt;Here's the complete system, before we write a single line of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│                         THE USER'S BROWSER                           │
│  ┌──────────────┐        ┌────────────────────────────────────┐    │
│  │  Text Input   │──────▶│   React + D3.js Canvas               │    │
│  │  "Paste       │       │   (renders nodes = events,           │    │
│  │   history      │       │    edges = causal links)             │    │
│  │   here"        │       │                                       │    │
│  └──────────────┘        │   User drags a node ───────┐         │    │
│                            └─────────────────────────────┼─────────┘    │
└──────────────────────────────────────────────────────────┼──────────────┘
                    │ POST /extract                         │ WS: node_moved
                    ▼                                        ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         FASTAPI BACKEND                              │
│                                                                        │
│  ┌────────────────────┐     ┌───────────────────────────────────┐  │
│  │  EXTRACTION ENGINE   │     │   SPATIAL INTELLIGENCE ENGINE       │  │
│  │  (Chapter 2)          │     │   (Chapter 3 — the heart of it)     │  │
│  │                        │     │                                      │  │
│  │  1. Split into         │     │  1. Build a graph: nodes=events,    │  │
│  │     sentences          │     │     edges=causal relations          │  │
│  │  2. Run NER            │────▶│  2. Init x,y for every node as a    │  │
│  │     (BERT) to find     │     │     TRAINABLE TENSOR                │  │
│  │     events, dates,     │     │     (requires_grad=True)            │  │
│  │     people, places     │     │  3. Define "Map Clarity Loss":      │  │
│  │  3. Run Relation       │     │     - causally linked nodes should  │  │
│  │     Extraction to      │     │       be CLOSE                     │  │
│  │     find "A causes B"  │     │     - unrelated nodes should be     │  │
│  │  4. Output: list of    │     │       FAR (repulsion)               │  │
│  │     (event, date,      │     │     - nothing should overlap        │  │
│  │      relation, event)  │     │  4. Run backprop on the             │  │
│  │     triples            │     │     COORDINATES (not the model's    │  │
│  │                        │     │     weights!) using a hand-written  │  │
│  │                        │     │     Adam optimizer                  │  │
│  │                        │     │  5. When user drags a node, PIN     │  │
│  │                        │     │     that node's coords, re-run      │  │
│  │                        │     │     optimization on everyone else   │  │
│  └────────────────────────┘     └───────────────────────────────────┘  │
│              │                                    │                      │
│              ▼                                    ▼                      │
│  ┌────────────────────────────────────────────────────────────────┐   │
│  │           PostgreSQL + pgvector (stores triples + embeddings)    │   │
│  └────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Data Flow: What Happens When a User Clicks "Generate"
&lt;/h2&gt;

&lt;p&gt;Walk through it end to end, because this sequence is the skeleton every later chapter hangs off of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User pastes text&lt;/strong&gt; (say, three paragraphs on the causes of WWI) and clicks Generate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt; POSTs the raw text to &lt;code&gt;/api/extract&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend NER model&lt;/strong&gt; (a fine-tuned BERT) tags every token: is it part of an EVENT, a DATE, a PERSON, a PLACE? Output: a list of entity spans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend Relation Extraction model&lt;/strong&gt; looks at pairs of entities in the same or nearby sentences and predicts a relation label: &lt;code&gt;CAUSES&lt;/code&gt;, &lt;code&gt;ENABLED_BY&lt;/code&gt;, &lt;code&gt;PRECEDES&lt;/code&gt;, &lt;code&gt;PART_OF&lt;/code&gt;, or &lt;code&gt;NONE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The result is a set of &lt;strong&gt;triples&lt;/strong&gt;: &lt;code&gt;(Assassination of Franz Ferdinand, CAUSES, Austria-Hungary ultimatum to Serbia)&lt;/code&gt;. These get written to Postgres, and each event also gets a &lt;strong&gt;text embedding&lt;/strong&gt; stored via pgvector (this lets us later cluster thematically similar events even without an explicit extracted relation).&lt;/li&gt;
&lt;li&gt;The triples define a &lt;strong&gt;graph&lt;/strong&gt;: events are nodes, relations are edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Spatial Intelligence Engine&lt;/strong&gt; initializes every node at a random (x, y) position — as a PyTorch tensor with &lt;code&gt;requires_grad=True&lt;/code&gt;. This is the part that makes ChronoWeave unusual: the &lt;em&gt;coordinates themselves&lt;/em&gt; are the parameters being learned, not neural network weights.&lt;/li&gt;
&lt;li&gt;We define a custom loss function — the &lt;strong&gt;Map Clarity Loss&lt;/strong&gt; — that penalizes bad layouts: causally-linked nodes that are far apart, unrelated nodes that overlap, edges that cross each other unnecessarily.&lt;/li&gt;
&lt;li&gt;We run &lt;strong&gt;gradient descent&lt;/strong&gt; (a hand-written Adam optimizer) for a few hundred steps. Each step: compute the loss, call &lt;code&gt;.backward()&lt;/code&gt;, get gradients &lt;em&gt;with respect to the x,y coordinates&lt;/em&gt;, nudge the coordinates a little in the direction that reduces loss. Repeat.&lt;/li&gt;
&lt;li&gt;After convergence, the backend streams the final &lt;code&gt;(node_id, x, y)&lt;/code&gt; positions back to the frontend over the initial REST response (or WebSocket, for the live re-layout case).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React + D3.js&lt;/strong&gt; renders nodes at those coordinates, with edges drawn between causally-linked events, styled by relation type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The magic moment:&lt;/strong&gt; the user drags a node to a new spot because they &lt;em&gt;disagree&lt;/em&gt; with the layout, or just want to explore. That node's position gets pinned (&lt;code&gt;requires_grad=False&lt;/code&gt; for it, fixed at the drag target). The backend re-runs the optimizer on every &lt;em&gt;other&lt;/em&gt; node, live, streaming intermediate frames over a WebSocket, so the user watches the rest of the graph flow and resettle around their edit — like iron filings reorganizing around a magnet you just moved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step is the entire reason this project exists. A physics engine (force-directed graph layout, e.g. D3's built-in &lt;code&gt;forceSimulation&lt;/code&gt;) can &lt;em&gt;also&lt;/em&gt; do node repulsion and edge attraction — but it can't easily express arbitrary, learned, semantically-weighted objectives (e.g., "cluster by decade AND by causal chain AND penalize edge crossings, with weights that adapt based on graph density"). Gradient descent on a custom loss can. That's the pitch. We'll come back to defending it rigorously in Chapter 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technology Stack: Why Each Piece Was Chosen
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ML core&lt;/td&gt;
&lt;td&gt;PyTorch (from raw tensors, then &lt;code&gt;nn.Module&lt;/code&gt; later)&lt;/td&gt;
&lt;td&gt;You need autograd exposed at the tensor level to do coordinate optimization; PyTorch's dynamic graph makes this natural&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NER / Relation Extraction&lt;/td&gt;
&lt;td&gt;Fine-tuned BERT-base, later maybe REBEL for joint extraction&lt;/td&gt;
&lt;td&gt;BERT is small enough to fine-tune on a single GPU and well documented; REBEL is a strong upgrade path for joint entity+relation extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;FastAPI + WebSockets&lt;/td&gt;
&lt;td&gt;Async-native, typed, and WebSocket support is first-class for the live re-layout streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL + pgvector&lt;/td&gt;
&lt;td&gt;Relational structure for triples, vector column for semantic similarity search on event embeddings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React + D3.js&lt;/td&gt;
&lt;td&gt;D3 gives you full control over force/position rendering; React manages component state and drag events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Docker Compose → cloud (Render/Fly.io/AWS)&lt;/td&gt;
&lt;td&gt;Reproducibility first, then scale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Journey Ahead
&lt;/h2&gt;

&lt;p&gt;Four phases, each roughly a month, building strictly bottom-up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1 (Weeks 1–4):&lt;/strong&gt; You build the &lt;em&gt;math&lt;/em&gt; — autograd, manual backprop, hand-rolled optimizers, a toy Transformer. No frameworks doing the work for you yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2 (Weeks 5–8):&lt;/strong&gt; You build the &lt;em&gt;reader&lt;/em&gt; — BERT-based NER and relation extraction, a real data pipeline, a real database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3 (Weeks 9–12):&lt;/strong&gt; You build the &lt;em&gt;cartographer&lt;/em&gt; — the spatial intelligence engine. This is the conceptual core of the whole project and gets the most depth in this guide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4 (Weeks 13–16):&lt;/strong&gt; You build the &lt;em&gt;body&lt;/em&gt; — the full-stack app, the live drag-and-reoptimize loop, and you ship it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🧭 &lt;strong&gt;The Mentor Says:&lt;/strong&gt; Don't let the ambition of the final product distract you from the ugliness of the early steps. In week 2 you'll be manually computing gradients for a two-layer network with a pencil-and-paper feel to it, and it will seem impossibly far from "beautiful interactive graph app." That gap is supposed to be there. Every person who deeply understands PyTorch went through exactly this tunnel. You don't skip it by using &lt;code&gt;nn.Module&lt;/code&gt; early — you skip &lt;em&gt;understanding&lt;/em&gt; by doing that.&lt;/p&gt;




&lt;h1&gt;
  
  
  CHAPTER 1: The Foundation — Building Your Neural Engine
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;(Weeks 1–4 · Prerequisite: comfort with Python, basic linear algebra — matrix multiply, dot products — and derivatives from a first calculus course)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 1: Environment and the Shape of a Tensor
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;A tensor is just a multi-dimensional array with two superpowers bolted on: it knows how to run on a GPU, and it can remember the sequence of operations that produced it, so it can later compute how a tiny nudge to its inputs would change its output. That second superpower is &lt;strong&gt;autograd&lt;/strong&gt;, and it's the single idea Phase 1 exists to demystify.&lt;/p&gt;

&lt;p&gt;Think of a tensor with &lt;code&gt;requires_grad=True&lt;/code&gt; as a spreadsheet cell that doesn't just hold a number — it holds a number &lt;em&gt;and&lt;/em&gt; a formula referencing other cells. Change an input cell, and every downstream cell knows exactly how much it would change, without you re-deriving the formula by hand. That "how much would it change" is the gradient.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;

&lt;p&gt;Set up your environment first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry init
poetry add torch numpy matplotlib jupyter
poetry shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in a notebook, don't build anything yet — just &lt;em&gt;observe&lt;/em&gt; autograd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="c1"&gt;# TODO: create a tensor x = 3.0 with requires_grad=True
&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# TODO: create y = x**2 + 2*x + 1
&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# TODO: call y.backward() and print x.grad
# Predict on paper what dy/dx should be at x=3 BEFORE you run this
&lt;/span&gt;&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;x.grad&lt;/code&gt; should equal &lt;code&gt;2*x + 2 = 8.0&lt;/code&gt; — exactly the calculus-class derivative, computed automatically. The click isn't "wow it did calculus," it's realizing PyTorch didn't look up a symbolic formula for &lt;code&gt;x**2 + 2*x + 1&lt;/code&gt; — it recorded the &lt;em&gt;sequence of primitive operations&lt;/em&gt; (&lt;code&gt;pow&lt;/code&gt;, &lt;code&gt;mul&lt;/code&gt;, &lt;code&gt;add&lt;/code&gt;) as you executed them, and walked that sequence backward, applying the chain rule at each step. This recorded sequence is called the &lt;strong&gt;computation graph&lt;/strong&gt;, and it's rebuilt fresh every time you run forward — which is why PyTorch is called "define-by-run."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;Every gradient ChronoWeave ever computes — whether it's a loss with respect to a BERT weight, or a loss with respect to a node's (x, y) position on the canvas — goes through this exact mechanism. There is no different code path for "coordinates" versus "weights." This is the whole trick behind Phase 3, twelve weeks from now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PyTorch autograd tutorial: &lt;a href="https://docs.pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html" rel="noopener noreferrer"&gt;https://docs.pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🕳️ &lt;strong&gt;The Rabbit Hole:&lt;/strong&gt; Read about &lt;code&gt;torch.autograd.grad()&lt;/code&gt; vs &lt;code&gt;.backward()&lt;/code&gt; — the former lets you compute gradients without accumulating them into &lt;code&gt;.grad&lt;/code&gt;, important later when you want gradients with respect to &lt;em&gt;coordinates specifically&lt;/em&gt;, while leaving model weights untouched.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Checkpoint 1.1:&lt;/strong&gt; Explain, without looking anything up, why calling &lt;code&gt;.backward()&lt;/code&gt; twice on the same graph (without &lt;code&gt;retain_graph=True&lt;/code&gt;) throws an error. (Answer: the graph is freed after the first backward pass to save memory.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 2: Manual Forward and Backward — No &lt;code&gt;nn.Module&lt;/code&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;nn.Module&lt;/code&gt; is a convenience wrapper. Underneath, a "layer" is nothing more than: take an input, multiply by a weight matrix, add a bias, pass through a nonlinearity. A "network" is several of those chained. "Training" is: compute a loss, get its gradient with respect to every weight, nudge each weight opposite to its gradient. Building this by hand once is the single highest-leverage exercise in this project.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;

&lt;p&gt;Fit &lt;code&gt;y = sin(x)&lt;/code&gt; on &lt;code&gt;x&lt;/code&gt; in &lt;code&gt;[-π, π]&lt;/code&gt; with &lt;strong&gt;manual&lt;/strong&gt; parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;manual_seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Architecture: 1 -&amp;gt; 32 -&amp;gt; 1, tanh activation
&lt;/span&gt;&lt;span class="n"&gt;W1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: (1,32), requires_grad=True, small random init
&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: (32,)
&lt;/span&gt;&lt;span class="n"&gt;W2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: (32,1)
&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: (1,)
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: z1 = x @ W1 + b1 ; a1 = tanh(z1) ; z2 = a1 @ W2 + b2
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;z2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mse_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: mean squared error, raw tensor ops
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y_true&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mse_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: zero old grads, loss.backward(), manually update all 4 params
&lt;/span&gt;    &lt;span class="c1"&gt;#       (wrap update in `with torch.no_grad():`)
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Loss will &lt;strong&gt;not&lt;/strong&gt; decrease if you forget to zero gradients — PyTorch accumulates them into &lt;code&gt;.grad&lt;/code&gt; by default. Once burned by this, you'll never forget &lt;code&gt;zero_grad()&lt;/code&gt; again — you'll understand &lt;em&gt;why&lt;/em&gt;, not just cargo-cult it. Then plot &lt;code&gt;forward(x)&lt;/code&gt; against &lt;code&gt;sin(x)&lt;/code&gt;: a sine wave emerges from 4 matrices you initialized as noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;This loop — forward, loss, zero grad, backward, manual update — is structurally identical to the loop you write in Phase 3 for optimizing node coordinates. Only what the "parameters" represent changes.&lt;/p&gt;

&lt;p&gt;😤 &lt;strong&gt;The Struggle:&lt;/strong&gt; You'll likely hit &lt;code&gt;RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn&lt;/code&gt;, usually from overwriting a leaf tensor without &lt;code&gt;torch.no_grad()&lt;/code&gt;. Print &lt;code&gt;.requires_grad&lt;/code&gt; / &lt;code&gt;.is_leaf&lt;/code&gt; on every parameter right before the failing line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PyTorch neural network basics: &lt;a href="https://docs.pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html" rel="noopener noreferrer"&gt;https://docs.pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html&lt;/a&gt; (concepts only this week)&lt;/li&gt;
&lt;li&gt;3Blue1Brown, "Backpropagation calculus"&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Week 3: Hand-Rolled SGD, Momentum, RMSprop, and Adam
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;An optimizer answers: given a gradient, how exactly should I change the parameter? Plain SGD (&lt;code&gt;subtract lr*grad&lt;/code&gt;) works but is slow and oscillates on ravine-shaped losses.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Momentum&lt;/strong&gt; — running average of past gradients, damping oscillation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RMSprop&lt;/strong&gt; — running average of &lt;em&gt;squared&lt;/em&gt; gradients per parameter; divides the step by its square root, shrinking effective learning rate for consistently-large-gradient parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adam&lt;/strong&gt; — momentum + RMSprop, plus bias-correction for early steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ManualSGD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;momentum&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;momentum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;momentum&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: zeros_like buffer per param
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocities&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="c1"&gt;# TODO: v = momentum*v - lr*p.grad ; p += v
&lt;/span&gt;                &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ManualAdam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;betas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.999&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;betas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: zeros_like per param (first moment)
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: zeros_like per param (second moment)
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;
                &lt;span class="c1"&gt;# TODO: m[i] = b1*m[i] + (1-b1)*g
&lt;/span&gt;                &lt;span class="c1"&gt;# TODO: v[i] = b2*v[i] + (1-b2)*g**2
&lt;/span&gt;                &lt;span class="c1"&gt;# TODO: m_hat = m[i]/(1-b1**t) ; v_hat = v[i]/(1-b2**t)
&lt;/span&gt;                &lt;span class="c1"&gt;# TODO: p -= lr * m_hat / (sqrt(v_hat) + eps)
&lt;/span&gt;                &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-run the Week 2 sine-fit with &lt;code&gt;ManualAdam&lt;/code&gt;, compare convergence speed to vanilla SGD.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Adam converges in hundreds of steps where SGD needed thousands, and is far less sensitive to learning rate choice. This robustness is why you'll reach for it again in Phase 3, where the loss surface (Map Clarity Loss over 2D positions) is even messier.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;Your Phase 3 &lt;code&gt;CoordinateAdam&lt;/code&gt; class will be a near copy of this one — same math, different thing being optimized.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;The Innovation (early preview):&lt;/strong&gt; &lt;em&gt;"I wrote my own Adam optimizer for coordinate updates because I needed to freeze individual coordinates mid-optimization when a user grabs a node — the standard &lt;code&gt;torch.optim&lt;/code&gt; API doesn't expose that cleanly."&lt;/em&gt; Keep this for Chapter 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Adam paper (Kingma &amp;amp; Ba, 2014): &lt;a href="https://arxiv.org/abs/1412.6980" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1412.6980&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sebastian Ruder, "An overview of gradient descent optimization algorithms": &lt;a href="https://www.ruder.io/optimizing-gradient-descent/" rel="noopener noreferrer"&gt;https://www.ruder.io/optimizing-gradient-descent/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Checkpoint 1.2:&lt;/strong&gt; Plot loss curves for SGD, SGD+momentum, RMSprop, Adam — same problem, same init, same step count. If Adam isn't fastest, check bias-correction first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 4: A Transformer From Scratch (Tiny, But Real)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;A Transformer's core operation: for every token, compute a &lt;strong&gt;weighted average of every other token's representation&lt;/strong&gt;, where weights ("attention") are learned and reflect relevance. Everything else — multi-head, positional encoding, layer norm, feedforward — is scaffolding.&lt;/p&gt;

&lt;p&gt;Analogy: a room full of people (tokens) forming an opinion on a topic — you weight each person's input by relevance. Query = what you're looking for; Key = what each token offers; Value = what it contributes if attended to.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch.nn.functional&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scaled_dot_product_attention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;d_k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: Q @ K.T / sqrt(d_k)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;masked_fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-inf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: softmax(scores, dim=-1)
&lt;/span&gt;    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;   &lt;span class="c1"&gt;# TODO: weights @ V
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;
&lt;span class="c1"&gt;# Sanity check: verify weights.sum(dim=-1) is all 1.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TinyMultiHeadAttention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_heads&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;d_model&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;n_heads&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;d_k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n_heads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d_model&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;n_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_heads&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: four Linear layers W_q, W_k, W_v, W_o
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: project, reshape to heads, attend per head, concat, W_o
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TinyEncoderLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_ff&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TinyMultiHeadAttention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_heads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayerNorm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_ff&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReLU&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_ff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayerNorm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# TODO: x = norm1(x + attn(x)) ; x = norm2(x + ff(x))
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Train on next-character prediction over a small corpus (a few historical-text paragraphs fits the theme).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Visualize attention weights for one sentence as a heatmap — rows should light up on semantically relevant tokens (e.g. a pronoun attending to the noun it refers to). Attention stops being a diagram and becomes something you watch your model do.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;In Chapter 2 you'll swap this toy encoder for a pretrained BERT, understanding exactly what happens inside every layer because you built the smallest version yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;"The Annotated Transformer" (Harvard NLP): &lt;a href="https://nlp.seas.harvard.edu/annotated-transformer/" rel="noopener noreferrer"&gt;https://nlp.seas.harvard.edu/annotated-transformer/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Attention Is All You Need": &lt;a href="https://arxiv.org/abs/1706.03762" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1706.03762&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Jay Alammar, "The Illustrated Transformer": &lt;a href="https://jalammar.github.io/illustrated-transformer/" rel="noopener noreferrer"&gt;https://jalammar.github.io/illustrated-transformer/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🕳️ &lt;strong&gt;The Rabbit Hole:&lt;/strong&gt; Attention has no inherent sense of token order. Read the sinusoidal positional encoding and ask why sine/cosine rather than a learned embedding.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Checkpoint 1.3 — End of Phase 1:&lt;/strong&gt; From memory, sketch a Transformer encoder layer's forward pass, and explain what breaks if you remove residual connections, layer norm, or the &lt;code&gt;sqrt(d_k)&lt;/code&gt; scaling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1 Milestone
&lt;/h2&gt;

&lt;p&gt;By end of Week 4, from raw tensor operations, you have: (1) intuitive autograd understanding, (2) a hand-built 2-layer network with manual gradient updates, (3) three hand-built optimizers, (4) a working tiny Transformer with visualized attention. Your Week 3 optimizers become the Phase 3 coordinate optimizer almost verbatim; your Week 4 Transformer understanding is what lets you debug BERT rather than treat it as a black box.&lt;/p&gt;




&lt;h1&gt;
  
  
  CHAPTER 2: The Understanding — Teaching Your Engine to Read History
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;(Weeks 5–8 · Prerequisite: Phase 1 complete, comfort reading model architecture diagrams)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 5: Fine-Tuning BERT for Named Entity Recognition
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;Your tiny Transformer from Week 4 learned character prediction from scratch on almost no data. BERT is the same architecture, scaled up (12 layers, 12 heads, 768-dim), pretrained on billions of words. Fine-tuning means: keep the pretrained weights as a strong starting point, attach a small task-specific head on top (here, a linear layer mapping each token's final hidden state to a label like &lt;code&gt;B-EVENT&lt;/code&gt;, &lt;code&gt;I-EVENT&lt;/code&gt;, &lt;code&gt;B-DATE&lt;/code&gt;, &lt;code&gt;O&lt;/code&gt;), and train the whole thing (or just the head) on your labeled data for a few epochs.&lt;/p&gt;

&lt;p&gt;The reason this works with so little labeled data compared to training from scratch: BERT already learned general-purpose language structure from pretraining (grammar, word relationships, some world knowledge). Fine-tuning just teaches it to &lt;em&gt;route&lt;/em&gt; that existing knowledge toward your specific labeling scheme.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BertTokenizerFast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BertForTokenClassification&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="c1"&gt;# TODO: load "bert-base-cased" tokenizer and BertForTokenClassification
#       with num_labels = len(your_label_list)
#       e.g. label_list = ["O","B-EVENT","I-EVENT","B-DATE","I-DATE","B-PERSON","I-PERSON"]
&lt;/span&gt;&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tokenize_and_align_labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;word_labels&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    text: list of words, e.g. [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assassination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Franz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ferdinand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]
    word_labels: list of label ids, one per word
    Returns tokenized input + labels aligned to WordPiece subtokens
    (subtokens after the first get label -100 so they&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re ignored in the loss)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: tokenizer(text, is_split_into_words=True, ...) then use
&lt;/span&gt;    &lt;span class="c1"&gt;# .word_ids() to map each subtoken back to its source word
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# TODO: set up a small labeled dataset (start with ~50-100 hand-labeled
# sentences from historical text — yes, you label it yourself first)
# TODO: standard fine-tuning loop: forward, CrossEntropyLoss (ignore_index=-100),
# backward, optimizer.step() -- use torch.optim.AdamW this time, not your manual one,
# since you've already proven you understand what it's doing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Run inference on a sentence the model never saw during fine-tuning and watch it correctly tag "the outbreak of war" as an EVENT span and "1914" as a DATE span, purely from ~100 examples. That's the pretraining transfer working — this would be essentially impossible to get right training from random initialization on 100 sentences.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;This NER output — spans tagged as EVENT, DATE, PERSON, PLACE — is exactly what Week 6's relation extraction will pair up into causal triples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Hugging Face token classification guide: &lt;a href="https://huggingface.co/docs/transformers/tasks/token_classification" rel="noopener noreferrer"&gt;https://huggingface.co/docs/transformers/tasks/token_classification&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;BERT paper (Devlin et al., 2018): &lt;a href="https://arxiv.org/abs/1810.04805" rel="noopener noreferrer"&gt;https://arxiv.org/abs/1810.04805&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;😤 &lt;strong&gt;The Struggle:&lt;/strong&gt; Label alignment between words and WordPiece subtokens is the single most common source of silent bugs in NER fine-tuning — a misaligned label doesn't crash, it just quietly trains the model on garbage. Always spot-check &lt;code&gt;tokenizer.convert_ids_to_tokens()&lt;/code&gt; against your aligned label array for a handful of examples before you trust any training run.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 6: Relation Extraction — Finding "A Causes B"
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;NER tells you &lt;em&gt;what&lt;/em&gt; the entities are. Relation extraction tells you &lt;em&gt;how they relate&lt;/em&gt;. The simplest approach: for every pair of entities that co-occur in the same sentence (or a short window of sentences), feed their contextualized representations (plus the sentence itself) into a classifier that predicts one of a fixed set of relation labels: &lt;code&gt;CAUSES&lt;/code&gt;, &lt;code&gt;ENABLED_BY&lt;/code&gt;, &lt;code&gt;PRECEDES&lt;/code&gt;, &lt;code&gt;PART_OF&lt;/code&gt;, or &lt;code&gt;NONE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A more powerful approach — &lt;strong&gt;REBEL&lt;/strong&gt; — does entity and relation extraction jointly, generating the full set of triples as a structured text sequence in one pass, rather than requiring you to first extract entities, then classify every pair. It's a strong upgrade path once your simpler pairwise classifier is working and you understand &lt;em&gt;why&lt;/em&gt; the joint approach is more efficient (it doesn't blow up combinatorially with the number of entities in a sentence).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;

&lt;p&gt;Start with the pairwise classifier — it's the better learning exercise, even though REBEL is more production-capable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# For each entity pair (e1, e2) in the same sentence:
#   1. Mark their spans in the input with special tokens, e.g.
#      "[E1] The assassination [/E1] of Franz Ferdinand triggered
#       [E2] Austria-Hungary's ultimatum [/E2] to Serbia."
#   2. Run through BERT
#   3. TODO: take the hidden states at the [E1] and [E2] marker positions,
#      concatenate them, pass through a small classifier head
#      -&amp;gt; softmax over {CAUSES, ENABLED_BY, PRECEDES, PART_OF, NONE}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;The output for that Franz Ferdinand sentence should be &lt;code&gt;CAUSES&lt;/code&gt; with high confidence. Then feed it a sentence with two entities that are merely mentioned near each other with no causal link ("The war began in 1914. Assassinations were common in the region.") and watch it correctly predict &lt;code&gt;NONE&lt;/code&gt;. That contrast — the model discriminating &lt;em&gt;relevance&lt;/em&gt;, not just &lt;em&gt;proximity&lt;/em&gt; — is the whole point of relation extraction over naive "entities near each other are related" heuristics.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;(entity_1, relation, entity_2)&lt;/code&gt; triple this produces becomes an edge in the causal graph that Phase 3's spatial engine will lay out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;REBEL paper (Huguet Cabot &amp;amp; Navigli, 2021): &lt;a href="https://aclanthology.org/2021.findings-emnlp.204/" rel="noopener noreferrer"&gt;https://aclanthology.org/2021.findings-emnlp.204/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;REBEL on Hugging Face: &lt;a href="https://huggingface.co/Babelscape/rebel-large" rel="noopener noreferrer"&gt;https://huggingface.co/Babelscape/rebel-large&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🕳️ &lt;strong&gt;The Rabbit Hole:&lt;/strong&gt; Read about "distant supervision" for relation extraction — a technique for auto-generating noisy training labels by aligning a knowledge base (like Wikidata) against text, instead of hand-labeling everything. Useful if your ~100 hand-labeled examples aren't enough once you scale up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 7: The Data Pipeline — Text → Tokens → Entities → Relations → Storage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;A pipeline is only as trustworthy as its weakest stage boundary. This week isn't about new ML — it's about wiring Weeks 5 and 6 together into something that reliably takes raw pasted text and emits a clean, deduplicated, database-ready set of triples, with sane error handling for the inevitable garbage input (empty text, non-English text, text with no extractable events).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExtractionPipeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ner_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relation_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        Returns: [{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAUSES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
                   &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1914-06-28&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; or None, ...}, ...]
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="c1"&gt;# TODO Step 1: sentence-split raw_text (spaCy or nltk sentence tokenizer)
&lt;/span&gt;        &lt;span class="c1"&gt;# TODO Step 2: run NER on each sentence -&amp;gt; entity spans
&lt;/span&gt;        &lt;span class="c1"&gt;# TODO Step 3: for entity pairs within a sentence AND across
&lt;/span&gt;        &lt;span class="c1"&gt;#      adjacent sentences (causal claims often span 2 sentences),
&lt;/span&gt;        &lt;span class="c1"&gt;#      run the relation classifier
&lt;/span&gt;        &lt;span class="c1"&gt;# TODO Step 4: filter relation predictions below a confidence
&lt;/span&gt;        &lt;span class="c1"&gt;#      threshold (start at 0.6, tune empirically)
&lt;/span&gt;        &lt;span class="c1"&gt;# TODO Step 5: deduplicate triples (same subject/object/relation
&lt;/span&gt;        &lt;span class="c1"&gt;#      appearing from overlapping sentence windows)
&lt;/span&gt;        &lt;span class="c1"&gt;# TODO Step 6: return structured triples
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up PostgreSQL with pgvector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;date_text&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- BERT's hidden size&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;relations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;source_event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;target_event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;relation_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="nb"&gt;FLOAT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Paste in three unrelated paragraphs of history (say, one on WWI causes, one on the French Revolution, one on the fall of Rome) and watch the pipeline correctly keep the triples from each topic separate, with no spurious cross-topic relations — because relation extraction is confidence-thresholded and entity embeddings from unrelated topics don't get spuriously matched.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;This pipeline is the entire backend of the &lt;code&gt;/api/extract&lt;/code&gt; endpoint from Chapter 0's data flow diagram. Everything downstream (Phase 3, Phase 4) consumes its output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;pgvector docs: &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;https://github.com/pgvector/pgvector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SQLAlchemy + pgvector integration: &lt;a href="https://github.com/pgvector/pgvector-python" rel="noopener noreferrer"&gt;https://github.com/pgvector/pgvector-python&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Checkpoint 2.1:&lt;/strong&gt; Run your pipeline on a genuinely messy input (a Wikipedia paragraph with footnote markers, inconsistent date formats, nested clauses) and verify it degrades gracefully — no crash, just fewer/lower-confidence triples — rather than throwing an unhandled exception.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 8: Evaluation and Hardening
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;An extraction pipeline that "looks right" on your three favorite test paragraphs is not the same as one that's reliable. This week is about building a small held-out evaluation set (hand-labeled, separate from training data) and computing real metrics: precision, recall, and F1 for both entity spans and relation classification.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate_ner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_sentences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: run inference, compute span-level (not token-level!) precision/recall/F1
&lt;/span&gt;    &lt;span class="c1"&gt;# Span-level means a predicted "B-EVENT I-EVENT" span only counts as
&lt;/span&gt;    &lt;span class="c1"&gt;# correct if it matches the gold span's exact boundaries, not just
&lt;/span&gt;    &lt;span class="c1"&gt;# individual token labels — this is a stricter and more meaningful metric.
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate_relations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_labels&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: per-class precision/recall/F1, plus a confusion matrix
&lt;/span&gt;    &lt;span class="c1"&gt;# (CAUSES vs ENABLED_BY vs PRECEDES are semantically close and
&lt;/span&gt;    &lt;span class="c1"&gt;# commonly confused — expect this)
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Your confusion matrix will likely show &lt;code&gt;CAUSES&lt;/code&gt; and &lt;code&gt;ENABLED_BY&lt;/code&gt; bleeding into each other — this is expected and informative, not a bug. It tells you those two categories are genuinely hard to distinguish from surface text alone (a human annotator would disagree with themselves on some of these too), which should inform how confidently the frontend displays that distinction later (e.g., maybe you merge them into one edge style with a subtler visual difference rather than two starkly different arrow types).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;Phase 2 milestone: a real, measured, imperfect-but-quantified extraction system, feeding real triples into a real database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;"Named Entity Recognition" evaluation conventions (seqeval library): &lt;a href="https://github.com/chakki-works/seqeval" rel="noopener noreferrer"&gt;https://github.com/chakki-works/seqeval&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Phase 2 Milestone
&lt;/h2&gt;

&lt;p&gt;You now have a fine-tuned BERT NER model, a relation extraction classifier, a hardened pipeline connecting them, a Postgres+pgvector store, and honest evaluation metrics on held-out data. This is a legitimate, demoable NLP system on its own — worth pausing to appreciate before diving into Phase 3, which is where the project becomes genuinely novel.&lt;/p&gt;




&lt;h1&gt;
  
  
  CHAPTER 3: The Intelligence — Making Your Engine Think Spatially
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;(Weeks 9–12 · Prerequisite: Phase 1 and 2 complete. This is the conceptual core of ChronoWeave — take this chapter slowly.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Chapter Gets the Most Depth
&lt;/h2&gt;

&lt;p&gt;Everything before this point — the Transformer, the NER model, the relation extractor — is, architecturally, "standard" deep learning applied to a specific domain. Impressive to build from scratch, but conceptually well-trodden. This chapter is where ChronoWeave does something genuinely uncommon: using gradient descent as a &lt;strong&gt;general-purpose layout algorithm&lt;/strong&gt;, with coordinates as first-class trainable parameters. Take this slowly. It's the part of the project you'll actually be excited to explain in an interview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 9: Coordinates as Parameters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;Every optimization problem in ML has the same shape: parameters, a loss function measuring how bad the current parameters are, and an optimizer that nudges parameters to reduce that loss. So far, "parameters" has meant neural network weights. Nothing in that shape &lt;em&gt;requires&lt;/em&gt; the parameters to be weights. A parameter is just a tensor with &lt;code&gt;requires_grad=True&lt;/code&gt; that appears somewhere in a differentiable computation whose output is your loss.&lt;/p&gt;

&lt;p&gt;So: what if the parameters are the (x, y) positions of graph nodes on a canvas, and the loss is a hand-designed function that scores how "good" a layout is?&lt;/p&gt;

&lt;p&gt;This reframes graph layout — traditionally solved with physics simulations (force-directed layout, spring-embedder algorithms) — as an optimization problem solvable with the exact same machinery you built in Phase 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;

&lt;p&gt;Set up the skeleton, no loss function yet — just prove coordinates can be optimized at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;

&lt;span class="n"&gt;num_nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="c1"&gt;# TODO: initialize positions as a (num_nodes, 2) tensor, random in
# some reasonable range (e.g. -5 to 5), requires_grad=True
&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# Toy goal: pull every node toward the origin (0,0) — trivial loss,
# just to prove the plumbing works before you write anything smarter
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;toy_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: sum of squared distances from origin
&lt;/span&gt;    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Adam&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toy_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# TODO: plot positions before and after — every node should have
# collapsed toward (0,0)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Watching ten random points converge toward the origin using &lt;code&gt;torch.optim.Adam&lt;/code&gt; — the exact same optimizer class you'd use to train a neural network — is the moment this project's central idea stops being abstract. You just used backpropagation to solve a &lt;em&gt;geometry&lt;/em&gt; problem. No physics, no forces, no simulation of springs — just calculus.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;toy_loss&lt;/code&gt; is a stand-in. Next week you replace it with the real Map Clarity Loss — but the training loop around it doesn't change at all.&lt;/p&gt;

&lt;p&gt;🧭 &lt;strong&gt;The Mentor Says:&lt;/strong&gt; Resist the urge to jump straight to the full Map Clarity Loss this week. Build up loss terms one at a time, verify each one does what you expect in isolation (attraction only, then repulsion only, then combined), and only then combine everything. Debugging a five-term loss function that's never converged correctly at any point is miserable. Debugging a five-term loss function where you've verified each term separately is straightforward.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 10: The Map Clarity Loss
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;A good layout satisfies several competing objectives simultaneously:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Attraction&lt;/strong&gt;: causally-linked events should be close together (short edges are more readable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repulsion&lt;/strong&gt;: all events should push apart from each other generally, so the graph doesn't collapse into a single point (this is what "attraction" alone would do — see Week 9's toy example)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-overlap&lt;/strong&gt;: no two event boxes should visually overlap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal ordering (optional but powerful)&lt;/strong&gt;: since these are &lt;em&gt;historical&lt;/em&gt; events, you may want an additional soft constraint that events with earlier dates trend toward one side of the canvas — turning the layout into something between a pure causal graph and a timeline&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these becomes a differentiable term, and the total loss is a weighted sum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;L_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;L_attraction&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;L_repulsion&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;L_overlap&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;L_temporal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weights (&lt;code&gt;w1..w4&lt;/code&gt;) are hyperparameters you'll tune empirically — this is exactly analogous to loss weighting in multi-task neural network training, another well-known hard problem, so don't be surprised if getting a visually pleasing balance takes real iteration.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;attraction_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    edges: list of (i, j) node index pairs that are causally linked
    Pulls linked nodes together — squared distance, like a spring
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;i_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;j_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: diff = positions[i_idx] - positions[j_idx]
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: return (diff ** 2).sum(dim=1).mean()
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;repulsion_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Pushes ALL pairs of nodes apart if they&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re closer than min_distance.
    This is O(n^2) -- fine for the node counts ChronoWeave targets
    (tens to low hundreds of events per graph), but know that this term
    is the one that would need approximating (e.g. Barnes-Hut) at scale.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: compute pairwise distance matrix
&lt;/span&gt;    &lt;span class="c1"&gt;# (hint: torch.cdist(positions, positions) does this in one call)
&lt;/span&gt;    &lt;span class="n"&gt;dist_matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: for pairs closer than min_distance, penalize (min_distance - dist)^2
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: mask out the diagonal (distance of a node to itself = 0, don't penalize that)
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;overlap_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;box_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: similar to repulsion but specifically penalizes overlap of
&lt;/span&gt;    &lt;span class="c1"&gt;# fixed-size boxes -- can start as a simplified version of repulsion_loss
&lt;/span&gt;    &lt;span class="c1"&gt;# with min_distance = box_size, and refine later once you see real overlaps
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;temporal_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dates_normalized&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    dates_normalized: tensor of shape (n,), each event&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s date mapped to
    [0, 1] (earliest event=0, latest=1)
    Soft-encourages x-coordinate to correlate with date
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: target_x = dates_normalized * canvas_width
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: return ((positions[:, 0] - target_x) ** 2).mean()
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map_clarity_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dates_normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;w1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;attraction_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;repulsion_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;overlap_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;temporal_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dates_normalized&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Run the full optimization on a real extracted graph (from your Phase 2 pipeline!) with all four terms weighted, and watch causally-connected clusters visually separate from unrelated clusters, while individual nodes within a cluster stay legibly spaced apart. This is the actual "map" in ChronoWeave — and unlike a force-directed layout from a library, you can explain &lt;em&gt;precisely&lt;/em&gt; why every node ended up where it did, because you wrote every term of the objective yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;This is the function whose gradient — with respect to &lt;code&gt;positions&lt;/code&gt;, not any neural network weight — drives literally everything the user sees on the canvas, and everything that happens when they drag a node in Chapter 4.&lt;/p&gt;

&lt;p&gt;😤 &lt;strong&gt;The Struggle:&lt;/strong&gt; The most common failure mode here is loss term imbalance — e.g. repulsion dominating so hard that attraction can't pull anything together, producing a uniform grid-like scatter with no visible clustering. When this happens, don't guess-and-check weights blindly. Log each individual loss term's value (not just the weighted sum) every N steps, and look at their &lt;em&gt;relative magnitudes&lt;/em&gt;. If &lt;code&gt;repulsion_loss&lt;/code&gt; is naturally 50x the scale of &lt;code&gt;attraction_loss&lt;/code&gt; just from how you defined it (e.g. squared distances over many more pairs), your weights need to correct for that scale difference before they can express your actual &lt;em&gt;priorities&lt;/em&gt; between the terms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Force-directed graph drawing (for conceptual contrast — read this to understand what you're deliberately doing differently): Fruchterman &amp;amp; Reingold, "Graph Drawing by Force-Directed Placement" (1991) — search for the PDF, it's a classic short paper&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;torch.cdist&lt;/code&gt; docs: &lt;a href="https://docs.pytorch.org/docs/stable/generated/torch.cdist.html" rel="noopener noreferrer"&gt;https://docs.pytorch.org/docs/stable/generated/torch.cdist.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ &lt;strong&gt;Checkpoint 3.1:&lt;/strong&gt; Take a graph with two clearly separate causal clusters (e.g. WWI causes and, unrelated, causes of the 2008 financial crisis, extracted from two different pasted texts) and verify optimization produces two visually distinct, non-overlapping clusters — not because you told it to, but because attraction pulls each cluster's internal nodes together while repulsion pushes the two clusters apart as aggregate masses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 11: Custom Gradient Hooks and the Coordinate Optimizer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;Two refinements this week. First: &lt;strong&gt;gradient hooks&lt;/strong&gt;. PyTorch lets you register a function that runs every time a gradient is computed for a specific tensor, which is how you'll implement per-node learning rate decay, gradient clipping for numerically unstable nodes, or debugging instrumentation (logging exactly which nodes have the largest gradients at each step — useful for diagnosing which part of the layout is "fighting" hardest).&lt;/p&gt;

&lt;p&gt;Second: formalize your Phase 1 &lt;code&gt;ManualAdam&lt;/code&gt; into a purpose-built &lt;code&gt;CoordinateAdam&lt;/code&gt; that supports &lt;strong&gt;freezing individual coordinates&lt;/strong&gt; — critical for Chapter 4's drag-and-reoptimize feature, where the dragged node must stay exactly where the user put it while every other node reoptimizes around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gradient hook example
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_logging_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_names&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# grad shape: (num_nodes, 2)
&lt;/span&gt;        &lt;span class="c1"&gt;# TODO: find and print the node with the largest gradient norm
&lt;/span&gt;        &lt;span class="c1"&gt;# this step -- useful for seeing which node is "hardest to place"
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;grad&lt;/span&gt;  &lt;span class="c1"&gt;# must return grad unchanged (or modified) -- don't return None
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;

&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make_logging_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_names&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CoordinateAdam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Like your Phase 1 ManualAdam, but supports freezing a subset of
    coordinates (e.g. a node the user just dragged).
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;betas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.999&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;betas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# TODO: a boolean mask (num_nodes,) -- True = frozen, don't update
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frozen_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node_idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frozen_mask&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node_idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unfreeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node_idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frozen_mask&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node_idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;
            &lt;span class="c1"&gt;# TODO: zero out gradient rows for frozen nodes BEFORE the
&lt;/span&gt;            &lt;span class="c1"&gt;# Adam update, so their moment buffers don't drift either
&lt;/span&gt;            &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frozen_mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
            &lt;span class="n"&gt;m_hat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;v_hat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;m_hat&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_hat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# TODO: apply update only to non-frozen rows
&lt;/span&gt;            &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frozen_mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Freeze one node mid-optimization (simulate a user drag by just calling &lt;code&gt;.freeze(node_idx)&lt;/code&gt; and manually setting that row of &lt;code&gt;positions&lt;/code&gt; to a fixed target), re-run &lt;code&gt;.step()&lt;/code&gt; for another 100 iterations, and watch every &lt;em&gt;other&lt;/em&gt; node smoothly re-flow into a new stable configuration around the pinned node — while the pinned node itself doesn't move a single pixel, even though it still has a nonzero gradient every step.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;This freeze/unfreeze mechanism is &lt;em&gt;exactly&lt;/em&gt; the backend logic Chapter 4's WebSocket drag handler calls into. You are not building new logic in Phase 4 — you're wiring this class up to a live event stream.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;The Innovation:&lt;/strong&gt; This is the strongest, most concrete "why is this novel" answer for the whole project: &lt;em&gt;"Standard force-directed layout libraries treat a dragged node as an external constraint bolted onto a physics simulation — usually by literally fixing its position and letting the simulation tick forward. My approach treats the drag as freezing one parameter's gradient within the exact same optimization loop used to generate the original layout, which means the re-layout objective is provably the same objective function, just re-solved under an added constraint — not a different, ad hoc mechanism."&lt;/em&gt; That's a real distinction, not a marketing line, and you can defend it because you wrote both halves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PyTorch hooks documentation: &lt;a href="https://docs.pytorch.org/docs/stable/notes/autograd.html#hooks-for-saved-tensors" rel="noopener noreferrer"&gt;https://docs.pytorch.org/docs/stable/notes/autograd.html#hooks-for-saved-tensors&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;register_hook&lt;/code&gt; API: &lt;a href="https://docs.pytorch.org/docs/stable/generated/torch.Tensor.register_hook.html" rel="noopener noreferrer"&gt;https://docs.pytorch.org/docs/stable/generated/torch.Tensor.register_hook.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Week 12: Debugging Gradient-Based Layout
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;Gradient-based layout fails in ways that are different from — and often more confusing than — a standard training loop failing, because there's no "accuracy" metric to sanity check against, only a loss value and a visual result. This week is a deliberate practice week: you'll intentionally break your Week 10–11 system in several ways and learn to recognize the symptoms.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge — Diagnose Each of These Failure Modes
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Everything collapses to one point.&lt;/strong&gt; (Hint: check the sign and relative magnitude of your attraction vs. repulsion terms — is repulsion actually being computed, or is a masking bug zeroing it out?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout oscillates forever, never converging.&lt;/strong&gt; (Hint: learning rate too high for this loss surface, OR two loss terms directly fighting with equal and opposite gradients at every step — try lowering &lt;code&gt;lr&lt;/code&gt; by 10x before assuming it's a logic bug)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NaN positions after N steps.&lt;/strong&gt; (Hint: &lt;code&gt;torch.cdist&lt;/code&gt; produces a zero distance for a node compared to itself; if your repulsion loss divides by distance anywhere instead of just using squared distance, you'll get a divide-by-zero on the diagonal. Always double-check you've masked the diagonal.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One node flies off to extreme coordinates while everything else looks fine.&lt;/strong&gt; (Hint: register a gradient hook — from Week 11 — on &lt;code&gt;positions&lt;/code&gt; and print per-node gradient norms; the runaway node almost always has an anomalously large gradient from a bug specific to its edges, e.g. it participates in zero attraction edges so repulsion has nothing to balance against)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;After deliberately inducing and then fixing all four failure modes above, run your full pipeline end-to-end — real text in, real triples extracted, real layout optimized — and watch it just... work, cleanly, without you needing to touch a single hyperparameter mid-run. That reliability is earned specifically by having debugged each failure mode once on purpose, rather than encountering them for the first time under deadline pressure in Phase 4.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;Phase 3 milestone: a working, debugged spatial intelligence engine that takes a causal graph and produces a legible 2D layout via gradient descent, with the ability to freeze nodes and re-optimize live.&lt;/p&gt;

&lt;p&gt;🧭 &lt;strong&gt;The Mentor Says:&lt;/strong&gt; Keep a running "failure log" as a markdown file next to your code: every bug you hit, what the symptom looked like, and what actually fixed it. Six months from now when this happens again in a different project, that log is worth more than any Stack Overflow search, because it's &lt;em&gt;your&lt;/em&gt; debugging vocabulary for &lt;em&gt;this specific class of problem&lt;/em&gt; (gradient-based systems with non-standard parameters), which almost nobody else has written down anywhere.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Checkpoint 3.2 — End of Phase 3:&lt;/strong&gt; Explain to a rubber duck (or a friend, or a mirror) why gradient descent on coordinates is a valid approach to graph layout at all — i.e., why the loss function's gradient with respect to position tells you a &lt;em&gt;meaningful&lt;/em&gt; direction to move a node, given that "position" isn't something with an obvious ground truth the way a classification label is. (The core answer: there's no ground truth position, but there's a well-defined &lt;em&gt;relative&lt;/em&gt; preference — closer is better for linked nodes, farther is better for unlinked ones — and gradient descent is a general algorithm for finding local optima of &lt;em&gt;any&lt;/em&gt; differentiable preference function, not just supervised losses with ground truth targets.)&lt;/p&gt;




&lt;h1&gt;
  
  
  CHAPTER 4: The Body — Giving Your Engine a Web Presence
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;(Weeks 13–16 · Prerequisite: Phase 3 complete)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 13: FastAPI Backend and WebSockets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;Two distinct interaction patterns need two different transport mechanisms. The initial "paste text, click Generate" flow is a classic request/response — REST fits fine. But the drag-and-reoptimize flow needs the server to push a &lt;em&gt;stream&lt;/em&gt; of intermediate frames to the client as optimization runs (so the user sees nodes flow smoothly into place rather than jumping instantly), which REST can't do — that's what WebSockets are for.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/extract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: run ExtractionPipeline (Chapter 2) -&amp;gt; triples
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: run spatial optimization (Chapter 3) to convergence
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: return {"nodes": [...], "edges": [...]}
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@app.websocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ws/relayout/{graph_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;relayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;graph_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: receive {"dragged_node_id": ..., "new_x": ..., "new_y": ...}
&lt;/span&gt;    &lt;span class="n"&gt;drag_event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# TODO: load current positions for graph_id, freeze the dragged node
&lt;/span&gt;    &lt;span class="c1"&gt;#       at (new_x, new_y) using CoordinateAdam.freeze() from Chapter 3
&lt;/span&gt;    &lt;span class="c1"&gt;# TODO: run optimization step-by-step (not to convergence in one go!)
&lt;/span&gt;    &lt;span class="c1"&gt;#       and after every K steps, send the current positions:
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# optimizer.step()...
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# TODO: await websocket.send_json({"positions": [...]})
&lt;/span&gt;            &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Open your browser's network tab, trigger a drag, and literally watch a stream of JSON frames arrive over the WebSocket connection every few milliseconds — the exact same optimization loop from Chapter 3, just now visible as it happens, one HTTP-adjacent message at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;This is the wiring that turns Chapter 3's offline optimization script into a live, interactive feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI WebSockets guide: &lt;a href="https://fastapi.tiangolo.com/advanced/websockets/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/advanced/websockets/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Week 14: React + D3.js Rendering
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;D3 excels at binding data to SVG elements and handling enter/update/exit transitions smoothly — exactly what you need when node positions update every few WebSocket frames. React manages component state and lifecycle; D3 manages the actual SVG manipulation and smooth transitions between positions. The common pattern: let React own the DOM structure (which nodes/edges exist), let D3 own the &lt;em&gt;transitions&lt;/em&gt; (how a node's x/y animates from old position to new).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TODO: a GraphCanvas component that:&lt;/span&gt;
&lt;span class="c1"&gt;// 1. Receives `nodes` and `edges` as props (from the /api/extract response)&lt;/span&gt;
&lt;span class="c1"&gt;// 2. Renders SVG circles for nodes, lines for edges, using D3 scales&lt;/span&gt;
&lt;span class="c1"&gt;//    to map data coordinates to screen coordinates&lt;/span&gt;
&lt;span class="c1"&gt;// 3. Uses d3.transition() to animate position changes smoothly whenever&lt;/span&gt;
&lt;span class="c1"&gt;//    the `nodes` prop updates (i.e., whenever a new WebSocket frame arrives)&lt;/span&gt;
&lt;span class="c1"&gt;// 4. Attaches d3.drag() behavior to each node, which on 'end' fires a&lt;/span&gt;
&lt;span class="c1"&gt;//    callback (e.g. onNodeDragged(nodeId, newX, newY)) that the parent&lt;/span&gt;
&lt;span class="c1"&gt;//    component uses to open the /ws/relayout WebSocket and stream in&lt;/span&gt;
&lt;span class="c1"&gt;//    the live re-layout frames from Week 13&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GraphCanvas&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onNodeDragged&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// TODO: useRef for the svg element, useEffect that runs D3 rendering&lt;/span&gt;
  &lt;span class="c1"&gt;// logic whenever `nodes` or `edges` change&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Drag a node and watch not just that node move, but every connected node smoothly animate to its new position over the following second or two — not teleporting, not choppy, an actual fluid re-settling. This is the "magic moment" the entire project overview promised in Chapter 0, now real in a browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;Nothing further — this &lt;em&gt;is&lt;/em&gt; the product. Everything from Chapter 1 onward has been building toward this specific ten seconds of interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;"Learn D3: Joining Data" (Observable): &lt;a href="https://observablehq.com/@d3/learn-d3-joining-data" rel="noopener noreferrer"&gt;https://observablehq.com/@d3/learn-d3-joining-data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amelia Wattenberger, "React + D3" (a well-regarded pattern guide for combining the two): &lt;a href="https://wattenberger.com/blog/react-and-d3" rel="noopener noreferrer"&gt;https://wattenberger.com/blog/react-and-d3&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;😤 &lt;strong&gt;The Struggle:&lt;/strong&gt; React re-rendering and D3 both wanting to own the DOM is a classic source of fights — D3 mutates elements directly, React expects to control them via its virtual DOM diff. The cleanest fix most people converge on: let React render the SVG container and static structure, but hand D3 a ref to manipulate node/edge elements directly inside a &lt;code&gt;useEffect&lt;/code&gt;, and &lt;em&gt;never&lt;/em&gt; let React's render also try to set those same attributes. Pick one owner per DOM property.&lt;/p&gt;




&lt;h2&gt;
  
  
  Week 15: Deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;Docker Compose ties your FastAPI backend, Postgres+pgvector database, and (optionally) a separate model-serving container into one reproducible unit you can run locally exactly as it'll run in the cloud — closing the "works on my machine" gap before it costs you a demo day.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml — TODO fill in the blanks&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgvector/pgvector:pg16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://...@db:5432/...&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: mount your fine-tuned model weights as a volume rather than&lt;/span&gt;
    &lt;span class="c1"&gt;# baking them into the image -- keeps image builds fast during dev&lt;/span&gt;

  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./frontend&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker-compose up&lt;/code&gt; on a completely fresh machine (or a cloud VM) and the whole system — model inference, database, backend, frontend — comes up correctly with zero manual steps beyond that one command. That's the difference between "a project on my laptop" and "a project."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extension
&lt;/h3&gt;

&lt;p&gt;Deploy to Render, Fly.io, or a similar platform for a public demo URL — essential for the hackathon-ready, portfolio-ready version of this project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Docker Compose docs: &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;https://docs.docker.com/compose/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Render's Docker deployment guide: &lt;a href="https://render.com/docs/docker" rel="noopener noreferrer"&gt;https://render.com/docs/docker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Week 16: Polish and the Full Demo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;The last week is deliberately not about new features. It's about the unglamorous 20% that makes a demo feel finished: loading states while extraction/optimization runs, graceful handling of bad input, a couple of pre-baked example texts a judge or interviewer can one-click try instead of needing to paste their own, and a README that explains the project in 30 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code Challenge
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add a loading skeleton/spinner state to the frontend while &lt;code&gt;/api/extract&lt;/code&gt; is in flight (this can take several seconds — a live optimization loop is not instant, and users need to know it's working, not frozen)&lt;/li&gt;
&lt;li&gt;Add 2–3 pre-loaded example texts (a good default: causes of WWI, causes of the French Revolution, causes of the 2008 financial crisis)&lt;/li&gt;
&lt;li&gt;Write a project README with: what it does, a GIF of the drag-and-reoptimize interaction, architecture diagram, how to run it locally&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Aha Moment
&lt;/h3&gt;

&lt;p&gt;Show the finished product to someone who has never seen it, say nothing, and watch them paste in their own text and drag a node without prompting. If they intuitively understand what happened without you explaining it, the product design succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4 Milestone — Project Complete
&lt;/h2&gt;

&lt;p&gt;A live, deployed, full-stack application: paste historical text, get an extracted causal graph laid out via gradient descent, drag any node and watch the rest of the graph re-optimize in real time — all running on infrastructure you understand end to end, built from raw tensor operations up.&lt;/p&gt;




&lt;h1&gt;
  
  
  EPILOGUE: Beyond ChronoWeave — What You Can Build Next
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Portfolio Presentation
&lt;/h2&gt;

&lt;p&gt;Lead with the live demo, not the tech stack list. The first ten seconds should be someone pasting text and dragging a node — let the magic moment sell itself before you explain any internals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Research Paper Possibilities
&lt;/h2&gt;

&lt;p&gt;The "gradient descent as general-purpose graph layout" idea, generalized beyond history specifically, touches on active research areas: differentiable graph drawing, learned layout objectives, and combining symbolic graph constraints with continuous optimization. A write-up comparing your Map Clarity Loss approach against classical force-directed layout on layout-quality metrics (edge crossing count, node overlap, cluster separation) would be a legitimate small research contribution, or at minimum a strong technical blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling the System
&lt;/h2&gt;

&lt;p&gt;Current design targets tens to low-hundreds of nodes per graph (the &lt;code&gt;O(n²)&lt;/code&gt; repulsion term is the binding constraint). Scaling further means either approximating repulsion (Barnes-Hut / quadtree methods, the same trick classical force-directed layout libraries use at scale) or moving to a hierarchical layout where clusters are optimized independently and then composed.&lt;/p&gt;




&lt;h1&gt;
  
  
  APPENDIX A: First Week Setup Guide (Day 1)
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install Python 3.11+, then:&lt;/span&gt;
curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://install.python-poetry.org | python3 -
poetry &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# verify install&lt;/span&gt;

&lt;span class="c"&gt;# 2. Project scaffold&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;chronoweave &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;chronoweave
poetry init &lt;span class="nt"&gt;--name&lt;/span&gt; chronoweave-ml &lt;span class="nt"&gt;-n&lt;/span&gt;
poetry add torch numpy matplotlib jupyter transformers

&lt;span class="c"&gt;# 3. Verify PyTorch sees your GPU (if you have one -- CPU is fine for Phase 1)&lt;/span&gt;
poetry run python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import torch; print(torch.cuda.is_available())"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Launch a notebook and run the Week 1 autograd exercise before doing&lt;/span&gt;
&lt;span class="c"&gt;#    anything else -- confirm your environment works before building on it&lt;/span&gt;
poetry run jupyter notebook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have a GPU: everything in Phase 1 and most of Phase 3 runs fine on CPU (small networks, small graphs). Phase 2's BERT fine-tuning is the one place a GPU meaningfully helps — Google Colab's free tier is sufficient for the scale of fine-tuning this project needs.&lt;/p&gt;




&lt;h1&gt;
  
  
  APPENDIX B: Troubleshooting
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Loss doesn't decrease at all&lt;/td&gt;
&lt;td&gt;Forgot &lt;code&gt;zero_grad()&lt;/code&gt; before &lt;code&gt;.backward()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Gradients are accumulating across steps — add &lt;code&gt;zero_grad()&lt;/code&gt; at the top of the loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RuntimeError: ...does not require grad and does not have a grad_fn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A leaf tensor got overwritten outside &lt;code&gt;torch.no_grad()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Check every in-place parameter update is inside &lt;code&gt;with torch.no_grad():&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loss becomes &lt;code&gt;NaN&lt;/code&gt; after N steps&lt;/td&gt;
&lt;td&gt;Divide-by-zero, usually in a pairwise-distance loss term where the diagonal (self-distance = 0) isn't masked&lt;/td&gt;
&lt;td&gt;Mask the diagonal before dividing by any distance term&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything collapses to a point during layout optimization&lt;/td&gt;
&lt;td&gt;Repulsion term isn't actually contributing (masking bug or weight of 0)&lt;/td&gt;
&lt;td&gt;Log each loss term separately, verify repulsion is nonzero and comparable in scale to attraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NER model predicts &lt;code&gt;O&lt;/code&gt; for everything&lt;/td&gt;
&lt;td&gt;Label misalignment between words and WordPiece subtokens&lt;/td&gt;
&lt;td&gt;Spot-check &lt;code&gt;tokenizer.convert_ids_to_tokens()&lt;/code&gt; against your label array on several examples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket disconnects mid-reoptimization&lt;/td&gt;
&lt;td&gt;Sending too many frames too fast, or an unhandled exception mid-loop killing the coroutine&lt;/td&gt;
&lt;td&gt;Throttle to sending every 5th step, wrap the optimization loop in try/except with a graceful &lt;code&gt;websocket.close()&lt;/code&gt; on error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D3 nodes flicker or don't animate smoothly&lt;/td&gt;
&lt;td&gt;React re-render fighting with D3's direct DOM manipulation&lt;/td&gt;
&lt;td&gt;Ensure only one of React/D3 sets a given DOM attribute; do D3 updates inside &lt;code&gt;useEffect&lt;/code&gt;, not render&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Compose backend can't reach the database&lt;/td&gt;
&lt;td&gt;Using &lt;code&gt;localhost&lt;/code&gt; instead of the service name in &lt;code&gt;DATABASE_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Compose networking resolves service names (&lt;code&gt;db&lt;/code&gt;), not &lt;code&gt;localhost&lt;/code&gt;, between containers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  APPENDIX C: Project Timeline (Text Gantt Chart)
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Week:        1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16
Phase 1      ██ ██ ██ ██
  Autograd    ██
  Manual NN      ██
  Optimizers        ██
  Tiny Xformer         ██
Phase 2                  ██ ██ ██ ██
  NER fine-tune            ██
  Relation extract            ██
  Data pipeline                   ██
  Evaluation                         ██
Phase 3                              ██ ██ ██ ██
  Coords as params                     ██
  Map Clarity Loss                        ██
  Grad hooks + optimizer                     ██
  Debugging practice                            ██
Phase 4                                          ██ ██ ██ ██
  FastAPI + WS                                     ██
  React + D3                                          ██
  Deployment                                             ██
  Polish + demo                                             ██
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  APPENDIX D: Success Metrics — How Do I Know I Built It Correctly?
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 (correctness of understanding, not the app):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can derive, on paper, the gradient of MSE loss with respect to a 2-layer network's weights, without a reference&lt;/li&gt;
&lt;li&gt;Your manual Adam implementation converges at a comparable rate to &lt;code&gt;torch.optim.Adam&lt;/code&gt; on the same toy problem&lt;/li&gt;
&lt;li&gt;You can explain attention's Q/K/V roles without analogy, in precise terms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 (quantitative, on your held-out eval set):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NER span-level F1 ≥ 0.75 on your hand-labeled eval set (BERT-base fine-tuned on ~100+ examples should clear this comfortably)&lt;/li&gt;
&lt;li&gt;Relation classifier per-class F1 ≥ 0.6 on &lt;code&gt;CAUSES&lt;/code&gt; specifically (the class you care most about); lower is acceptable for rarer/harder classes like &lt;code&gt;ENABLED_BY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pipeline handles at least 3 different genuinely messy real-world text samples without crashing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 (behavioral, verified visually + numerically):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Map Clarity Loss converges to a stable value (not oscillating) within 500 optimization steps on a graph of ~20 nodes&lt;/li&gt;
&lt;li&gt;Two independently-clustered causal groups in the same graph visually separate without being told to&lt;/li&gt;
&lt;li&gt;Freezing a node mid-optimization and resuming produces a stable re-layout where the frozen node's position never changes by more than floating-point rounding error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 4 (product-level):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;End-to-end latency from clicking "Generate" to seeing a laid-out graph is under ~10 seconds for a few paragraphs of input&lt;/li&gt;
&lt;li&gt;Drag-and-reoptimize visually completes (settles, stops moving) within ~2 seconds&lt;/li&gt;
&lt;li&gt;A first-time user, with zero explanation, successfully pastes text and drags a node within 60 seconds of opening the app&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  APPENDIX E: Interview Prep — Explaining ChronoWeave to a Hiring Manager
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;The 30-second version:&lt;/strong&gt;&lt;br&gt;
"I built a system that reads historical text, extracts cause-and-effect relationships between events using a fine-tuned Transformer, and lays those events out on a 2D map using gradient descent instead of a physics engine — meaning the layout is generated by backpropagating a custom loss function with respect to node coordinates, the same way you'd train a neural network, except the 'parameters' are positions on a canvas instead of weights."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anticipated follow-up questions and how to answer them:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"Why not just use a force-directed graph library?"&lt;/em&gt; — Because I needed the layout objective to express more than physical forces can naturally capture: e.g., a soft temporal-ordering constraint alongside causal clustering, with tunable relative weights between competing goals, and the ability to freeze arbitrary subsets of nodes mid-optimization while re-solving the &lt;em&gt;same&lt;/em&gt; objective for everyone else. Gradient descent on a custom differentiable loss generalizes to all of that; a spring simulation would need a bespoke mechanism bolted on for each one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"How did you handle relation extraction being noisy?"&lt;/em&gt; — I measured it rather than assumed it: held-out precision/recall/F1 per relation class, a confidence threshold before a triple gets displayed, and I designed the frontend to reflect uncertainty where the model itself is uncertain (e.g., visually softer distinction between relation types the model confuses most, per the confusion matrix).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"What was the hardest bug?"&lt;/em&gt; — Talk about one specific failure mode from Chapter 3 Week 12 in detail (e.g., the NaN-from-unmasked-diagonal bug) — specificity here is what separates "I used ML" from "I understand ML."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"Why build the optimizer from scratch instead of using &lt;code&gt;torch.optim.Adam&lt;/code&gt; directly?"&lt;/em&gt; — Because I needed per-node freezing during live re-layout, which meant controlling exactly how the moment buffers and update step interacted with a frozen mask — not something the standard API exposes, and writing it myself meant I fully understood what I was modifying.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"What would you do differently at scale?"&lt;/em&gt; — The repulsion loss term is O(n²); past a few hundred nodes I'd move to an approximate method (Barnes-Hut quadtree, the same technique classical force-directed layouts use), or a hierarchical approach that optimizes clusters independently before composing them.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;End of guide. This document covers the full architecture, all four learning phases, and the operational appendices for building ChronoWeave. Treat each chapter's code skeletons as a starting point to fill in and argue with — the goal was never for you to run code someone else wrote, but to be the one who could have written PyTorch's &lt;code&gt;autograd&lt;/code&gt; yourself, at least once, in miniature.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>learning</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Your Own Deep Learning Framework: A 3–5 Month Journey From Zero to Tiny Transformer</title>
      <dc:creator>Biki Kalita</dc:creator>
      <pubDate>Thu, 23 Jul 2026 14:24:34 +0000</pubDate>
      <link>https://dev.to/bikidev/building-your-own-deep-learning-framework-a-3-5-month-journey-from-zero-to-tiny-transformer-51gn</link>
      <guid>https://dev.to/bikidev/building-your-own-deep-learning-framework-a-3-5-month-journey-from-zero-to-tiny-transformer-51gn</guid>
      <description>&lt;h2&gt;
  
  
  Prologue: The Machine That Learns to Learn
&lt;/h2&gt;

&lt;p&gt;Somewhere in a research lab in the early 2010s, a small group of engineers sat staring at a whiteboard covered in arrows. They weren't drawing a neural network. They were drawing a &lt;em&gt;graph of computation&lt;/em&gt; — a map of every multiplication, every addition, every function that data would pass through on its way to becoming a prediction. That whiteboard sketch, refined over years, eventually became the beating heart of PyTorch: a system that doesn't just compute forward, but remembers how it got there so it can compute &lt;em&gt;backward&lt;/em&gt; — and learn.&lt;/p&gt;

&lt;p&gt;Here's the secret almost nobody tells beginners: PyTorch is not magic. It's not some impenetrable fortress of genius that only PhDs can understand. At its core, it's a few ideas, executed carefully:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Track every operation performed on a number.&lt;/li&gt;
&lt;li&gt;Remember how to undo that operation mathematically (its derivative).&lt;/li&gt;
&lt;li&gt;Walk backward through that history, multiplying derivatives together (the chain rule).&lt;/li&gt;
&lt;li&gt;Use the result to nudge numbers in a direction that reduces error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. That's the whole trick. Everything else — tensors, GPUs, layers, optimizers, transformers — is engineering built on top of that one idea.&lt;/p&gt;

&lt;p&gt;Over the next 3 to 5 months, you are going to rebuild that idea yourself, from the ground up, in Python. Not because the world needs another deep learning framework. But because there is no faster way to actually &lt;em&gt;understand&lt;/em&gt; deep learning than to build the thing that makes deep learning possible.&lt;/p&gt;

&lt;p&gt;This is not a copy-paste tutorial. I will not hand you a finished library. What I will hand you is a map, a set of checkpoints, the theory you need at each stage, the mistakes you're likely to make, and the tests that will tell you whether you're right. The actual code — the thinking, the debugging, the "aha" moments — that has to be yours. That's where the learning lives.&lt;/p&gt;

&lt;p&gt;Let's begin.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Why Build This At All?
&lt;/h2&gt;

&lt;p&gt;There are three ways to learn deep learning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a framework.&lt;/strong&gt; You import &lt;code&gt;torch&lt;/code&gt;, call &lt;code&gt;.backward()&lt;/code&gt;, and a neural network trains. You learn &lt;em&gt;what&lt;/em&gt; deep learning does, but the internals stay a black box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read papers and textbooks.&lt;/strong&gt; You learn the math in isolation, but it often stays abstract — symbols on a page that never quite connect to working code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the framework yourself.&lt;/strong&gt; You are forced to confront every hidden assumption, every edge case, every "wait, why does this even work?" moment, because nothing works until &lt;em&gt;you&lt;/em&gt; make it work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third path is slower. It is also the only one that produces the kind of deep, load-bearing understanding that shows up in job interviews, research intuition, and your ability to debug a model that refuses to train. When you've implemented backpropagation by hand, you will never again see a &lt;code&gt;loss.backward()&lt;/code&gt; call as a magic incantation. You'll see it as: "gradients flowing backward through a graph I understand, because I've built one."&lt;/p&gt;

&lt;p&gt;This project is also a phenomenal way to practice a skill that's rarely taught explicitly: &lt;strong&gt;reading the world's collective knowledge and turning it into your own working system.&lt;/strong&gt; You won't invent autodiff from nothing. You'll read blog posts, skim papers, study existing tiny frameworks like &lt;a href="https://github.com/karpathy/micrograd" rel="noopener noreferrer"&gt;micrograd&lt;/a&gt; and &lt;a href="https://github.com/tinygrad/tinygrad" rel="noopener noreferrer"&gt;tinygrad&lt;/a&gt; for &lt;em&gt;inspiration&lt;/em&gt; (not for copying), and synthesize that knowledge into code that is unmistakably yours. That synthesis skill — reading, understanding, rebuilding — is exactly what separates engineers who can only follow tutorials from engineers who can solve novel problems.&lt;/p&gt;

&lt;p&gt;By the end, you will have a working library that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically differentiate arbitrary chains of operations (autodiff)&lt;/li&gt;
&lt;li&gt;Represent and manipulate multi-dimensional arrays with broadcasting (a tensor engine)&lt;/li&gt;
&lt;li&gt;Compose neural network layers (Linear, activations, normalization)&lt;/li&gt;
&lt;li&gt;Compute losses and optimize weights (SGD, Adam)&lt;/li&gt;
&lt;li&gt;Train a small transformer to generate text, character by character&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And more importantly, you will understand &lt;em&gt;why&lt;/em&gt; every piece exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: The Big Picture — What You're Actually Building
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of code, hold the whole system in your head, even blurrily. Deep learning frameworks are built in layers, like geological strata. Each layer depends on the one below it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 5:  Transformer blocks (attention, feedforward, positional encoding)
Layer 4:  Training loop (forward → loss → backward → optimizer step)
Layer 3:  Losses &amp;amp; Optimizers (MSE, cross-entropy, SGD, Adam)
Layer 2:  Neural network layers (Linear, ReLU, Softmax, LayerNorm)
Layer 1:  Tensor engine (N-dimensional arrays, broadcasting, matmul)
Layer 0:  Autodiff engine (the core: track operations, compute gradients)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the direction of dependency: a transformer is just layers, arranged cleverly. Layers are just tensor operations, arranged into reusable objects. Tensor operations are just autodiff-tracked math. &lt;strong&gt;Everything above Layer 0 is organization. Layer 0 is the one true foundation.&lt;/strong&gt; This is why you'll spend real, unhurried time there before moving on — everything else stands or falls on whether your autodiff engine is correct.&lt;/p&gt;

&lt;p&gt;By the end of this project, you should be able to say, in plain language and without hand-waving:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I built a system where every math operation remembers its inputs and knows its own derivative. When I ask for gradients, the system walks backward through the history of operations it recorded, applying the chain rule at each step, until every parameter knows exactly how much it contributed to the final error — and therefore how it should change."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you can say that sentence and &lt;em&gt;mean&lt;/em&gt; it, mission accomplished. Everything else is detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Roadmap — Nine Phases Over 3 to 5 Months
&lt;/h2&gt;

&lt;p&gt;Here is the full journey. Don't worry about memorizing it — we'll walk through each phase in depth below. Treat this as the trail map you'll keep returning to.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Approx. Duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Python + NumPy foundations, mathematical warm-up&lt;/td&gt;
&lt;td&gt;1–2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Scalar autodiff engine (the heart of the project)&lt;/td&gt;
&lt;td&gt;2–3 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Tensor engine with broadcasting &amp;amp; matmul&lt;/td&gt;
&lt;td&gt;3–4 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Neural network layers (Linear, activations, init)&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Losses and optimizers (SGD, momentum, Adam)&lt;/td&gt;
&lt;td&gt;1–2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Training loop + first real experiments (XOR, MNIST-lite)&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Transformer components (attention, embeddings, LayerNorm)&lt;/td&gt;
&lt;td&gt;3–4 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Mini language model demo (character-level generation)&lt;/td&gt;
&lt;td&gt;2–3 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Cleanup, tests, documentation, packaging&lt;/td&gt;
&lt;td&gt;1–2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That totals roughly 17–24 weeks — comfortably inside your 3–5 month window, with room for the inevitable week where nothing works and you have to slow down. Budget for that week. It will happen, probably more than once, and it is not a sign of failure. It's a sign you've hit a real idea.&lt;/p&gt;

&lt;p&gt;A sane weekly rhythm: &lt;strong&gt;3–5 focused sessions per week, 1–3 hours each&lt;/strong&gt;, mixing reading/study with hands-on building. Deep learning concepts reward &lt;em&gt;spaced repetition&lt;/em&gt; — you'll understand backprop better the third time you re-derive it than the first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Python + NumPy Foundations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;Comfort with NumPy's array operations, broadcasting rules, and vectorized thinking — because your tensor engine will essentially be a thin, gradient-aware wrapper around NumPy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;If you don't deeply understand how NumPy broadcasts shapes like &lt;code&gt;(3, 1)&lt;/code&gt; and &lt;code&gt;(1, 4)&lt;/code&gt; into &lt;code&gt;(3, 4)&lt;/code&gt;, you will not understand why your tensor engine's gradients sometimes come out the wrong shape later. This bug bites &lt;em&gt;everyone&lt;/em&gt;, and the fix is always the same: understand broadcasting now, thoroughly, so you recognize the symptom later.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;NumPy's official broadcasting documentation (read it twice — once now, once after Phase 3)&lt;/li&gt;
&lt;li&gt;Any solid "vectorization vs. loops" tutorial that shows the same computation written both ways&lt;/li&gt;
&lt;li&gt;A refresher on matrix multiplication, dot products, and the chain rule from calculus (Khan Academy or 3Blue1Brown's "Essence of Calculus" series are excellent for building visual intuition)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A handful of small NumPy exercises: matrix multiply without &lt;code&gt;np.dot&lt;/code&gt;, broadcast a &lt;code&gt;(3,1)&lt;/code&gt; array against a &lt;code&gt;(1,4)&lt;/code&gt; array and predict the output shape before running it, implement &lt;code&gt;softmax&lt;/code&gt; and &lt;code&gt;sigmoid&lt;/code&gt; by hand.&lt;/li&gt;
&lt;li&gt;A "shape tracer" habit: for every array operation you write, say out loud what shape goes in and what shape comes out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tiny illustrative idea
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;   &lt;span class="c1"&gt;# shape (3, 1)
&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# shape (4,)
# predict the output shape BEFORE running a + b
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test yourself
&lt;/h3&gt;

&lt;p&gt;Before running any broadcasting expression, write down the predicted output shape on paper. Then check. If you're wrong more than once, stop and reread the broadcasting rules until you can predict shapes reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Assuming broadcasting "just works" without understanding the alignment rule (dimensions are compared from the &lt;em&gt;right&lt;/em&gt;, and must either match or be 1).&lt;/li&gt;
&lt;li&gt;Confusing element-wise multiplication (&lt;code&gt;*&lt;/code&gt;) with matrix multiplication (&lt;code&gt;@&lt;/code&gt; or &lt;code&gt;np.matmul&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  You're ready to move on when…
&lt;/h3&gt;

&lt;p&gt;You can look at two array shapes and confidently predict whether they'll broadcast together, and what shape the result will be — without running code to check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mini exercises
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Implement &lt;code&gt;softmax&lt;/code&gt; for a batch of vectors (shape &lt;code&gt;(batch, classes)&lt;/code&gt;) using only NumPy, handling numerical stability (subtract the max before exponentiating).&lt;/li&gt;
&lt;li&gt;Write a function that multiplies two matrices using nested Python loops, then verify it matches &lt;code&gt;A @ B&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 2: Scalar Autodiff — The Heart of Everything
&lt;/h2&gt;

&lt;p&gt;This is the phase that matters most. Take it slow. Everything you build later is a generalization of what you build here.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;How to build a system where every number "remembers" the operations that created it, so that gradients can be computed automatically via the chain rule. This is the single idea that makes deep learning trainable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;Backpropagation is often taught as an abstract algorithm on paper. Building it yourself turns it into something concrete: a graph of tiny objects, each holding a value, a gradient, and a memory of its parents. Once you've built this for scalars, tensors are "just" the same idea applied to arrays.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Andrej Karpathy's "The spelled-out intro to neural networks and backpropagation" video and the &lt;strong&gt;micrograd&lt;/strong&gt; repository. Watch it, understand the &lt;em&gt;ideas&lt;/em&gt;, then close the tab and build your own version from memory and understanding — don't transcribe his code.&lt;/li&gt;
&lt;li&gt;A clear explanation of computational graphs (search "computational graph backpropagation explained") — focus on the idea of a &lt;strong&gt;DAG (directed acyclic graph)&lt;/strong&gt; and &lt;strong&gt;topological sort&lt;/strong&gt;, because that's how you'll decide the correct order to walk backward through the graph.&lt;/li&gt;
&lt;li&gt;The chain rule, revisited specifically in the context of composed functions: if &lt;code&gt;z = f(y)&lt;/code&gt; and &lt;code&gt;y = g(x)&lt;/code&gt;, then &lt;code&gt;dz/dx = dz/dy * dy/dx&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;p&gt;Design a &lt;code&gt;Scalar&lt;/code&gt; (or &lt;code&gt;Value&lt;/code&gt;) class that wraps a single float and:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stores its numeric value&lt;/li&gt;
&lt;li&gt;Stores a gradient (&lt;code&gt;grad&lt;/code&gt;), initialized to zero&lt;/li&gt;
&lt;li&gt;Stores references to the "parent" scalars that created it (if any)&lt;/li&gt;
&lt;li&gt;Stores a function that knows how to propagate gradient &lt;em&gt;backward&lt;/em&gt; to those parents&lt;/li&gt;
&lt;li&gt;Overloads Python operators (&lt;code&gt;__add__&lt;/code&gt;, &lt;code&gt;__mul__&lt;/code&gt;, &lt;code&gt;__pow__&lt;/code&gt;, and later things like &lt;code&gt;tanh&lt;/code&gt; or &lt;code&gt;relu&lt;/code&gt;) so that writing normal-looking math (&lt;code&gt;c = a * b + a&lt;/code&gt;) automatically builds a graph behind the scenes&lt;/li&gt;
&lt;li&gt;Implements a &lt;code&gt;.backward()&lt;/code&gt; method that performs a topological sort of the graph, then walks it in reverse, accumulating gradients using the chain rule&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tiny illustrative idea
&lt;/h3&gt;

&lt;p&gt;Don't copy this — but here's the &lt;em&gt;shape&lt;/em&gt; of the thinking, for a single operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__mul__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_backward&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;
        &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_backward_fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_backward&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the pattern: every operation defines its own tiny local rule for how gradient flows backward to its inputs. That's the whole idea, repeated for every operation you support.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;This is where you build the habit that will save you for the rest of the project: &lt;strong&gt;compare your analytical gradient against a numerical gradient.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The numerical gradient of a function &lt;code&gt;f&lt;/code&gt; at point &lt;code&gt;x&lt;/code&gt; can be approximated as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(f(x + h) - f(x - h)) / (2h)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for a small &lt;code&gt;h&lt;/code&gt; (like &lt;code&gt;1e-5&lt;/code&gt;). If your hand-derived, backprop-computed gradient doesn't match this numerical approximation within a small tolerance, your backward pass has a bug — full stop, no exceptions. This technique is called &lt;strong&gt;gradient checking&lt;/strong&gt;, and it is the single most important debugging tool in this entire project. Build it early. Use it constantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting to zero out gradients between training steps (gradients &lt;em&gt;accumulate&lt;/em&gt; by design, so you must explicitly reset them, or they'll silently corrupt every subsequent step).&lt;/li&gt;
&lt;li&gt;Getting the local derivative wrong for a specific operation (e.g., writing the derivative of &lt;code&gt;x**2&lt;/code&gt; as &lt;code&gt;2&lt;/code&gt; instead of &lt;code&gt;2*x&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Forgetting that when a value is used &lt;em&gt;multiple times&lt;/em&gt; in a graph (e.g., &lt;code&gt;y = x + x&lt;/code&gt;), gradients from both usages must be &lt;strong&gt;summed&lt;/strong&gt;, not overwritten. This is the single most common bug in every autodiff implementation ever written by a beginner.&lt;/li&gt;
&lt;li&gt;Building the backward traversal in the wrong order (not respecting topological order), which causes gradients to be computed using stale, not-yet-fully-accumulated values.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  You're ready to move on when…
&lt;/h3&gt;

&lt;p&gt;You can build an arbitrary expression using &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;**&lt;/code&gt;, and &lt;code&gt;tanh&lt;/code&gt;, call &lt;code&gt;.backward()&lt;/code&gt; on the output, and have every intermediate scalar's &lt;code&gt;.grad&lt;/code&gt; match a numerically-computed gradient within &lt;code&gt;1e-4&lt;/code&gt; or so — for at least five different hand-built expressions, including ones where a variable is reused multiple times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mini exercises
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Build the expression &lt;code&gt;L = (a * b + c) ** 2&lt;/code&gt; for scalar values &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;. Call backward and verify all three gradients against numerical gradient checking.&lt;/li&gt;
&lt;li&gt;Deliberately introduce the "reused variable" bug (don't accumulate, just overwrite gradients) and observe how the numerical check catches it. This will burn the failure mode into your memory permanently.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 3: The Tensor Engine — Scaling Up From Scalars
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;How to generalize your scalar autodiff engine to operate on N-dimensional arrays (tensors), including the trickiest and most bug-prone part of any framework: &lt;strong&gt;gradient broadcasting.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;Real neural networks don't operate on individual numbers — they operate on batches of vectors and matrices, for speed and for the mathematical elegance of representing whole layers as matrix multiplications. This phase is where your framework becomes genuinely useful rather than a toy.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Revisit NumPy broadcasting rules, this time thinking specifically about the &lt;em&gt;reverse&lt;/em&gt; problem: if a tensor was broadcast during the forward pass (say, a &lt;code&gt;(1,4)&lt;/code&gt; bias added to a &lt;code&gt;(32,4)&lt;/code&gt; batch), how must the gradient be &lt;em&gt;summed back down&lt;/em&gt; to the original smaller shape during the backward pass? (Hint: you sum gradients over exactly the dimensions that were broadcast.)&lt;/li&gt;
&lt;li&gt;Matrix calculus basics — specifically the gradient of a matrix multiplication &lt;code&gt;C = A @ B&lt;/code&gt; with respect to &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt;. Search for "gradient of matrix multiplication backpropagation" and work through the derivation on paper before coding it.&lt;/li&gt;
&lt;li&gt;The tinygrad or micrograd source code, again for &lt;em&gt;inspiration and comparison after you've attempted your own design&lt;/em&gt; — not as a template to copy line by line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;Tensor&lt;/code&gt; class wrapping a NumPy array, with the same structure as your &lt;code&gt;Scalar&lt;/code&gt; class: value, gradient, parent references, and a local backward function per operation.&lt;/li&gt;
&lt;li&gt;Core operations: elementwise &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, matrix multiplication (&lt;code&gt;@&lt;/code&gt;), &lt;code&gt;sum&lt;/code&gt;, &lt;code&gt;mean&lt;/code&gt;, &lt;code&gt;reshape&lt;/code&gt;, and broadcasting-aware arithmetic.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.backward()&lt;/code&gt; method that performs topological sort and reverse traversal, just like your scalar engine — but now the "local backward rule" for each operation must handle array shapes correctly, including reducing broadcasted gradients back down.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tiny illustrative idea
&lt;/h3&gt;

&lt;p&gt;The conceptual shape of the fix for broadcast gradients (not literal code to paste, but the idea to implement yourself):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"After computing the gradient at the operation's output shape, if the input was broadcast to get there, sum the gradient over the broadcasted axes until it matches the input's original shape."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;Gradient checking again — but now applied to whole tensors. Perturb a single entry of a tensor by &lt;code&gt;h&lt;/code&gt;, recompute the forward pass, measure the change in the loss, and compare to your backprop-computed gradient at that entry. Do this for a random sample of entries across several test cases: elementwise ops, broadcasting ops, and matrix multiplication.&lt;/p&gt;

&lt;p&gt;Also write shape assertions liberally during development: after every backward operation, assert that the gradient's shape exactly matches the corresponding tensor's shape. Shape mismatches are the number one symptom of a broadcasting bug, and catching them immediately (rather than three layers later) will save you hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting to sum gradients back down after broadcasting (the gradient tensor ends up the wrong shape, or numerically wrong even if the shape happens to match).&lt;/li&gt;
&lt;li&gt;Getting the matrix multiplication gradient transposed incorrectly (a very common and very confusing bug — if &lt;code&gt;C = A @ B&lt;/code&gt;, then the gradient w.r.t. &lt;code&gt;A&lt;/code&gt; involves &lt;code&gt;B&lt;/code&gt; transposed, and the gradient w.r.t. &lt;code&gt;B&lt;/code&gt; involves &lt;code&gt;A&lt;/code&gt; transposed; get this backwards and your loss will often still &lt;em&gt;decrease&lt;/em&gt;, just slower or incorrectly, which makes the bug sneaky).&lt;/li&gt;
&lt;li&gt;Silent shape bugs: NumPy broadcasting is forgiving, so a wrong-shape gradient often doesn't crash — it just silently gives you wrong numbers. This is why explicit shape assertions matter more here than almost anywhere else in the project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  You're ready to move on when…
&lt;/h3&gt;

&lt;p&gt;You can build a small computation involving matrix multiplication, broadcasting (like adding a bias vector to a batch), and elementwise operations, call backward, and have every gradient match numerical gradient checking — and you understand, in your own words, why broadcasted gradients need to be summed back down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mini exercises
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Implement &lt;code&gt;y = X @ W + b&lt;/code&gt; where &lt;code&gt;X&lt;/code&gt; is &lt;code&gt;(batch, in_features)&lt;/code&gt;, &lt;code&gt;W&lt;/code&gt; is &lt;code&gt;(in_features, out_features)&lt;/code&gt;, and &lt;code&gt;b&lt;/code&gt; is &lt;code&gt;(out_features,)&lt;/code&gt;. Verify all three gradients numerically.&lt;/li&gt;
&lt;li&gt;Deliberately break the broadcasting-gradient-reduction step and observe what kind of numerical mismatch results — get familiar with what "wrong" looks like on this specific bug.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 4: Neural Network Layers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;How to organize tensor operations into reusable, composable objects — the &lt;code&gt;Module&lt;/code&gt; abstraction that every framework uses (PyTorch calls it &lt;code&gt;nn.Module&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;This is where your project starts to &lt;em&gt;feel&lt;/em&gt; like a real framework. You'll design an API elegant enough that stacking layers to build a network feels natural, not clunky.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PyTorch's &lt;code&gt;nn.Module&lt;/code&gt; documentation — not to copy the implementation, but to study the &lt;em&gt;interface design&lt;/em&gt;: how parameters are registered, how &lt;code&gt;forward()&lt;/code&gt; is called, how submodules nest inside each other.&lt;/li&gt;
&lt;li&gt;Weight initialization strategies (Xavier/Glorot, Kaiming/He) — read the original reasoning for &lt;em&gt;why&lt;/em&gt; initialization scale matters (poor initialization causes vanishing or exploding activations before training even starts).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A base &lt;code&gt;Module&lt;/code&gt; class with a &lt;code&gt;parameters()&lt;/code&gt; method that can recursively collect every trainable tensor from itself and its submodules.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Linear&lt;/code&gt; layer (&lt;code&gt;y = xW + b&lt;/code&gt;) with properly initialized weights.&lt;/li&gt;
&lt;li&gt;Activation functions as their own small modules or functions: &lt;code&gt;ReLU&lt;/code&gt;, &lt;code&gt;Tanh&lt;/code&gt;, &lt;code&gt;Sigmoid&lt;/code&gt;, &lt;code&gt;Softmax&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A way to compose modules sequentially (a &lt;code&gt;Sequential&lt;/code&gt; container, or simply nesting modules inside a custom class with a &lt;code&gt;forward&lt;/code&gt; method).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Forward-pass shape tests: build a small network, push a batch through it, and confirm the output shape is exactly what you expect at every layer.&lt;/li&gt;
&lt;li&gt;Initialization sanity checks: after initializing a &lt;code&gt;Linear&lt;/code&gt; layer, check that the &lt;em&gt;variance&lt;/em&gt; of its weights roughly matches what your chosen initialization scheme predicts (this catches typos in initialization formulas early, before they cause mysterious training failures three phases from now).&lt;/li&gt;
&lt;li&gt;Gradient flow sanity check: run a forward and backward pass through a multi-layer network and confirm gradients exist (are non-zero and non-&lt;code&gt;NaN&lt;/code&gt;) for every parameter, at every layer, not just the last one. This catches "dead" layers early.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Initializing weights to all zeros (a classic mistake — with zero weights, every neuron in a layer computes the exact same output and gradient, so they never differentiate from each other, a failure mode called &lt;strong&gt;symmetry&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Forgetting to zero gradients before each new backward pass, causing gradients to silently accumulate across training steps.&lt;/li&gt;
&lt;li&gt;Building &lt;code&gt;forward()&lt;/code&gt; methods that accidentally break the autodiff graph — for example, converting a &lt;code&gt;Tensor&lt;/code&gt; to a raw NumPy array partway through a computation, which severs the graph and makes &lt;code&gt;.backward()&lt;/code&gt; unable to reach earlier layers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  You're ready to move on when…
&lt;/h3&gt;

&lt;p&gt;You can build a 2–3 layer network with your &lt;code&gt;Module&lt;/code&gt; system, push a batch of random data through it, call backward, and confirm every single parameter (from the first layer to the last) receives a sensible, non-zero, non-&lt;code&gt;NaN&lt;/code&gt; gradient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mini exercises
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Build a &lt;code&gt;Sequential&lt;/code&gt; container that chains &lt;code&gt;Linear → ReLU → Linear&lt;/code&gt; and verify the output shape for a batch input.&lt;/li&gt;
&lt;li&gt;Deliberately zero-initialize a layer's weights and observe the symmetry problem directly — confirm that all neurons in that layer produce identical gradients.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 5: Losses and Optimizers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;How to measure "wrongness" (loss functions) and how to translate gradients into actual parameter updates (optimizers).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;Loss functions define &lt;em&gt;what&lt;/em&gt; the network is trying to learn. Optimizers define &lt;em&gt;how&lt;/em&gt; it gets there. A subtle bug in either one produces the most frustrating kind of failure: a model that trains, technically, but never quite gets good — and you won't immediately know which piece is at fault.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Mean Squared Error and Cross-Entropy loss — read a derivation of &lt;em&gt;why&lt;/em&gt; cross-entropy is the natural loss for classification (its connection to maximum likelihood estimation is worth understanding, not just memorizing the formula).&lt;/li&gt;
&lt;li&gt;The original SGD-with-momentum intuition (think of it as a ball rolling downhill, accumulating velocity) and the Adam paper's core idea (adaptive per-parameter learning rates using running estimates of the gradient's mean and variance).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MSELoss&lt;/code&gt; and &lt;code&gt;CrossEntropyLoss&lt;/code&gt; (careful with the numerically stable combination of softmax + cross-entropy — computing them separately can cause overflow; study how frameworks combine them into one numerically stable operation).&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;Optimizer&lt;/code&gt; base structure, then &lt;code&gt;SGD&lt;/code&gt; (with optional momentum) and &lt;code&gt;Adam&lt;/code&gt;, each implementing a &lt;code&gt;step()&lt;/code&gt; method that reads &lt;code&gt;.grad&lt;/code&gt; off every parameter and updates &lt;code&gt;.value&lt;/code&gt; accordingly, plus a &lt;code&gt;zero_grad()&lt;/code&gt; method.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Compare your loss function's output against a hand-computed value on a tiny example with numbers small enough to check by hand or calculator.&lt;/li&gt;
&lt;li&gt;Overfit a tiny synthetic dataset (even just 4–8 data points) with your optimizer and confirm the loss goes essentially to zero. If it can't even overfit a handful of points, something is fundamentally broken before you try anything harder.&lt;/li&gt;
&lt;li&gt;For Adam specifically, verify the bias-correction terms are implemented — a very common subtle bug is forgetting bias correction on the first few steps, which causes unstable early training.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Not zeroing gradients before each &lt;code&gt;step()&lt;/code&gt;, so gradients from previous batches contaminate the current update.&lt;/li&gt;
&lt;li&gt;Using the wrong sign in the update rule (subtracting instead of adding, or vice versa — always double check: parameters move in the &lt;em&gt;negative&lt;/em&gt; gradient direction to reduce loss).&lt;/li&gt;
&lt;li&gt;Learning rate too high (loss explodes or oscillates) or too low (loss barely moves) — learn to recognize both failure signatures by deliberately inducing them once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  You're ready to move on when…
&lt;/h3&gt;

&lt;p&gt;You can take a tiny synthetic dataset, run several hundred optimization steps with either SGD or Adam, and watch the loss curve monotonically (or nearly so) decrease toward zero — and if it doesn't, you know how to diagnose why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mini exercises
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Implement plain SGD and use it to fit a single &lt;code&gt;Linear&lt;/code&gt; layer to noisy linear data (&lt;code&gt;y = 3x + 2 + noise&lt;/code&gt;) — check that the learned weight and bias converge close to &lt;code&gt;3&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Implement Adam and compare its convergence speed against plain SGD on the same tiny problem — observe (don't just read about) why adaptive methods often converge faster on ill-conditioned problems.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 6: The Training Loop and First Real Experiments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;How to assemble everything into the actual training loop — the rhythm of forward pass, loss computation, backward pass, and optimizer step that every deep learning system repeats millions of times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;This is the moment your framework stops being a pile of isolated pieces and becomes a &lt;em&gt;system&lt;/em&gt;. It's also, often, the first moment something genuinely surprising happens: a model that actually learns a nontrivial function, using code you wrote entirely yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Classic explanations of the XOR problem and why it historically mattered (a single-layer perceptron &lt;em&gt;cannot&lt;/em&gt; solve it — this is a famous, important lesson in why depth and nonlinearity matter).&lt;/li&gt;
&lt;li&gt;General training loop patterns across frameworks — notice the near-universal shape: &lt;code&gt;zero_grad() → forward() → compute loss → backward() → step()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A full training loop for the XOR problem: 4 data points, a small 2-layer network with a nonlinear activation, and your loss + optimizer from Phase 5.&lt;/li&gt;
&lt;li&gt;Basic logging: print or plot the loss every N steps so you can watch it decrease (or fail to).&lt;/li&gt;
&lt;li&gt;If you're feeling ambitious, a tiny subset of MNIST (even just a few hundred examples, downsampled) as a slightly harder next test.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Watch the loss curve. It should decrease, generally, though not necessarily perfectly monotonically.&lt;/li&gt;
&lt;li&gt;Check final predictions against ground truth directly — for XOR, after training, does the network correctly predict &lt;code&gt;0&lt;/code&gt; for &lt;code&gt;(0,0)&lt;/code&gt; and &lt;code&gt;(1,1)&lt;/code&gt;, and &lt;code&gt;1&lt;/code&gt; for &lt;code&gt;(0,1)&lt;/code&gt; and &lt;code&gt;(1,0)&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;This is a good moment to also test &lt;strong&gt;overfitting a single batch&lt;/strong&gt; as a diagnostic reflex you'll use for the rest of your career: if a model can't overfit a tiny amount of data, it has a bug, not a "needs more data" problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting &lt;code&gt;zero_grad()&lt;/code&gt; at the start of each loop iteration (the single most common training-loop bug in every framework, home-built or professional).&lt;/li&gt;
&lt;li&gt;Using a network without a nonlinearity between layers, which collapses multiple linear layers into a mathematically equivalent single linear layer — and a single linear layer &lt;em&gt;cannot&lt;/em&gt; solve XOR, no matter how long you train it. If your XOR loss refuses to go below a certain plateau, this is the first thing to check.&lt;/li&gt;
&lt;li&gt;Learning rate mis-set for the specific optimizer (Adam and SGD often want quite different learning rate scales).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The documentary moment
&lt;/h3&gt;

&lt;p&gt;There will be a specific instant — maybe late at night, maybe after your third attempt at fixing a shape bug — when you run your training loop and watch the loss actually fall, and the XOR network actually gets every example right, using nothing but code you wrote from first principles. That moment is worth the whole project. Let yourself notice it when it happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  You're ready to move on when…
&lt;/h3&gt;

&lt;p&gt;Your framework can solve XOR reliably, and you can explain, without notes, why a single linear layer fundamentally cannot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mini exercises
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Train your XOR network and plot the decision boundary it learns (even a crude ASCII or matplotlib grid) — see the nonlinear boundary emerge visually.&lt;/li&gt;
&lt;li&gt;Remove the nonlinear activation from your network and confirm it now fails to solve XOR, exactly as theory predicts.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 7: Transformer Components
&lt;/h2&gt;

&lt;p&gt;This is the most ambitious and mathematically rich phase. Budget extra time here, and don't be discouraged if it takes longer than planned — this is genuinely advanced material, and moving slowly here is the difference between "I copied a transformer" and "I understand a transformer."&lt;/p&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;The architecture that underlies essentially all modern large language models: token embeddings, positional information, self-attention, multi-head attention, layer normalization, and residual connections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;Understanding attention from the ground up — implementing the query/key/value mechanism yourself, watching attention weights sum to 1 via softmax, seeing residual connections stabilize deep networks — turns transformers from a buzzword into a mechanism you genuinely understand. This is the difference that shows in technical interviews and in your ability to read modern ML papers without getting lost.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The original "Attention Is All You Need" paper — read it once quickly for the shape of the ideas, then again slowly, focusing specifically on the scaled dot-product attention formula and why the scaling factor (dividing by the square root of the key dimension) exists (it prevents softmax saturation for large dimensions).&lt;/li&gt;
&lt;li&gt;A visual walkthrough of self-attention (there are several excellent illustrated blog posts and diagrams online — search for "illustrated transformer" or "illustrated self-attention") to build geometric intuition before diving into the formula.&lt;/li&gt;
&lt;li&gt;Layer normalization and residual connections — understand these as stabilization techniques that make deep networks trainable, not as arbitrary architectural decoration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;Embedding&lt;/code&gt; layer (a lookup table mapping token indices to vectors — this is really just indexed access into a trainable matrix, with the indexing operation needing its own backward rule).&lt;/li&gt;
&lt;li&gt;Positional encoding (either the fixed sinusoidal version from the original paper, or a simpler learned positional embedding — the learned version is easier to implement correctly first).&lt;/li&gt;
&lt;li&gt;Scaled dot-product self-attention: compute queries, keys, and values via linear projections; compute attention scores as &lt;code&gt;Q @ K^T / sqrt(d_k)&lt;/code&gt;; apply softmax; multiply by &lt;code&gt;V&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Multi-head attention: run several attention "heads" in parallel on different projected subspaces, then concatenate and project back.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LayerNorm&lt;/code&gt; and residual (skip) connections wrapping your attention and feedforward sublayers.&lt;/li&gt;
&lt;li&gt;A causal mask for autoregressive generation (so a token at position &lt;code&gt;i&lt;/code&gt; can only attend to positions &lt;code&gt;≤ i&lt;/code&gt;, not the future).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tiny illustrative idea
&lt;/h3&gt;

&lt;p&gt;The conceptual shape of attention, not literal code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"For each token, compute how much it should 'attend to' every other token, using a similarity score between its query and every key. Normalize those scores with softmax so they form a valid weighting. Use those weights to compute a weighted average over the value vectors — that weighted average becomes the token's new, context-aware representation."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Shape tests, relentlessly: attention involves several reshapes and transposes for multi-head splitting, and shape bugs here are extremely common. At every step, know exactly what shape you expect and assert it.&lt;/li&gt;
&lt;li&gt;Attention weight sanity check: after softmax, confirm attention weights for each query sum to exactly 1 across the key dimension.&lt;/li&gt;
&lt;li&gt;Causal mask sanity check: confirm that after masking, a token's attention weights to &lt;em&gt;future&lt;/em&gt; positions are exactly zero, not just small.&lt;/li&gt;
&lt;li&gt;Gradient checking one more time, on a small attention block in isolation, before trusting it inside a larger model — attention has enough moving parts that isolating it for testing is worth the extra effort.&lt;/li&gt;
&lt;li&gt;Overfit a tiny synthetic sequence task (like copying a short sequence, or predicting the next character in a short repeating pattern) to confirm the whole transformer block can actually learn something before scaling up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting the causal mask, which lets the model "cheat" by looking at future tokens during training — a model trained this way often looks like it's learning beautifully, until you try to generate text with it and discover it never actually learned to predict anything, it just memorized answers using information it shouldn't have had access to.&lt;/li&gt;
&lt;li&gt;Getting the transpose wrong when splitting into multiple attention heads, silently mixing information across heads that should stay separate.&lt;/li&gt;
&lt;li&gt;Applying LayerNorm at the wrong point relative to the residual connection (pre-norm vs. post-norm architectures behave differently, and mixing conventions inconsistently causes subtle instability).&lt;/li&gt;
&lt;li&gt;Numerical instability in softmax over attention scores when the scaling factor is missing or wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  You're ready to move on when…
&lt;/h3&gt;

&lt;p&gt;You can build a single transformer block, push a small batch of token sequences through it, confirm shapes and attention-weight sums are correct at every step, confirm the causal mask genuinely blocks future information, and overfit a tiny toy sequence task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mini exercises
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Implement attention &lt;em&gt;without&lt;/em&gt; multi-head splitting first (single head), verify it fully, then generalize to multi-head — resist the urge to implement multi-head attention directly, since debugging the simpler version first will save you real time.&lt;/li&gt;
&lt;li&gt;Visualize attention weights (as a heatmap) for a short sequence and manually inspect whether they look sensible before and after training.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 8: The Mini Language Model Demo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;How to assemble embeddings, positional information, stacked transformer blocks, and a final projection layer into a complete, trainable, generative language model — and how to actually generate new text from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;This is the payoff phase. Everything from Phase 2 onward converges here: autodiff, tensors, layers, losses, optimizers, and attention, working together to do something that feels genuinely impressive — a small AI system, built by you, generating text one character at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Character-level language modeling as a task — read about why it's a good learning demo (small vocabulary, no tokenizer complexity, fast to train, easy to sanity-check output).&lt;/li&gt;
&lt;li&gt;Autoregressive sampling strategies: greedy decoding, temperature sampling, top-k sampling — understand at least greedy and temperature sampling before implementing generation.&lt;/li&gt;
&lt;li&gt;Cross-entropy loss specifically as applied to next-token prediction (the standard training objective for language models).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A small character-level dataset (a public domain text file works well — something like a short story or a chunk of Shakespeare).&lt;/li&gt;
&lt;li&gt;A vocabulary built from unique characters, with encode/decode functions between characters and integer indices.&lt;/li&gt;
&lt;li&gt;A small GPT-style model: embedding layer + positional encoding + a few stacked transformer blocks + a final linear layer projecting to vocabulary size.&lt;/li&gt;
&lt;li&gt;A training loop that samples random chunks of text, predicts the next character at every position, and computes cross-entropy loss.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;generate()&lt;/code&gt; function that starts from a seed string and autoregressively samples new characters one at a time, feeding each generated character back in as input for the next step.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Training loss should decrease steadily over time — plot it.&lt;/li&gt;
&lt;li&gt;Periodically, during training, generate a short sample of text and read it. Early on, it will be gibberish. Partway through, you'll start seeing real word-shapes and plausible letter combinations, even if the content is nonsensical. This progression — noise, to word-shapes, to something reading almost like language — is one of the most satisfying things you'll observe in this entire project.&lt;/li&gt;
&lt;li&gt;Compare generation with different temperatures: very low temperature should produce repetitive, "safe" text; higher temperature should produce more varied (and eventually incoherent) text. Seeing this tradeoff directly builds real intuition about sampling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Off-by-one errors in constructing input/target pairs for next-character prediction (the target sequence should be the input sequence shifted by one position — get this wrong and the model trains on a nonsensical objective, usually still producing &lt;em&gt;decreasing&lt;/em&gt; loss, which makes the bug sneaky).&lt;/li&gt;
&lt;li&gt;Forgetting the causal mask here specifically, which lets the model see future characters during training and then produce oddly perfect-looking loss curves that mean nothing, since real generation won't have access to the future.&lt;/li&gt;
&lt;li&gt;Training on a dataset too small to generalize, then being surprised that generated text is mostly memorized rather than genuinely learned patterns — a useful phase to observe and understand, not necessarily to "fix."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The documentary moment
&lt;/h3&gt;

&lt;p&gt;This is the second big emotional payoff of the project. The first time you seed your model with a few characters and watch it generate a full paragraph of text that &lt;em&gt;looks&lt;/em&gt; like language — even if it's utter nonsense semantically — you are watching a system you built, atom by atom, produce something that feels alive. Sit with that moment. It's earned.&lt;/p&gt;

&lt;h3&gt;
  
  
  You're ready to move on when…
&lt;/h3&gt;

&lt;p&gt;You have a full training run showing decreasing loss, and generated samples that show clear qualitative improvement from early training to late training — moving from random noise toward recognizable word-shapes and plausible local structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mini exercises
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Train on two very different text sources (say, one is repetitive and structured, one is more free-form) and compare how quickly and how well the model picks up each one's patterns.&lt;/li&gt;
&lt;li&gt;Implement and compare greedy decoding vs. temperature sampling on the same trained model, and describe in your own words the qualitative difference you observe.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Phase 9: Cleanup, Tests, Documentation, Packaging
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you're learning
&lt;/h3&gt;

&lt;p&gt;How to turn a working research prototype into something that looks and behaves like real, professional software — because a framework nobody else (including future-you) can read or trust isn't really finished.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;This phase is often skipped by beginners eager to move on to the next shiny project, but it's exactly the phase that turns "a thing I hacked together" into "a project I can show in an interview, link on my resume, and actually reuse." It also forces you to re-read your own code with fresh eyes, which reliably surfaces bugs you missed the first time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to study online
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Basic &lt;code&gt;pytest&lt;/code&gt; usage, if you haven't used a testing framework before — how to structure test files, write assertions, and run a full test suite with one command.&lt;/li&gt;
&lt;li&gt;Good README structure for open-source projects — look at a few well-regarded small ML repos on GitHub for inspiration on structure, not content.&lt;/li&gt;
&lt;li&gt;Basic Python packaging (a &lt;code&gt;pyproject.toml&lt;/code&gt; or &lt;code&gt;setup.py&lt;/code&gt;, so your library can be &lt;code&gt;pip install -e .&lt;/code&gt;'d locally).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to build yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A formal test suite covering: gradient checks for every operation, shape tests for every layer, an overfitting test for a tiny dataset, and a small end-to-end test that trains for a few steps and confirms loss decreases.&lt;/li&gt;
&lt;li&gt;A README explaining what the project is, how to install it, a quickstart example, and a short explanation of the architecture (this is also a superb exercise in explaining your own system clearly — if you struggle to explain a part, that's a signal you don't fully understand it yet).&lt;/li&gt;
&lt;li&gt;Docstrings and type hints across your public API.&lt;/li&gt;
&lt;li&gt;A couple of example scripts (XOR training, character-level generation) that a stranger could run to see the library work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;Run your full test suite from a clean checkout, ideally in a fresh virtual environment, to confirm there are no hidden dependencies on leftover state from your development process. Ask a friend, if possible, to follow your README's quickstart from scratch and tell you where they got confused — this is the single best way to find documentation gaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common mistakes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tests that only check "does it run" without checking "is the output correct" (a test that passes even when the math is subtly wrong is worse than no test at all, because it creates false confidence).&lt;/li&gt;
&lt;li&gt;A README that describes what you &lt;em&gt;intended&lt;/em&gt; to build rather than what actually exists in the code — keep them in sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  You're ready to call it done when…
&lt;/h3&gt;

&lt;p&gt;A stranger could clone your repository, follow your README, install the package, run the examples, and understand — at least at a high level — how the pieces fit together, without needing to ask you anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: The Debugging Mindset
&lt;/h2&gt;

&lt;p&gt;You will spend a significant fraction of this project's time debugging, not writing new code. That is not a sign you're doing it wrong — it's the nature of building something this foundational. A few principles that will serve you well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Numerical gradient checking is your best friend.&lt;/strong&gt; Reach for it constantly, especially any time you add a new operation to your autodiff engine. It is objective, mechanical, and doesn't lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overfit small before you scale up.&lt;/strong&gt; If your model can't perfectly memorize four data points, it has a bug — it does not need more data, a bigger model, or more training time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert shapes aggressively during development.&lt;/strong&gt; Most bugs in tensor code are shape bugs. Catching them the instant they occur, rather than three layers downstream, saves enormous time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate before you integrate.&lt;/strong&gt; Test each new component (an operation, a layer, an attention block) completely on its own, with its own small test cases, before wiring it into the larger system. Debugging a fully assembled transformer is much harder than debugging one attention block in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When something is "almost right," be suspicious, not relieved.&lt;/strong&gt; A model that trains but plateaus at a mediocre loss, or a gradient that's close but not quite matching the numerical check, is very often hiding a real bug — broadcasting summed over the wrong axis, a transpose in the wrong place — rather than being "good enough." Chase these down; they compound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce with the smallest possible example.&lt;/strong&gt; When something breaks in your full training loop, don't debug it there. Strip the problem down to the smallest snippet that reproduces the bug — often just two or three lines — and debug that instead.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 5: What Not To Do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't copy an existing tiny framework's source code line by line.&lt;/strong&gt; Reading micrograd, tinygrad, or similar projects for &lt;em&gt;conceptual&lt;/em&gt; inspiration is not just fine but encouraged. Transcribing their code without building your own understanding defeats the entire purpose and will leave you unable to debug your own system when it inevitably breaks in a way the original project didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't skip gradient checking "because it seems to be working."&lt;/strong&gt; Loss curves that go down can mask real bugs (a common one: gradients computed with a sign error can still sometimes decrease loss, just less efficiently, hiding the bug for a long time).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't jump straight to the transformer phase before your autodiff and tensor engine are solid.&lt;/strong&gt; Every bug you haven't found in Phase 2 or 3 will resurface, disguised and much harder to diagnose, somewhere deep inside your attention mechanism in Phase 7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't optimize for speed prematurely.&lt;/strong&gt; Your framework will be slow compared to PyTorch — that's fine and expected. This project is about correctness and understanding, not competing with a production framework backed by compiled CUDA kernels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't work in total isolation.&lt;/strong&gt; Read forum discussions, ask questions in ML-focused communities when you're stuck, and compare notes with other builders. Building alone doesn't mean learning alone.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 6: How to Actually Learn From the Internet While Building This
&lt;/h2&gt;

&lt;p&gt;Since you won't be handed finished code, your primary skill throughout this project is &lt;strong&gt;turning scattered online knowledge into working understanding.&lt;/strong&gt; Here's how to do that well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read broadly before you write code, narrowly while you're stuck.&lt;/strong&gt; Before starting a new phase, skim two or three different explanations of the core concept (a blog post, a paper, a video) to build a rounded mental model. Once you're implementing and hit a specific wall, narrow your search to that exact problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat existing tiny frameworks as reference material, not templates.&lt;/strong&gt; Open micrograd or tinygrad &lt;em&gt;after&lt;/em&gt; you've made your own honest attempt, specifically to compare your design decisions against theirs — not before, and not to copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Papers are denser than blog posts but often clearer once you know what to look for.&lt;/strong&gt; Read the "Attention Is All You Need" paper's method section only after you already have a rough intuition from a blog post or video — the paper will then click into place much faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub issues and forum threads are goldmines for edge cases.&lt;/strong&gt; If you hit a subtle bug (say, in broadcasting gradients or in causal masking), search for how others have described that exact symptom — someone has almost certainly hit it before you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write your own notes as you go.&lt;/strong&gt; A short paragraph per phase, in your own words, explaining what you built and why, is one of the highest-leverage habits in this entire project. It forces synthesis, and it becomes both your documentation and your future interview prep material.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Epilogue: What You'll Have, and Where To Go Next
&lt;/h2&gt;

&lt;p&gt;By the end of this project, you will have built — with your own hands, your own debugging sessions, your own late-night "oh, THAT'S why" moments — a working deep learning framework that can automatically differentiate arbitrary computations, represent and manipulate tensors with broadcasting, compose neural network layers, optimize them with SGD and Adam, and train a small transformer to generate text.&lt;/p&gt;

&lt;p&gt;More importantly, you will have built a kind of understanding that's very hard to get any other way. You will look at a PyTorch stack trace and recognize exactly what kind of shape mismatch or graph-disconnection bug is happening, because you've caused and fixed that exact bug in your own code. You'll read a new architecture paper and be able to mentally sketch how you'd implement its core mechanism, because you've implemented the building blocks it's made from. In interviews, when someone asks "how does backpropagation actually work," you won't recite a definition — you'll explain a system you built.&lt;/p&gt;

&lt;p&gt;This project also naturally sets up several strong next steps, whenever you're ready for them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU acceleration:&lt;/strong&gt; Extend your tensor engine to dispatch operations to a GPU backend, learning the basics of CUDA or a framework like CuPy along the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A larger, real transformer:&lt;/strong&gt; Scale your mini language model up on a real dataset, and explore techniques like learning rate scheduling, gradient clipping, and mixed precision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convolutional networks:&lt;/strong&gt; Apply the same "build it yourself" approach to a CNN, implementing convolution and pooling operations and their gradients from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributing to a real open-source ML framework:&lt;/strong&gt; With this foundation, reading and contributing to real frameworks like PyTorch or JAX becomes dramatically more approachable — you'll recognize the patterns immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever you build after this, you'll build it with a kind of foundational confidence that's genuinely rare — the confidence of someone who has personally taken deep learning apart, piece by piece, and put it back together again with their own hands.&lt;/p&gt;

&lt;p&gt;Now go write your first &lt;code&gt;Scalar&lt;/code&gt; class. The journey starts there.&lt;/p&gt;

</description>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>BugVault AI: Never Solve the Same Bug Twice (A VS Code Extension)</title>
      <dc:creator>Biki Kalita</dc:creator>
      <pubDate>Sat, 11 Jul 2026 02:59:30 +0000</pubDate>
      <link>https://dev.to/bikidev/bugvault-ai-never-solve-the-same-bug-twice-a-vs-code-extension-1n9h</link>
      <guid>https://dev.to/bikidev/bugvault-ai-never-solve-the-same-bug-twice-a-vs-code-extension-1n9h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzi4zz2vlrj5f2tc541nw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzi4zz2vlrj5f2tc541nw.jpg" alt="BugVault AI Banner" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As developers, we've all been there: staring at an error message, a nagging sense of déjà vu creeping in. "Didn't I fix this last month?" you wonder, before diving back into the same rabbit hole. This cycle of re-solving known issues is a silent productivity killer, draining precious time and mental energy.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;BugVault AI&lt;/strong&gt;, a new open-source VS Code extension designed to put an end to this frustration. BugVault acts as your personal — and team — debugging assistant, intelligently capturing errors, remembering their fixes, and warning you when a familiar foe reappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Debugging Déjà Vu
&lt;/h2&gt;

&lt;p&gt;Manual documentation is often outdated, Stack Overflow answers are generic, and human memory is fallible. The cost? Hours lost, momentum stalled, and the sheer frustration of tackling the same problem repeatedly. BugVault AI was built to address this core pain point, transforming how developers approach recurring bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BugVault AI Works: Intelligent Memory for Your Codebase
&lt;/h2&gt;

&lt;p&gt;BugVault AI integrates seamlessly into your VS Code workflow, operating silently in the background to provide a powerful, context-aware debugging experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌟 Key Features for Developers:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Automatic Error Capture&lt;/strong&gt;: BugVault passively monitors your terminal output, VS Code diagnostics (Problems tab), and build tasks. Errors are fingerprinted and stored without interrupting your flow.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;AI-Powered Solutions&lt;/strong&gt;: When you mark a bug as solved, BugVault leverages VS Code's built-in Language Model API (e.g., GitHub Copilot) to generate a concise, actionable fix description. This solution is highly contextual, drawing from your &lt;code&gt;git diff HEAD&lt;/code&gt; and relevant code snippets.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Semantic Memory Matching&lt;/strong&gt;: Beyond exact text matching, BugVault uses semantic search (powered by Supermemory) to identify similar bugs, even if the error message isn't identical. This means you get relevant solutions faster.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Shared Memory Mode&lt;/strong&gt;: Collaborate effortlessly. Point BugVault to a central Supermemory instance, and your entire team benefits from fixes discovered by teammates. Knowledge silos become a thing of the past.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Time Saved Counter&lt;/strong&gt;: Every time BugVault identifies a repeat bug and provides a solution, it logs approximately 15 minutes saved. Watch your productivity grow in the status bar: &lt;code&gt;$(watch) Saved ~X hrs&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Day in the Life with BugVault AI
&lt;/h2&gt;

&lt;p&gt;Imagine this workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Code Normally&lt;/strong&gt;: You're focused on writing code. BugVault is quietly capturing potential issues.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Gutter Hint&lt;/strong&gt;: An error pops up. If BugVault recognizes it, a subtle CodeLens annotation appears above the line: &lt;code&gt;$(bug) Seen Nx · fix: "..."&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Confidence Card&lt;/strong&gt;: A WebView card slides in, showing an AI-powered confidence score and the stored fix. If it's a team fix, it's clearly labeled &lt;code&gt;👥 Team Fix&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Mark as Solved&lt;/strong&gt;: You apply the fix. With a quick command, you mark the bug as solved, and BugVault generates and stores the solution for future reference.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Productivity Boost&lt;/strong&gt;: You move on, knowing that bug won't bother you or your team again.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;VS Code 1.90+&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Node.js &amp;amp; npm&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Supermemory Local&lt;/strong&gt; (for semantic search, can run locally or connect to a shared instance)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;AI Access&lt;/strong&gt; (e.g., GitHub Copilot extension)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Installation (from source):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/biki-dev/BugVault.git
&lt;span class="nb"&gt;cd &lt;/span&gt;BugVault
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the project in VS Code and press &lt;code&gt;F5&lt;/code&gt; to launch the Extension Development Host.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration:
&lt;/h3&gt;

&lt;p&gt;BugVault is highly configurable. Open VS Code Settings (&lt;code&gt;Ctrl+,&lt;/code&gt; / &lt;code&gt;Cmd+,&lt;/code&gt;) and search for &lt;code&gt;BugVault&lt;/code&gt; to adjust settings like &lt;code&gt;similarityThreshold&lt;/code&gt; or enable &lt;code&gt;sharedMemory.enabled&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contribute to BugVault AI
&lt;/h2&gt;

&lt;p&gt;BugVault AI is open-source and thrives on community contributions. Whether you want to report a bug, suggest a feature, or dive into the codebase, we welcome your involvement.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/biki-dev/BugVault" rel="noopener noreferrer"&gt;https://github.com/biki-dev/BugVault&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Issues&lt;/strong&gt;: &lt;a href="https://github.com/biki-dev/BugVault/issues" rel="noopener noreferrer"&gt;https://github.com/biki-dev/BugVault/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We've designed the architecture to be modular and easy to understand. Check out the &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; (or the Architecture Overview in the README) for a deeper dive into its components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Join the BugVault Community
&lt;/h2&gt;

&lt;p&gt;Stop solving the same bugs twice. Install BugVault AI today and reclaim your development time. We're excited to see how you use it and contribute to its evolution!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>IssueWatch — Never Miss a GitHub Issue That Matters to You</title>
      <dc:creator>Biki Kalita</dc:creator>
      <pubDate>Fri, 05 Jun 2026 09:57:39 +0000</pubDate>
      <link>https://dev.to/bikidev/issuewatch-never-miss-a-github-issue-that-matters-to-you-2k7</link>
      <guid>https://dev.to/bikidev/issuewatch-never-miss-a-github-issue-that-matters-to-you-2k7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;IssueWatch is a mobile app built with React Native and Expo that turns the chaos of open source issue tracking into a calm, intelligent feed delivered straight to your phone.&lt;/p&gt;

&lt;p&gt;The idea came from a real frustration. I wanted to contribute to open source, but I had no reliable way to know when a good issue dropped on a repository I cared about. By the time I found it, ten people had already commented. I was always late.&lt;/p&gt;

&lt;p&gt;So I built the tool I wished existed.&lt;/p&gt;

&lt;p&gt;You subscribe to GitHub repositories, pick the labels you care about — &lt;code&gt;good first issue&lt;/code&gt;, &lt;code&gt;bug&lt;/code&gt;, &lt;code&gt;help wanted&lt;/code&gt; — add optional keyword filters, and the app does the rest. The moment a matching issue opens, your phone gets a push notification. Not a generic one — an AI-triaged one that already tells you the severity, the type, and a one-line summary of what the issue is actually about before you even tap.&lt;/p&gt;

&lt;p&gt;IssueWatch is for contributors who want to be first. For maintainers who want to stay on top of incoming bugs. For anyone who lives in open source and needs a smarter way to filter the noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/70e-hFx3RDM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Biki-dev" rel="noopener noreferrer"&gt;
        Biki-dev
      &lt;/a&gt; / &lt;a href="https://github.com/Biki-dev/Opensource-issue-notify" rel="noopener noreferrer"&gt;
        Opensource-issue-notify
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Open-source GitHub issue tracker with real-time notifications, issue monitoring, and mobile alerts.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;IssueWatch&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/Biki-dev/Opensource-issue-notify/./mobile/assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FBiki-dev%2FOpensource-issue-notify%2FHEAD%2F.%2Fmobile%2Fassets%2Flogo.png" alt="IssueWatch Logo" width="240"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;About&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;IssueWatch is a powerful, open-source mobile application and backend system designed to help developers and teams stay on top of their GitHub issues. It provides real-time push notifications for new issues, comments, and activity on tracked repositories, ensuring you never miss a critical update. With features like personal GitHub token integration for enhanced rate limits and private repository access, and a tiered scheduler for efficient monitoring, IssueWatch streamlines your workflow and keeps you informed.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;✨ Features&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Notifications:&lt;/strong&gt; Get instant push notifications for new issues and comments on your tracked GitHub repositories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal GitHub Token Integration:&lt;/strong&gt; Connect your personal GitHub token to unlock higher API rate limits and track private repositories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiered Scheduler:&lt;/strong&gt; An intelligent backend scheduler optimizes GitHub API calls based on your token status, ensuring efficient and timely updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue Tracking &amp;amp; Detail View:&lt;/strong&gt; Follow specific issues, view their details, and monitor comment activity directly within…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Biki-dev/Opensource-issue-notify" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;IssueWatch started as a backend-heavy side project — a Node.js server that could poll GitHub and send push notifications. The core scheduler worked. The database models were solid. But the mobile app was incomplete, rough around the edges, and nowhere near ready to show anyone.&lt;/p&gt;

&lt;p&gt;Here is what was missing or broken when I picked it back up:&lt;/p&gt;
&lt;h3&gt;
  
  
  What existed:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Basic Express server with auth routes&lt;/li&gt;
&lt;li&gt;MongoDB models for users, repositories, subscriptions, notifications&lt;/li&gt;
&lt;li&gt;A working scheduler that polled GitHub on a timer&lt;/li&gt;
&lt;li&gt;A bare-bones React Native app with navigation set up&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I finished:
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Mobile App — rebuilt from rough screens to a polished product
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Designed and built the entire UI system from scratch — custom tab bar with animated indicators, skeleton loaders, haptic feedback, smooth moti animations throughout&lt;/li&gt;
&lt;li&gt;Built the Dashboard screen with live stats, an activity chart that reads from the notification database instead of hammering the GitHub API, and expandable issue cards&lt;/li&gt;
&lt;li&gt;Built the Subscriptions grid with mute, hide, and edit-labels inline actions&lt;/li&gt;
&lt;li&gt;Built the Inbox with swipe-to-dismiss, severity filters, repository filters, and the Follow feature&lt;/li&gt;
&lt;li&gt;Built the Following tab with full comment thread rendering, markdown support, maintainer badges, and new comment indicators&lt;/li&gt;
&lt;li&gt;Built the Repo Detail screen showing full GitHub metadata, README rendered in markdown, stats, and a one-tap refresh&lt;/li&gt;
&lt;li&gt;Built the GitHub Token Settings screen with live rate limit status&lt;/li&gt;
&lt;li&gt;Fixed the onboarding flow and authentication edge cases&lt;/li&gt;
&lt;li&gt;Added web platform support via platform-specific chart components&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Backend — hardened and extended
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Added AI triage using OpenRouter and NVIDIA as providers — every new issue is classified by severity, type, effort, and summarized in one sentence&lt;/li&gt;
&lt;li&gt;Built a tiered scheduler — default users check every 60 minutes, personal token users every 30, premium every 15&lt;/li&gt;
&lt;li&gt;Built the comment poller service — tracks followed issues, detects new comments, and sends targeted push notifications&lt;/li&gt;
&lt;li&gt;Added the issue tracker routes and MongoDB model for the Following feature&lt;/li&gt;
&lt;li&gt;Fixed duplicate notification bugs with atomic upserts and compound unique indexes&lt;/li&gt;
&lt;li&gt;Added AES-256-GCM encryption for stored GitHub tokens&lt;/li&gt;
&lt;li&gt;Added metadata fetching — stars, forks, watchers, open issues, PRs, license, topics, README — fetched at subscribe time and cached&lt;/li&gt;
&lt;li&gt;Built repository detail and refresh endpoints&lt;/li&gt;
&lt;li&gt;Added orphaned subscription cleanup on startup and a daily cron cleanup job&lt;/li&gt;
&lt;li&gt;Added push receipt verification running every 15 minutes via Expo's receipt API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between where this project was and where it is now is the entire product. The foundation was there. Everything you can see and use was built in this push to finish it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;Copilot was present throughout this build in ways that genuinely changed how fast I could move.&lt;/p&gt;

&lt;p&gt;The place it helped most was in the repetitive but critical work — the kind of code that has to be correct but follows clear patterns. Writing MongoDB aggregation pipelines, setting up axios interceptors, structuring the tiered cron scheduler, building out the push notification chunking logic with Expo's SDK — Copilot would get me 80% of the way there on the first suggestion, and I would refine the remaining 20% to fit the exact requirements of the project.&lt;/p&gt;

&lt;p&gt;For the React Native UI, Copilot was surprisingly good at completing component patterns once it understood the design system I was building. After writing two or three card components by hand, suggestions for the next one were almost exactly right — matching the shadow styles, the moti animation patterns, and the NativeWind class conventions I had established.&lt;/p&gt;

&lt;p&gt;Where I had to stay sharp was in the logic that required deep context about the whole system — like the interaction between the scheduler, the notification deduplication, and the push receipt verification. Copilot would suggest plausible-looking code that missed a subtle constraint. Knowing when to accept, when to modify, and when to ignore entirely was the real skill. Copilot made me faster. It did not replace thinking.&lt;/p&gt;

&lt;p&gt;The honest summary: this project would have taken significantly longer without it. Not because I could not write the code — but because the volume of work required to go from an incomplete side project to a finished, shippable product is enormous. Copilot compressed the time on the mechanical parts so I could spend my energy on the parts that actually required judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built With
&lt;/h2&gt;

&lt;p&gt;Built with React Native, Expo, Node.js, Express, MongoDB, GitHub API, Expo Push Notifications, and OpenRouter AI.&lt;/p&gt;

&lt;p&gt;Every notification. Every triage. Every comment update. Delivered to your pocket.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
    </item>
    <item>
      <title>Building a Reusable React Knowledge Graph Component: OKVE v0.3.0</title>
      <dc:creator>Biki Kalita</dc:creator>
      <pubDate>Tue, 07 Apr 2026 13:24:32 +0000</pubDate>
      <link>https://dev.to/bikidev/building-a-reusable-react-knowledge-graph-component-okve-v030-2do8</link>
      <guid>https://dev.to/bikidev/building-a-reusable-react-knowledge-graph-component-okve-v030-2do8</guid>
      <description>&lt;p&gt;I wanted a graph component that felt native in React apps, not a one-off visualization.&lt;br&gt;
So I built OKVE: a TypeScript-first knowledge graph component powered by D3.&lt;/p&gt;

&lt;p&gt;In v0.3.0, I focused on product UX:&lt;/p&gt;

&lt;p&gt;programmatic camera focus&lt;br&gt;
built-in node search&lt;br&gt;
group filtering&lt;br&gt;
keyboard deselect&lt;br&gt;
stats overlay&lt;br&gt;
PNG export API&lt;br&gt;
In this post I will break down the component API, implementation tradeoffs, and what I learned shipping it in a monorepo with a live demo.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Biki-dev/OKVE" rel="noopener noreferrer"&gt;https://github.com/Biki-dev/OKVE&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/@biki-dev/okve" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@biki-dev/okve&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am looking for product-oriented feedback:&lt;/p&gt;

&lt;p&gt;What API feels awkward?&lt;br&gt;
What is missing for production usage?&lt;br&gt;
What should be next: minimap, clustering, virtualization, or something else?&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>react</category>
      <category>npm</category>
    </item>
    <item>
      <title>I built an open-source CLI tool that turns your developer portfolio into a terminal experience</title>
      <dc:creator>Biki Kalita</dc:creator>
      <pubDate>Fri, 06 Mar 2026 14:03:11 +0000</pubDate>
      <link>https://dev.to/bikidev/i-built-an-open-source-cli-tool-that-turns-your-developer-portfolio-into-a-terminal-experience-28a4</link>
      <guid>https://dev.to/bikidev/i-built-an-open-source-cli-tool-that-turns-your-developer-portfolio-into-a-terminal-experience-28a4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmch3xn6r5sg2fpsvn362.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmch3xn6r5sg2fpsvn362.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;Ever wanted your developer portfolio to feel like a real hacker terminal?&lt;/p&gt;

&lt;p&gt;I just open-sourced &lt;strong&gt;own-term&lt;/strong&gt; — a framework that lets any developer showcase their portfolio directly in the terminal, runnable with a single &lt;code&gt;npx&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx own-term
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎨 &lt;strong&gt;6 built-in themes&lt;/strong&gt; — dark, light, hacker, neo, dracula, nordic&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;5 prompt styles&lt;/strong&gt; — git, time, dashboard, minimal, cyberpunk&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Interactive project browser&lt;/strong&gt; — pick a project, see details, open in browser&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Skills with progress bars&lt;/strong&gt; — or simple pill-style tags&lt;/li&gt;
&lt;li&gt;🔌 &lt;strong&gt;Plugin system&lt;/strong&gt; — extend with custom commands&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero config to start&lt;/strong&gt; — works out of the box&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to create your own
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;termfolio.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Full-Stack Developer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;promptStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/yourusername&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;you@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Project&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something awesome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/you/project&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TypeScript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Python&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Git&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Docker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VS Code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx own-term &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./termfolio.config.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;As developers we spend so much time building impressive things — but our portfolios are just another website. I wanted something that felt native to what we actually do: work in terminals.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Custom ASCII logo rendering&lt;/li&gt;
&lt;li&gt;Live theme switching (no restart needed)&lt;/li&gt;
&lt;li&gt;Tab completion &amp;amp; command history&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Contribute
&lt;/h2&gt;

&lt;p&gt;The project is open source and actively looking for contributors.&lt;br&gt;
Check the issues tab — several are tagged &lt;code&gt;good first issue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Biki-dev/own-term" rel="noopener noreferrer"&gt;https://github.com/Biki-dev/own-term&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop a comment if you build your own — I'd love to see it! 🚀&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>cli</category>
      <category>terminal</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
