<?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: Madhumitha Kolkar</title>
    <description>The latest articles on DEV Community by Madhumitha Kolkar (@madhumithakolkar).</description>
    <link>https://dev.to/madhumithakolkar</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%2F4032482%2F8d0c4f6e-c85f-494e-b264-f2a2ac9f8a3a.jpg</url>
      <title>DEV Community: Madhumitha Kolkar</title>
      <link>https://dev.to/madhumithakolkar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/madhumithakolkar"/>
    <language>en</language>
    <item>
      <title>I Rewrote My RL Agent in DeepMind's Neural Network Library - Day 9 (Haiku)</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Tue, 04 Aug 2026 15:00:55 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/i-rewrote-my-rl-agent-in-deepminds-neural-network-library-day-9-haiku-5d4e</link>
      <guid>https://dev.to/madhumithakolkar/i-rewrote-my-rl-agent-in-deepminds-neural-network-library-day-9-haiku-5d4e</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind :)&lt;/p&gt;




&lt;p&gt;Days 4 through 8 were raw JAX. Every weight matrix initialized by hand, every forward pass written out explicitly. It was intentional - I wanted to understand exactly what was happening under the hood.&lt;/p&gt;

&lt;p&gt;Day 9 is the payoff. Same Actor-Critic algorithm, rewritten in Haiku. DeepMind's neural network library.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What Haiku is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Haiku is what DeepMind researchers actually use. If you look at the code releases for AlphaFold, Acme (DeepMind's RL framework), or AlphaStar, the network definitions are in Haiku.&lt;/p&gt;

&lt;p&gt;It sits on top of JAX. You get all of JAX's superpowers - grad, jit, vmap - plus a cleaner way to define networks.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The one thing that makes Haiku different from PyTorch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In PyTorch, the model stores its own weights:&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MyNetwork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# weights hidden inside
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Haiku, weights are always separate from the network:&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;network&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# weights created here
&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&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="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# passed in explicitly
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels like more work at first. But it means your network is a pure function with no hidden state. JAX can jit it, vmap it, grad it - exactly like any other function. This is why DeepMind code is so composable.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The before and after&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before (raw JAX - what we wrote on Days 4-8):&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;init_actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;k1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&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.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;jnp&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="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;jnp&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&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;actor_forward&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;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tanh&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w1&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jax&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="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w2&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;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After (Haiku):&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;actor_fn&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hk&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;hk&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="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;jax&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;tanh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hk&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="n"&gt;jax&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;softmax&lt;/span&gt;&lt;span class="p"&gt;,&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;actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;without_apply_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor_fn&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same network. Same output. Half the code. No manual weight shapes. No room to accidentally write &lt;code&gt;(64, 16)&lt;/code&gt; instead of &lt;code&gt;(16, 64)&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What hk.transform actually does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hk.transform&lt;/code&gt; wraps your function into two methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;actor.init(key, sample_input)&lt;/code&gt; - runs the network once, captures all the weights it creates, returns them as a dictionary&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;actor.apply(params, input)&lt;/code&gt; - runs the forward pass using those params&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After &lt;code&gt;init&lt;/code&gt;, you have a plain dictionary of arrays. You can save it, load it, pass it to &lt;code&gt;jax.grad&lt;/code&gt;, inspect individual layers, merge it with another network's params. All things that are painful with PyTorch.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same gridworld. Same Actor-Critic algorithm. Same policy arrows pointing toward the goal. But when I printed the parameter structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;linear/w: shape (16, 64)
linear/b: shape (64,)
linear_1/w: shape (64, 4)
linear_1/b: shape (4,)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Haiku named and organized everything automatically. In the raw JAX version I had to track all of this myself.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Why I spent days on raw JAX before this&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because if you jump straight to Haiku, &lt;code&gt;actor.apply(params, x)&lt;/code&gt; looks like magic. You do not know what params is or why it needs to be passed in.&lt;/p&gt;

&lt;p&gt;Having built the thing manually for five days, I now know exactly what Haiku is doing behind the scenes. It is not magic. It is just a clean wrapper around the same dictionary-of-arrays pattern we wrote by hand.&lt;/p&gt;

&lt;p&gt;That is the version of understanding that research work requires. Not "I know how to use the library." But "I know what the library is doing."&lt;/p&gt;




&lt;p&gt;From Day 10, we start applying this to real problems. The gridworld served its purpose. Time to build something that actually goes on a resume.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;

&lt;p&gt;~ Madhumitha Kolkar ( index_0 )&lt;/p&gt;

</description>
      <category>jax</category>
      <category>reinforcementlearning</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>I Implemented the Algorithm Behind ChatGPT From Scratch - Day 8 (PPO).</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Fri, 31 Jul 2026 16:12:33 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/i-implemented-the-algorithm-behind-chatgpt-from-scratch-day-8-ppo-o3f</link>
      <guid>https://dev.to/madhumithakolkar/i-implemented-the-algorithm-behind-chatgpt-from-scratch-day-8-ppo-o3f</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind :)&lt;/p&gt;




&lt;p&gt;When people ask "how was ChatGPT trained?", the answer usually involves RLHF - Reinforcement Learning from Human Feedback. And the RL part of RLHF is PPO.&lt;/p&gt;

&lt;p&gt;Day 8 is PPO. And it turns out to be simpler than I expected.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The problem that PPO solves&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Actor-Critic (Day 7) worked, but it had an instability problem. A single episode with an unusually high or low return would push the network weights too far in one direction. The policy would change dramatically. Sometimes it would unlearn things it already knew. Training would collapse.&lt;/p&gt;

&lt;p&gt;The fix is elegant: put a speed limit on learning.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The one idea that is PPO&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After each update, PPO compares the new policy to the old one:&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;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_probability&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;old_probability&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If ratio = 1.0: nothing changed for this action.&lt;br&gt;
If ratio = 1.5: the new policy is 50% more likely to take this action.&lt;br&gt;
If ratio = 0.5: the new policy is 50% less likely.&lt;/p&gt;

&lt;p&gt;Then PPO clips this ratio. It says: I will not let you change by more than 20% in one update.&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;clipped_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&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="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;ratio&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;advantage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clipped_ratio&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;advantage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is literally it. One clip. One min. That is the entirety of what makes PPO different from Actor-Critic.&lt;/p&gt;

&lt;p&gt;The gradient from a clipped update becomes zero once the ratio hits the boundary. The policy stops updating further for that action in that step. Come back next batch and nudge it again if you want more.&lt;/p&gt;

&lt;p&gt;Small stable steps, every update.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The three things PPO adds over Actor-Critic&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Clipping (the main idea)&lt;/strong&gt;&lt;br&gt;
Keeps the policy from changing too much in one shot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Multiple epochs per batch&lt;/strong&gt;&lt;br&gt;
Actor-Critic uses each episode once. PPO collects a batch of episodes and then runs 4 gradient updates on that same data. More learning per episode. More sample efficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Entropy bonus&lt;/strong&gt;&lt;br&gt;
Entropy measures how spread out the action probabilities are. High entropy means the agent is still considering many options. Low entropy means it has collapsed to always picking one action.&lt;/p&gt;

&lt;p&gt;PPO adds a small reward for entropy:&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;total_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;policy_loss&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;critic_loss&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;entropy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the agent exploring longer before committing. Without it, policies can collapse to one action too early and get stuck.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What the training loop looks like&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;iteration&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;# collect 10 episodes with current policy
&lt;/span&gt;    &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;critic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;episodes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# PPO epochs: squeeze 4 updates out of this batch
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;epoch&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;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;actor_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clipped_ppo_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;critic_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;value_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;critic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two loops. Outer loop collects data. Inner loop extracts learning from that data. Actor-Critic had one loop. This is the reason PPO trains faster per episode collected.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I saw when I ran this&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The value estimates from the critic after training:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;states near goal:  +0.6 to +0.8
safe middle path:  +0.1 to +0.4
states near holes: -0.3 to -0.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critic mapped out the whole gridworld just by watching the actor fail and succeed. Nobody told it where the holes were. It figured it out.&lt;/p&gt;

&lt;p&gt;The policy arrows lined up cleanly. Same result as Q-learning (Day 4), DQN (Day 5), REINFORCE (Day 6), Actor-Critic (Day 7). Different algorithm every time. Same learned behavior.&lt;/p&gt;

&lt;p&gt;That pattern keeps hitting me. The Bellman intuition runs through all of it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Why PPO became the default&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three reasons: it is stable (the clip prevents collapse), it is simple (one extra line over Actor-Critic), and it is general (discrete actions, continuous actions, LLM fine-tuning - all the same algorithm).&lt;/p&gt;

&lt;p&gt;When OpenAI trained ChatGPT, they had humans rank responses. Those rankings became a reward signal. PPO optimized the language model against that signal. The RL loop you just implemented is the same loop, just with a very different environment and a very different reward function.&lt;/p&gt;

&lt;p&gt;When I built the forge project (a GRPO trainer), I did not fully understand why it worked the way it did. GRPO is PPO with one change: instead of a single critic estimating advantage, it runs a group of episodes and compares their returns to each other. Same clipping. Same stability. The group comparison replaces the critic.&lt;/p&gt;

&lt;p&gt;Five days ago I could not have explained that. Now I can.&lt;/p&gt;




&lt;p&gt;Day 9: Haiku. DeepMind's neural network library. We rewrite all of this in a fraction of the code, and it starts looking like actual research-grade JAX.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;

&lt;p&gt;~ Madhumitha Kolkar (index_0)&lt;/p&gt;

</description>
      <category>reinforcementlearning</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>python</category>
    </item>
    <item>
      <title>I Added a Second Brain to My RL Agent - Day 7 (Actor-Critic)</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Wed, 29 Jul 2026 04:05:19 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/i-added-a-second-brain-to-my-rl-agent-day-7-actor-critic-45h8</link>
      <guid>https://dev.to/madhumithakolkar/i-added-a-second-brain-to-my-rl-agent-day-7-actor-critic-45h8</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind.&lt;/p&gt;




&lt;p&gt;Day 6 ended with REINFORCE working. The agent learned to navigate the gridworld using probabilities instead of Q-values. Clean idea, clean code.&lt;/p&gt;

&lt;p&gt;But it was noisy. Training was slow. Some episodes the agent got lucky, some unlucky, and the network kept getting confused by the difference. The signal was there but it was buried in too much variance.&lt;/p&gt;

&lt;p&gt;Day 7 is the fix.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The basketball analogy that made it click&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a basketball player scores 15 points in a game. Is that good or bad?&lt;/p&gt;

&lt;p&gt;Depends entirely on what's normal for them. If they usually score 10, it's a great game. If they usually score 25, it's a bad one. The number alone tells you nothing. You need context.&lt;/p&gt;

&lt;p&gt;REINFORCE has no context. It sees "return = 0.7" and tries to update the network. But is 0.7 good or bad for that particular state? It has no idea. It just treats every positive return as good and every negative one as bad.&lt;/p&gt;

&lt;p&gt;This is why training is noisy. Lucky episodes push the network in the wrong direction. Unlucky ones push it away from good decisions.&lt;/p&gt;

&lt;p&gt;Actor-Critic adds the context.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Two networks, two jobs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Actor - same as REINFORCE. Takes a state, outputs action probabilities. Decides what to do.&lt;/p&gt;

&lt;p&gt;The Critic - new. Takes a state, outputs a single number: "how good is it to be in this state?"&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;actor_output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&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="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# probabilities over 4 actions
&lt;/span&gt;&lt;span class="n"&gt;critic_output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mf"&gt;0.72&lt;/span&gt;                     &lt;span class="c1"&gt;# estimated value of this state
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critic never picks actions. It just watches and judges. Its only job is to give the actor a better signal.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The advantage: the number that matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of using the raw return to update the actor, we compute:&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;advantage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actual_return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;critic_estimate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Advantage positive: this played out better than expected. Do this more.&lt;/li&gt;
&lt;li&gt;Advantage negative: this played out worse than expected. Do this less.&lt;/li&gt;
&lt;li&gt;Advantage zero: exactly average. No change needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one subtraction removes a huge amount of noise. The lucky episode problem disappears because "better than expected" is now a meaningful signal, not just a big number.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What the update looks like&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both networks update every episode. First the critic (so it gets better at estimating), then the actor (using the fresh estimates):&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;# critic: learn to predict returns accurately
&lt;/span&gt;&lt;span class="n"&gt;critic_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual_return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;critic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;# actor: same as REINFORCE but now uses advantage instead of raw return
&lt;/span&gt;&lt;span class="n"&gt;actor_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;advantage&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="n"&gt;probability_of_action_taken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critic trains like a simple regression. The actor trains like REINFORCE but with a cleaner signal. They improve together over time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I saw in the training output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code prints the critic's value estimates as a grid at the end. After training it looked roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;states near goal:    high value  (~0.7 to 0.9)
states near holes:   low value   (negative)
safe middle states:  medium value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critic learned the layout of the gridworld just by watching the actor play. Nobody told it where the holes were. It figured it out through experience.&lt;/p&gt;

&lt;p&gt;That was genuinely cool to watch.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Why this is the architecture that everything is built on&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I keep mentioning PPO and GRPO. Here is the direct line:&lt;/p&gt;

&lt;p&gt;PPO (used to train ChatGPT): actor-critic. Adds a clip to stop updates from overshooting.&lt;/p&gt;

&lt;p&gt;A3C (DeepMind, 2016): actor-critic. Runs many agents in parallel to collect more diverse experience.&lt;/p&gt;

&lt;p&gt;GRPO (DeepSeek-R1): actor-critic variant. Replaces the single critic with group comparisons across multiple episodes. Instead of "better than my estimate," it asks "better than the other episodes in this batch?"&lt;/p&gt;

&lt;p&gt;The forge project I built earlier in this series implements GRPO. When I built it I did not fully understand why the reward normalization worked the way it did. Now I do. It is computing advantage within a group.&lt;/p&gt;

&lt;p&gt;Understanding actor-critic was the missing piece.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The family tree in one place:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Actor-Critic
    |
    ├── A3C  (DeepMind, 2016)   - parallel actors
    ├── PPO  (OpenAI, 2017)     - clipped updates, trains ChatGPT
    ├── SAC  (Berkeley, 2018)   - entropy bonus, used in robotics
    └── GRPO (DeepSeek, 2024)   - group-relative advantage, trains R1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every single one: an actor that picks actions + something that estimates quality. The differences are in how they compute advantage and stabilize training.&lt;/p&gt;

&lt;p&gt;Day 8: we move into Haiku, DeepMind's neural network library. Same algorithms, much cleaner code. That is where the codebase starts looking like actual research code.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;

&lt;p&gt;~ Madhumitha Kolkar (index_0)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>python</category>
    </item>
    <item>
      <title>I Taught an Agent to Act Directly - No Q-Values Needed (Day 6: REINFORCE)</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Sat, 25 Jul 2026 05:05:19 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/i-taught-an-agent-to-act-directly-no-q-values-needed-day-6-reinforce-9cl</link>
      <guid>https://dev.to/madhumithakolkar/i-taught-an-agent-to-act-directly-no-q-values-needed-day-6-reinforce-9cl</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind :)&lt;/p&gt;




&lt;p&gt;Days 4 and 5 were value-based methods. Q-learning, DQN - the agent learns how good each action is, then picks the best one. The policy is implicit. It falls out of the Q-values as a side effect.&lt;/p&gt;

&lt;p&gt;Day 6 changes the approach entirely. What if you just... learn the policy directly?&lt;/p&gt;

&lt;p&gt;That is REINFORCE. And it is the foundation of every modern RL algorithm I care about - PPO, A3C, and GRPO (the algorithm behind DeepSeek-R1 that I built a small implementation of earlier in this journey).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The problem with DQN that I did not see until now :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DQN always picks the action with the highest Q-value. It is always confident. The only randomness comes from epsilon-greedy, which is literally just "sometimes ignore what you learned and act randomly."&lt;/p&gt;

&lt;p&gt;That is a hack. A useful hack, but still a hack.&lt;/p&gt;

&lt;p&gt;Also, DQN breaks completely for continuous actions. If your action is a steering angle between -180 and 180 degrees, you cannot have one Q-value per action - there are infinite actions. DQN has no answer for this.&lt;/p&gt;

&lt;p&gt;REINFORCE does.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The one change that makes REINFORCE :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In DQN, the network outputs Q-values :&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;q_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [2.1, 0.8, 1.4, 3.2]
&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;argmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# always pick the best
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In REINFORCE, the network outputs probabilities :&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;probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# [0.1, 0.3, 0.2, 0.4]
&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# sample from distribution
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole difference. The network now represents the policy directly. It says: given this state, here is how likely I think each action is. You sample from that - sometimes the 30% action wins, and that is fine.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;How it learns :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;REINFORCE runs a full episode first. Then it looks back at what happened :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If an action led to a high total reward: increase its probability.&lt;/li&gt;
&lt;li&gt;If an action led to a low or negative reward: decrease it.&lt;/li&gt;
&lt;li&gt;Scale the update by how large the reward was.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In math :&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;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;G_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;π&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a_t&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;G_t is the total discounted return from that timestep. log π is the log probability of the action you took. The negative sign is because JAX minimizes and we want to maximize return.&lt;/p&gt;

&lt;p&gt;In plain English: reward what worked, penalize what did not. That is it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I built today :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same 4x4 gridworld. But instead of Q-values, the network outputs action probabilities via softmax. The training loop looks like this:&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;for&lt;/span&gt; &lt;span class="n"&gt;episode&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;3000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;states&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rewards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_episode&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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;returns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rewards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gamma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.99&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="n"&gt;grads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;grad_fn&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;states&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opt_state&lt;/span&gt; &lt;span class="o"&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;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opt_state&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="n"&gt;optax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_updates&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;updates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three key differences from DQN :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No replay buffer - we use the episode as it happened&lt;/li&gt;
&lt;li&gt;No target network - no Bellman bootstrapping needed&lt;/li&gt;
&lt;li&gt;Compute returns from the end of the episode backwards&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After 3000 episodes, the policy arrows matched Days 4 and 5. Same gridworld. Same result. Completely different mechanism.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;One thing that clicked for me today :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;REINFORCE is Monte Carlo RL. It waits for the full episode, computes the actual return, then learns. DQN is temporal difference learning - it learns after every single step using an estimate of future value.&lt;/p&gt;

&lt;p&gt;Monte Carlo is more accurate in principle (you see what actually happened, not an estimate). But it is high variance - one bad episode can throw off your update badly. This is why REINFORCE can be slow and noisy compared to DQN.&lt;/p&gt;

&lt;p&gt;Every modern algorithm after REINFORCE is essentially trying to fix this variance problem. Actor-Critic methods combine both: a policy network that acts (REINFORCE) and a value network that estimates returns (like DQN) to reduce variance.&lt;/p&gt;

&lt;p&gt;That is Day 7. The actor-critic architecture.&lt;/p&gt;




&lt;p&gt;Also : I keep saying GRPO builds on this. Here is the direct line. GRPO runs multiple episodes in a group, computes relative returns within the group (instead of raw G_t), and uses those relative returns as the update signal. The policy gradient update structure is identical to REINFORCE. Just a smarter way of computing what "good" means.&lt;/p&gt;

&lt;p&gt;When I built the forge project a few days ago, I did not fully appreciate why the reward computation worked the way it did. Now I do.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;

&lt;p&gt;~ Madhumitha Kolkar (index_0)&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Replaced a Q-Table With a Neural Network and Everything Changed - Day 5 (DQN).</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Fri, 24 Jul 2026 03:06:41 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/i-replaced-a-q-table-with-a-neural-network-and-everything-changed-day-5-dqn-31ag</link>
      <guid>https://dev.to/madhumithakolkar/i-replaced-a-q-table-with-a-neural-network-and-everything-changed-day-5-dqn-31ag</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind :)&lt;/p&gt;




&lt;p&gt;Day 4 ended with a working Q-learning agent. A table of numbers that an agent used to navigate a gridworld. Clean, simple, satisfying.&lt;/p&gt;

&lt;p&gt;Day 5 started with one question: what happens when the state space is too large for a table?&lt;/p&gt;

&lt;p&gt;Imagine instead of a 16-cell grid, your state is the pixels of a video game screen. There are more possible Atari frames than atoms in the universe. You cannot have a table row for each one. The Q-table breaks immediately.&lt;/p&gt;

&lt;p&gt;DQN is the answer. And it is simpler than it sounds.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The one change that makes DQN&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Q-learning:&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;q_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q_table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In DQN:&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;q_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;neural_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# returns one Q-value per action
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole idea. Replace the table lookup with a neural network forward pass. The Bellman equation stays the same. Epsilon-greedy stays the same. The training loop structure stays the same.&lt;/p&gt;

&lt;p&gt;The network takes the current state as input and outputs a Q-value for every possible action. You still pick the highest one. The only difference is where the numbers come from.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Why neural networks generalise where tables cannot :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Q-table only knows about states it has explicitly visited. State 7 ? Only useful if the agent has been to state 7 before.&lt;/p&gt;

&lt;p&gt;A neural network learns patterns. If states 3, 7, and 11 all have similar features (say, they are all near a hole), the network learns that similarity and can make reasonable predictions for states it has never visited. This is what allows DQN to scale.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The two problems DeepMind had to solve :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before DeepMind's 2013 paper, people had been trying to combine neural networks with RL for years. It kept failing. The network would improve for a while and then suddenly collapse and forget everything.&lt;/p&gt;

&lt;p&gt;DeepMind solved this with two tricks that are now standard in every RL codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: Correlated samples :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In supervised learning you shuffle your dataset and sample random batches. This is important because the network needs diverse examples each step, not 32 consecutive frames from the same game session.&lt;/p&gt;

&lt;p&gt;In RL, consecutive transitions are extremely correlated. The agent is in state 4, goes to state 5, then state 6. Training on these in sequence is like training a classifier on 32 pictures of the same cat in a row.&lt;/p&gt;

&lt;p&gt;Fix: Experience Replay. Store every transition in a buffer. Sample randomly from it when training. Correlation broken.&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;replay_buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replay_buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_size&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="c1"&gt;# random, not sequential
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem 2: Moving targets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Bellman target is:&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;gamma&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But Q(next_state) comes from the same network you are updating. So every time you nudge the network, the target moves too. You are chasing a bullseye that runs away from you every time you take a step.&lt;/p&gt;

&lt;p&gt;Fix: Target Network. Keep two copies of the network. Update only the online network each step. Every 100 steps, copy the online network weights into the target network. The target network provides stable Bellman targets while the online network trains against them.&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;# use target network for stable bellman target
&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;gamma&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;target_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# update only the online network
&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;online_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;What I built today :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same 4x4 gridworld from Day 4. But instead of a Q-table, a three-layer neural network in raw JAX. Input: one-hot encoding of the state. Output: Q-values for all four actions.&lt;/p&gt;

&lt;p&gt;The training loop :&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;for&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;episode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;epsilon_greedy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;online_network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;replay_buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replay_buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&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="n"&gt;grads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;grad_fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;online_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;online_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;online_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grads&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;100&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="n"&gt;target_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;online_params&lt;/span&gt;  &lt;span class="c1"&gt;# refresh anchor
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After 1500 episodes, the policy arrows matched what Q-learning produced on Day 4. Same environment, same result, different mechanism. The DQN learned it too.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The thing that actually hit me today :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I printed the Section 7 comparison at the end of the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q-learning:  Q-values stored in a table (64 numbers)
DQN:         Q-values stored in a neural network
Same Bellman equation. Same epsilon-greedy. Same goal.
The only difference is where the Q-values come from.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six years in ML and I had never sat down and traced the line from Q-learning to DQN to AlphaGo. It is one continuous idea, each step replacing one component with something that scales better. Tables become networks. Networks become deeper. State spaces become images. Rewards become sparse. But the core Bellman intuition runs through all of it.&lt;/p&gt;

&lt;p&gt;That is the thing about learning from first principles. You stop using tools and start understanding systems.&lt;/p&gt;




&lt;p&gt;Also worth noting: this is the paper that put DeepMind on the map. "Playing Atari with Deep Reinforcement Learning", 2013. If you have not read it, it is surprisingly readable. The ideas are exactly what we implemented today, just applied to pixel inputs and 18 actions instead of 16 states and 4 actions.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;

&lt;p&gt;~ Madhumitha Kolkar (index_0)&lt;/p&gt;

</description>
      <category>reinforcementlearning</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>python</category>
    </item>
    <item>
      <title>I Built My First Reinforcement Learning Agent From Scratch - Day 4.</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Thu, 23 Jul 2026 03:18:05 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/i-built-my-first-reinforcement-learning-agent-from-scratch-day-4-2fp7</link>
      <guid>https://dev.to/madhumithakolkar/i-built-my-first-reinforcement-learning-agent-from-scratch-day-4-2fp7</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind :)&lt;/p&gt;




&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%2F4iszc8ekpbcm51lml5s8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4iszc8ekpbcm51lml5s8.png" alt="An image of a grid world in Q-learning, steps on how a grid world RL problem works and how an agent starts." width="686" height="715"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Today was different. Today the agent actually learned something.&lt;/p&gt;

&lt;p&gt;Days 1 through 3 were JAX fundamentals - arrays, gradients, jit, vmap. All of it was preparing for this. Day 4 is where reinforcement learning actually begins.&lt;/p&gt;

&lt;p&gt;I built a Q-learning agent from scratch and watched it navigate a gridworld. By the end of 2000 episodes, it was finding the goal reliably. No hand-coded rules. No labels. Just an agent figuring out the world through trial and error.&lt;/p&gt;

&lt;p&gt;Here is how it works.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The setup :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 4x4 grid. The agent starts at the top-left. The goal is at the bottom-right. There are holes scattered around. Fall in a hole and the episode ends with a negative reward. Reach the goal and you get a positive reward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; S   .   .   .
 .   H   .   H
 .   .   .   H
 H   .   .   G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S = start, G = goal, H = hole.&lt;/p&gt;

&lt;p&gt;The agent can move up, down, left, or right. That is it. 16 states, 4 actions.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What is a Q-value ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before writing any code, I had to understand this properly.&lt;/p&gt;

&lt;p&gt;A Q-value answers one question: if I am in this state and I take this action, how much total reward will I collect from here onwards if I play well?&lt;/p&gt;

&lt;p&gt;It is a table. Rows are states. Columns are actions. Every cell is a number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;         UP    DOWN   LEFT   RIGHT
state 0:  1.2   3.4    0.8    2.1
state 1:  0.5   0.9    2.3    1.1
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At state 0, the agent looks at the row, picks the highest number, and takes that action. That is the entire policy. The whole job of Q-learning is to fill this table with the right numbers.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The Bellman equation :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the start, every Q-value is zero. The agent knows nothing.&lt;/p&gt;

&lt;p&gt;After taking an action and getting a reward, it makes a small correction to the relevant cell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q(s, a) = Q(s, a) + alpha * (target - Q(s, a))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where target = reward + gamma * best Q-value in the next state.&lt;/p&gt;

&lt;p&gt;This is the Bellman equation. It says: the value of being here and taking this action should equal the immediate reward plus the best value available from where you end up.&lt;/p&gt;

&lt;p&gt;The agent does this correction thousands of times. Slowly the table fills with values that reflect how good each action actually is.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Exploration vs exploitation :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a problem. If the agent always picks the action with the highest Q-value, it never tries anything new. It might settle for a mediocre path because it never explored better ones.&lt;/p&gt;

&lt;p&gt;The solution is epsilon-greedy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With probability epsilon: pick a random action (explore)&lt;/li&gt;
&lt;li&gt;With the rest: pick the best known action (exploit)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Epsilon starts at 1.0 (fully random) and decays over time toward 0.01 (mostly greedy). At the start the agent wanders. By the end it has earned the right to trust its own table.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The training loop in code :&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;episode&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;START_STATE&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;choose_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;epsilon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&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;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Bellman update
&lt;/span&gt;        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;gamma&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;next_state&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;q_table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;q_table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_state&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="n"&gt;epsilon&lt;/span&gt; &lt;span class="o"&gt;=&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.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;epsilon&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.995&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole thing. No neural network. No backpropagation. Just a table being updated one cell at a time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What the agent learned :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After 2000 episodes, the policy arrows looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  &amp;gt;   &amp;gt;   v   v
  ^   H   v   H
  ^   &amp;gt;   v   H
  H   &amp;gt;   &amp;gt;   G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It learned to avoid holes. It learned the shortest path. It built this entirely from reward signals, no supervision.&lt;/p&gt;

&lt;p&gt;The reward curve told the whole story. Early episodes: negative reward, the agent kept falling in holes. Around episode 500: reward starts climbing. By episode 2000: consistently positive, consistently reaching the goal.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Why does this matter beyond a gridworld ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Q-learning is the foundation of DQN, which is the foundation of AlphaGo, which is the foundation of everything DeepMind built in the RL space. The gridworld feels toy-sized but the mechanics are identical. The only difference in modern deep RL is that the Q-table gets replaced by a neural network because the state space is too large to enumerate.&lt;/p&gt;

&lt;p&gt;That is literally the next step in this series.&lt;/p&gt;




&lt;p&gt;One thing I genuinely did not expect: how satisfying it is to watch an agent learn. There is something different about seeing a system figure something out on its own versus supervising it toward a known answer. I think this is why RL has such a hold on people who work in it.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;

&lt;p&gt;~ Madhumitha Kolkar (index_0)&lt;/p&gt;

</description>
      <category>reinforcementlearning</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The JAX Superpower Nobody Talks About Enough - vmap - Day 3.</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 06:02:06 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/the-jax-superpower-nobody-talks-about-enough-vmap-day-3-57nb</link>
      <guid>https://dev.to/madhumithakolkar/the-jax-superpower-nobody-talks-about-enough-vmap-day-3-57nb</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind :)&lt;/p&gt;




&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%2Fitiouxzip6j0y2g0lele.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitiouxzip6j0y2g0lele.png" alt="An image showing the code implementation to use vmap in jax and how it helps in massive parallelism." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Everyone talks about JAX's automatic differentiation and JIT compilation. Those are great. But vmap is the one that made me actually understand why research teams at places like DeepMind and Google Brain write everything in JAX. It is also the one that is hardest to explain with a one-liner, so let me take a proper shot at it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The problem :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You have a function that works on one input. A single vector, a single example, a single data point.&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;score&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jnp&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;x&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have a batch of 1000 inputs and you need to run this on all of them.&lt;/p&gt;

&lt;p&gt;The Python way:&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;score&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# slow loop
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PyTorch way: rewrite your function to handle batches explicitly, add batch dimensions, make sure everything broadcasts correctly. Tedious. Error-prone.&lt;/p&gt;

&lt;p&gt;The JAX way: vmap !!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What vmap does :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;vmap takes a function written for a single input and returns a new function that runs on a whole batch, in parallel, without you changing anything.&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;batch_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;batch_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# runs on all 1000 inputs at once
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You wrote score thinking about one input. JAX handles the batching. Same results as the loop, but running as a single vectorised operation on the hardware.&lt;/p&gt;

&lt;p&gt;I benchmarked this today. The vmap version was 32 times faster than a Python loop on CPU. On a GPU, that gap is even wider.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The in_axes argument :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one thing about vmap that needs a second read. Some arguments you want to batch over, and some you want to keep fixed across the whole batch.&lt;/p&gt;

&lt;p&gt;Example: you have a loss function with a shared weight vector w, but different inputs x and targets y for each example.&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;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&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;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&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="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&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;batch_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vmap&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="n"&gt;in_axes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in_axes=(None, 0, 0) means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;None: do not batch over w. It is shared across all examples.&lt;/li&gt;
&lt;li&gt;0: batch over the 0th axis of x. One x per example.&lt;/li&gt;
&lt;li&gt;0: batch over the 0th axis of y. One y per example.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is it. You tell vmap which arguments vary across the batch and which stay fixed.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The real JAX pattern: jit + vmap + grad together :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what DeepMind code looks like. One line, all three superpowers:&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;fast_batched_grad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loss_fn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;in_axes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your loss function is now differentiable, batched across all examples simultaneously, and compiled for speed. In PyTorch this would take 10-15 lines and careful attention to tensor shapes.&lt;/p&gt;

&lt;p&gt;I ran a full batched gradient descent loop today with this pattern. w started at [0, 0, 0] and converged to [2, 3, 5] in 115 steps with loss hitting exactly 0.0. I played with the iteration count to find that number and found the convergence satisfying to watch.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Why this matters for research :&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you are running experiments on large models, you often want to compute gradients for an entire batch of examples simultaneously. Without vmap you either loop (slow) or rewrite your functions to be batch-aware (messy and error-prone at scale).&lt;/p&gt;

&lt;p&gt;With vmap you write clean single-example functions and compose them up to batch scale. Your code stays readable. Your functions are testable on one example at a time. And vmap handles the rest.&lt;/p&gt;

&lt;p&gt;This is why JAX code in research papers is often remarkably compact compared to equivalent PyTorch. The functions stay simple. The composability does the scaling.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The summary I keep in my notes :&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want&lt;/th&gt;
&lt;th&gt;How you get it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPU acceleration&lt;/td&gt;
&lt;td&gt;automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradients&lt;/td&gt;
&lt;td&gt;jax.grad(fn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;jax.jit(fn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch without a loop&lt;/td&gt;
&lt;td&gt;jax.vmap(fn)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All three&lt;/td&gt;
&lt;td&gt;jax.jit(jax.vmap(jax.grad(fn)))&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Next up: Day 4, which is where things get genuinely exciting. Q-learning from scratch. First real reinforcement learning algorithm. I built a gridworld environment in pure JAX and trained an agent to navigate it. See you there.&lt;/p&gt;

&lt;p&gt;If you have been following along: which of the four superpowers surprised you most? For me it was vmap. I expected grad to be the revelation but vmap is the one that changed how I think about writing ML code.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;

&lt;p&gt;~ Madhumitha Kolkar (index_0)&lt;/p&gt;

</description>
      <category>jax</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>I Built a Neural Network Training Loop in 5 Lines Using JAX - Day 2</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Tue, 21 Jul 2026 04:24:25 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/i-built-a-neural-network-training-loop-in-5-lines-using-jax-day-2-1a4j</link>
      <guid>https://dev.to/madhumithakolkar/i-built-a-neural-network-training-loop-in-5-lines-using-jax-day-2-1a4j</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind :)&lt;/p&gt;




&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%2Fjoi3sfr0rd91dluf0xpn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjoi3sfr0rd91dluf0xpn.png" alt="A glowing loss curve descending toward zero against a dark background, with a dial and mathematical gradient arrows floating beside it. Deep blue and orange tones, cinematic lighting, minimal and clean, digital art style." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;On Day 1 I covered what JAX is and the three superpowers: GPU acceleration, grad, and jit. Today I actually used them. And the moment where I manually verified a gradient by hand and watched JAX confirm it is the one that made everything real for me.&lt;/p&gt;

&lt;p&gt;But before the code, I had to understand what a gradient actually is. Because I realised I had been using them for years without a clean mental model.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What is a derivative, really?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you are adjusting a dial on a machine. The dial controls the volume. Your goal is to get the volume to exactly 50. Right now it is at 30.&lt;/p&gt;

&lt;p&gt;You turn the dial a tiny bit. The volume goes up.&lt;/p&gt;

&lt;p&gt;The derivative answers one question: if I nudge this dial by a tiny amount, how much does the output change?&lt;/p&gt;

&lt;p&gt;That is it. In math:&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) = x * x
f(3) = 9
f(3.001) = 9.006001

nudge of 0.001 caused a change of 0.006
rate of change = 0.006 / 0.001 = 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The derivative of x squared at x equals 3 is 6. Meaning: at this point, if x increases by a tiny amount, the output increases 6 times as fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a gradient?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A derivative is for a function with one input. A gradient is the same idea for a function with many inputs. It is just a list of derivatives, one per input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function with 1 input  -&amp;gt;  derivative  (a single number)
function with N inputs -&amp;gt;  gradient    (a list of N numbers)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why does this matter for ML?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every neural network has weights. Training means finding the weights that make the model least wrong. "How wrong" is measured by a number called the loss.&lt;/p&gt;

&lt;p&gt;The gradient of the loss tells you: for each weight, does increasing it make the loss bigger or smaller, and by how much?&lt;/p&gt;

&lt;p&gt;Nudge every weight in the direction that reduces the loss. Repeat thousands of times. That is gradient descent. That is literally how every neural network on the planet trains.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Now the code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First I verified grad by hand:&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;jax&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;square&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="k"&gt;return&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;x&lt;/span&gt;

&lt;span class="n"&gt;grad_of_square&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;square&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="nf"&gt;grad_of_square&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 6.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Derivative of x squared is 2x. At x equals 3, that is 6. JAX says 6.0. Verified.&lt;/p&gt;

&lt;p&gt;Then I wrote a real loss function:&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;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&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="mf"&gt;2.0&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;
    &lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&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;prediction&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&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;grad_of_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grad&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loss measures how wrong a prediction is. The ideal weight is 5.0 because 5 times 2 equals 10.&lt;/p&gt;

&lt;p&gt;Then gradient descent from scratch:&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;learning_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&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;5&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="nf"&gt;grad_of_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;learning_rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;grad&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;step &lt;/span&gt;&lt;span class="si"&gt;{&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;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: w = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, loss = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;step 1: w = 2.6000, loss = 21.1600
step 2: w = 3.6640, loss = 8.6491
step 3: w = 4.3302, loss = 3.5369
step 4: w = 4.7481, loss = 1.4461
step 5: w = 5.0105, loss = 0.0044 (almost zero, w approaching 5.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That loop is the core of every neural network training run. More weights, more complex functions, but the same mechanic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The jit speedup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I benchmarked a large computation with and without jit over 100 runs:&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;slow_computation&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jnp&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;jnp&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="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&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="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="n"&gt;fast_computation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slow_computation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: 32x speedup on my MacBook CPU. On a GPU this goes to 100x or more.&lt;/p&gt;

&lt;p&gt;The first call is always slower because that is when JAX compiles. Every call after uses the cached compiled version. One catch: if you change the shape of your input, JAX recompiles. So keep your array shapes consistent.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;One question I had: do I need to memorise all this syntax?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The important thing is knowing what to use and when. Nobody in a research role writes jax.grad from memory every time - they know it exists, they know what it does, they look up the exact syntax when needed.&lt;/p&gt;

&lt;p&gt;What you do need to carry in your head: grad differentiates, jit compiles, and they compose together. That mental model handles 90% of situations.&lt;/p&gt;




&lt;p&gt;Day 3 tomorrow: vmap, the fourth JAX superpower. This one genuinely surprised me.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Madhumitha Kolkar (index_0)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>jax</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>I Started Learning JAX as a Senior ML Engineer - Here's My First Impression.</title>
      <dc:creator>Madhumitha Kolkar</dc:creator>
      <pubDate>Mon, 20 Jul 2026 05:41:20 +0000</pubDate>
      <link>https://dev.to/madhumithakolkar/i-started-learning-jax-as-a-senior-ml-engineer-heres-my-first-impression-bm3</link>
      <guid>https://dev.to/madhumithakolkar/i-started-learning-jax-as-a-senior-ml-engineer-heres-my-first-impression-bm3</guid>
      <description>&lt;p&gt;SERIES: Learning RL and JAX in Public - from zero to DeepMind !&lt;/p&gt;




&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%2Fz0y5rhsflp47ubxaiggm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0y5rhsflp47ubxaiggm.png" alt="A glowing Python logo transforming into a JAX array grid, floating above a dark background with GPU chip patterns. Deep blue and teal tones, cinematic lighting, minimal and clean, digital art style." width="742" height="232"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I have been a machine learning engineer for six years. PyTorch is basically muscle memory at this point. So when I decided to seriously learn JAX, I expected it to feel like switching keyboards - same idea, different layout.&lt;/p&gt;

&lt;p&gt;It is not that. JAX is a genuinely different way of thinking about numerical computing. And on Day 1, I want to share the mental model that made it click for me.&lt;/p&gt;




&lt;p&gt;If you already know NumPy, you already know 80% of JAX's syntax. That part is intentional. The JAX team designed the API to mirror NumPy almost exactly so the learning curve stays shallow.&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;# NumPy
&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="n"&gt;x&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="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&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="mf"&gt;3.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;np&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;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 6.0
&lt;/span&gt;
&lt;span class="c1"&gt;# JAX - identical
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jax.numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;jnp&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;jnp&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="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&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="mf"&gt;3.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;jnp&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;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 6.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same thing. Where JAX diverges is in three superpowers that NumPy simply does not have.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Superpower 1: It runs on GPU and TPU automatically&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No .to('cuda'). No .cuda(). JAX looks for available accelerators and uses them. You write the same code whether you are on a laptop CPU or a cloud TPU. This alone is huge if you work across different environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Superpower 2: jax.grad - automatic differentiation as a function&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In PyTorch, you call .backward() on a loss tensor. In JAX, grad is a standalone function transformer. You hand it any Python function, it hands you back a new function that computes the gradient of the original.&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;jax&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;square&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&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;derivative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;square&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="nf"&gt;derivative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 6.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The derivative of x squared is 2x. At x equals 3, that is 6. JAX confirms it instantly. The fact that this works on any arbitrary Python function is what makes JAX powerful for research.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Superpower 3: jax.jit - compile your code for speed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Python is slow by default. jit (Just-In-Time compilation) takes your function, compiles it to optimised machine code on the first call, and every subsequent call skips Python entirely.&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;fast_fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First call is slower because of compilation. Every call after that is significantly faster. On Day 2 I benchmarked this and got a 32x speedup. More on that in the next post.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The one rule that surprised me: no in-place mutation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PyTorch lets you modify arrays in place. JAX does not. Every operation creates a new array. This feels wrong for about two days. Then you realise it is what makes grad and jit mathematically clean - they need functions to be pure (same input always gives same output). Mutation breaks that.&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;# This errors in JAX
&lt;/span&gt;&lt;span class="n"&gt;x&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="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;

&lt;span class="c1"&gt;# Do this instead
&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;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;at&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# creates a new array
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Why top research labs use JAX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The more I dig into JAX, the more I understand why the most ambitious ML research happening today is built on it. Labs like Google DeepMind run their entire research stack on JAX and Flax, and it is not arbitrary. When you are training models across hundreds of TPUs, you need code that is compiled, mathematically clean, and composable at scale. The combination of jit and grad doing exactly that is genuinely elegant. I find myself wanting to understand not just how to use these tools but why they were designed this way, and that curiosity is pushing me to go deeper into the fundamentals than I ever did with PyTorch. There is a certain level of ML engineering where understanding the infrastructure is just as important as understanding the models, and JAX sits right at that intersection.&lt;/p&gt;




&lt;p&gt;Day 1 was arrays and basic operations. Simple stuff. But getting the mental model right before writing complex code matters more than rushing into the deep end.&lt;/p&gt;

&lt;p&gt;Day 2 is grad, jit, and a gradient descent loop from scratch. See you there.&lt;/p&gt;

&lt;p&gt;If you are on a similar journey from PyTorch to JAX, I would love to hear what tripped you up first. Drop it in the comments.&lt;/p&gt;

&lt;p&gt;All code from this series, organised by day, is on my GitHub: &lt;a href="https://github.com/MadhumithaKolkar/jax-rl-lab" rel="noopener noreferrer"&gt;https://github.com/MadhumithaKolkar/jax-rl-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy learning everyone !&lt;/p&gt;




&lt;p&gt;~ Madhumitha Kolkar (index_0)&lt;/p&gt;

</description>
      <category>jax</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
