<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Practical RL-Self driving car agent]]></title><description><![CDATA[Practical RL-Self driving car agent]]></description><link>https://practical-rl-self-driving-car-agent.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 03 Nov 2026 02:50:46 GMT</lastBuildDate><atom:link href="https://practical-rl-self-driving-car-agent.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Training a Self-Driving Car with PPO: What Worked, What Failed, and What I Learned]]></title><description><![CDATA[Stable-Baselines3 PPO trained on CarRacing-v3 for 1M timestamps, how the +1-per-new-tile reward in the env creates a grass donut.]]></description><link>https://practical-rl-self-driving-car-agent.hashnode.dev/training-a-self-driving-car-with-ppo-what-worked-what-failed-and-what-i-learned</link><guid isPermaLink="true">https://practical-rl-self-driving-car-agent.hashnode.dev/training-a-self-driving-car-with-ppo-what-worked-what-failed-and-what-i-learned</guid><category><![CDATA[self driving car agent]]></category><category><![CDATA[rl agent]]></category><category><![CDATA[car racing v3 environment]]></category><category><![CDATA[ #ReinforcementLearning ]]></category><category><![CDATA[Reinforcement Learning]]></category><category><![CDATA[self-driving cars]]></category><category><![CDATA[PPO reinforcement learning]]></category><dc:creator><![CDATA[lakshmi prasanna]]></dc:creator><pubDate>Sun, 04 Jan 2026 11:09:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767520281747/9dc21891-5ecf-4a41-8515-b678babe3aab.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introductionwhy-i-let-a-pixel-car-drive-itself-and-regretted-nothing"><strong><mark>Introduction</mark>—Why I Let a Pixel-Car Drive Itself (and Regretted Nothing)</strong></h2>
<p>I did not start this project to build a production-ready autopilot. I started it because every RL tutorial ends with “…and the agent solved the task!” while my own runs ended with a pixel-car doing donuts in a digital meadow. I wanted to know <em>why</em>.</p>
<p>CarRacing-v3 is a toy environment, but it captures every headache of real-world continuous control: high-dimensional images, noisy rewards, and a control surface smooth enough to let you believe you’re making progress—right up to the moment the agent locks itself into a 200-frame circle.<br />Proximal Policy Optimization (PPO) is the algorithm we are all told to reach for first. If PPO cannot learn to stay on a gray road that is literally painted for it, what hope do we have on a real highway?<br />Those are the stakes. The blog that follows is a postmortem, not a victory lap.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767510476350/a6b1e002-897b-4c08-b68e-4865ac1d3f78.png" alt="Flowchart illustrating a reinforcement learning pipeline for the CarRacing-v3 environment. It starts with raw image observations, followed by preprocessing to grayscale and resizing. Then, frames are stacked and input into a CNN encoder, which connects to a PPO policy network. The network produces actions (steer, throttle, brake) and receives reward signals, completing the loop." class="image--center mx-auto" /></p>
<p>flowchart illustrating a reinforcement learning pipeline for the CarRacing-v3 environment. It starts with raw image observations, followed by preprocessing to grayscale and resizing. Then, frames are stacked and input into a CNN encoder, which connects to a PPO policy network. The network produces actions (steer, throttle, brake) and receives reward signals, completing the loop.</p>
<h3 id="heading-the-reward-trap-why-the-agent-loops"><strong>The Reward Trap: Why the Agent Loops</strong></h3>
<p>The environment rewards the agent for visiting new green tiles (+1), but penalizes time (-0.1 per frame). This creates a perverse incentive: - A 12-tile loop gives +12 reward per lap. - After 40 frames, the time cost is -4. - Net gain: +8 per lap. - Once the loop is closed, no new tiles are found, so the reward turns negative. - But the policy stays in the loop because the gradient is flat—PPO refuses to explore actions that might break the loop.</p>
<p><strong>The agent is rewarded for visiting new tiles, not for driving forward—like being paid for walking into a forest, not for reaching the exit</strong></p>
<h2 id="heading-environment-amp-setup-a-9696-playground-that-hides-a-trap"><strong>Environment &amp; Setup – A 96×96 Playground That Hides a Trap</strong></h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Property</strong></td><td>Value</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Observation</strong></td><td>96×96×3 RGB, <strong>no velocity vector</strong></td></tr>
<tr>
<td><strong>Action</strong></td><td>Box(3) ∈ [-1,1]² × [0,1]</td></tr>
<tr>
<td><strong>Reward</strong></td><td>+1 per <strong>new</strong> green tile, –0.1 per frame, –100 per boundary hit</td></tr>
<tr>
<td><strong>Ep. length</strong></td><td>1 000 steps</td></tr>
<tr>
<td><strong>Training rig</strong></td><td>Stable-Baselines3 PPO, 3-layer CNN, 4-frame stack, Colab T4 GPU.</td></tr>
</tbody>
</table>
</div><p><strong>Key takeaway</strong>: the agent only sees pixels. No speedometer, no gyroscope.<br /><strong>Milestones</strong>:</p>
<ul>
<li><p>20 k steps → 48 tiles (random wiggles)</p>
</li>
<li><p>300 k steps → 210 tiles (partial track)</p>
</li>
<li><p><strong>1 M steps → 353 tiles, then a longer path still a donut</strong></p>
</li>
</ul>
<p>The road is empty, the tyres are virtual, and the embarrassment is real, I recorded the moment; watch the 1-M agent spinning in a loop</p>
<p>Click here on the link symbol:- <a target="_blank" href="https://drive.google.com/file/d/171u1f4nNjmBBkK18HBMNusIl01RVcDNN/view?usp=sharing">CLICK HERE🔗 1M Timestamps car racing ppo</a></p>
<h3 id="heading-the-ppo-pipeline-how-i-fed-pixels-to-a-policy-and-got-a-merry-go-round"><strong>The PPO Pipeline: How I Fed Pixels to a Policy and Got a Merry-Go-Round</strong></h3>
<p>Stable-Baselines3 gives you a one-liner <code>PPO("CnnPolicy", env, …)</code>, but the defaults are <strong>not</strong> your friend in visual control.<br />Here is the pipeline that reached 353 tiles before the donut phase.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767512861648/a0ea0de7-adc6-4464-b00a-a9637aac81a6.png" alt /></p>
<h3 id="heading-hyper-parameters-that-mattered"><strong>Hyper-parameters That Mattered</strong></h3>
<pre><code class="lang-python">n_steps          = <span class="hljs-number">2048</span>
batch_size       = <span class="hljs-number">256</span>
learning_rate    = <span class="hljs-number">3e-4</span>  (no schedule)
ent_coef         = <span class="hljs-number">0.0</span>   ← big change; entropy encouraged donuts
clip_range       = <span class="hljs-number">0.2</span>
gae_lambda       = <span class="hljs-number">0.95</span>
total_timesteps  = <span class="hljs-number">1</span> <span class="hljs-number">000</span> <span class="hljs-number">000</span>
</code></pre>
<p>Entropy regularization (default 0.01) literally <strong>paid the agent to spin</strong>—a wider action distribution samples tight circles more often. After I zeroed it, the loops became <strong>tighter but still present</strong>, proving the reward, not entropy, is the root.</p>
<h2 id="heading-reward-is-a-liar-how-1-per-tile-trained-a-professional-figure-skater"><strong>Reward is a Liar: How +1 Per Tile Trained a Professional Figure-Skater</strong></h2>
<p>The environment’s reward looks sensible on paper: “Visit new tiles, get +1.”<br />In practice it creates a <strong>greedy, myopic scorer</strong> that cares exactly zero about <em>where</em> the car will be in 50 steps—perfect fodder for PPO’s expected-return maximizer.</p>
<h3 id="heading-what-the-agent-learnt"><strong>What the Agent Learnt</strong></h3>
<ul>
<li><p>A 12-tile clockwise loop yields <strong>+12 every 40 frames</strong></p>
</li>
<li><p>Net return after step cost: +12 – 0.1 × 40 = <strong>+8 per lap</strong></p>
</li>
<li><p>Velocity is low ⇒ no wall penalty ⇒ <strong>risk-free income stream</strong></p>
</li>
</ul>
<p>Once the policy stumbles on this cycle, the PPO’s clipping objective keeps it there: the ratio<br />rₜ(θ) = π_θ(a|s) / π_θ_old(a|s)<br />hovers near 1 because no competing action looks <strong>significantly</strong> better inside the clip range. The optimizer quietly <strong>fine-tunes the spin</strong> instead of breaking it.</p>
<h3 id="heading-human-eye-vs-policy-eye"><strong>Human Eye vs Policy Eye</strong></h3>
<p>To us the tyres are drawing a perfect circle on grass; to the policy it is simply revisiting <strong>fresh green tiles</strong> at the edge of its 96×96 window.<br />Because the track is closed and symmetric, the convolutional features for “entering turn” and “exiting turn” collapse to the same embedding—<strong>the network literally can’t tell it’s looping</strong>.</p>
<h3 id="heading-failed-quick-fixes-ablation-summary"><strong>Failed Quick-Fixes (ablation summary)</strong></h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>action</strong></td><td>reward per frame</td><td><strong>net return per lap</strong></td></tr>
</thead>
<tbody>
<tr>
<td>new tile</td><td>+1</td><td>+1</td></tr>
<tr>
<td>revisiting tile</td><td>0</td><td>0</td></tr>
<tr>
<td>time cost</td><td>-0.1</td><td>-4</td></tr>
<tr>
<td>before loop [12 tiles]</td><td>-0.1</td><td>+8</td></tr>
<tr>
<td>while loop</td><td>0</td><td>-4</td></tr>
</tbody>
</table>
</div><ul>
<li><p><strong>Before loop</strong>: The agent <strong>gains</strong> +8 per lap.</p>
</li>
<li><p><strong>After loop</strong>: The agent <strong>loses</strong> -4 per lap.</p>
</li>
</ul>
<p>But the <strong>policy still stays</strong> in the self loop because:</p>
<ul>
<li><p>The <strong>gradient is flat</strong> — PPO’s clipped surrogate objective refuses to explore actions that might break the loop.</p>
</li>
<li><p>The <strong>local optimum</strong> is <strong>stable</strong> — the policy doesn’t update to escape.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The <strong>clipped surrogate objective</strong> is a key component of the Proximal Policy Optimization (PPO) algorithm. In simpler terms, it's a mechanism that <strong>limits how much a policy can change in each training update</strong> to prevent large, unstable updates that could degrade performance.</div>
</div>

<p><strong>Take-away</strong></p>
<blockquote>
<p>“More training” only <strong>amplifies</strong> whatever the reward signal praises.<br />If the praise is misaligned with human intent, PPO will deliver a <strong>higher-resolution failure</strong>.</p>
</blockquote>
<p>The video still shows 353 tiles, but every frame after step 450 is déjà-vu.</p>
<h1 id="heading-code-amp-reproducibility-snapshot"><strong>Code &amp; Reproducibility Snapshot</strong></h1>
<p>Want to re-create the 353-tile donut yourself?<br />Grab the exact notebook I ran on Google Colab (T4 GPU, ~35 min):</p>
<p>📦 <strong>Notebook</strong>: <a target="_blank" href="https://colab.research.google.com/drive/1BNtsI_Chl4_ezyfvmDsPnmERf7roMRZX?usp=sharing">carselfdriving.ipynb</a><br />(All cells are self-contained; just “Run All” after mounting Drive.)</p>
<p>Key library pins (as of 2026-01-03):</p>
<pre><code class="lang-plaintext">gymnasium==0.29.1  
stable-baselines3==2.3.0  
box2d-py==2.3.5  
opencv-python-headless==4.9.0
</code></pre>
<p>Difficulty check: <strong>Intermediate</strong></p>
<ul>
<li><p>Requires a free Colab account + 5 GB Drive space</p>
</li>
<li><p>No local GPU needed; do <strong>not</strong> try on CPU unless you like 6-hour spins.</p>
</li>
</ul>
<h2 id="heading-video-first-evaluation-why-your-eyeball-is-a-better-metric-than-episodereward"><strong>Video-First Evaluation: Why Your Eyeball is a Better Metric than</strong> <code>episode_reward</code></h2>
<p>Reward curves are seductive—they slope upward, you tweet the screenshot, and nobody notices the car is figure-skating.<br />I only realised the agent was looping after <strong>watching</strong> a 60-second clip. That embarrassment spawned a rule I now apply to every RL project:</p>
<blockquote>
<p><strong>Never trust a scalar you can’t see.</strong></p>
</blockquote>
<pre><code class="lang-sql">Reward
  ↑           *
  |         *   *
  |       *       *   ← global optimum (desired)
  |     *
  |   *        ← local optimum (looping)
  +<span class="hljs-comment">----------------------------&gt; Policy Space</span>
</code></pre>
<h3 id="heading-my-cheap-evaluation-stack"><strong>My Cheap Evaluation Stack</strong></h3>
<ol>
<li><p><code>VecVideoRecorder</code> wrapped around a 4-env vector</p>
</li>
<li><p>Record every 50 k steps (≈ 3 min of video)</p>
</li>
<li><p>Push mp4 to Drive, watch at 2× speed on phone</p>
</li>
<li><p>Tag behaviours: <code>straight</code>, <code>wiggle</code>, <code>spin</code>, <code>stall</code></p>
</li>
</ol>
<p>One evening I had four videos open in split-screen—20 k, 300 k, 500 k, 1 M.<br />The 1-M policy looked <em>smoother</em>, tyres never left tarmac, steering wheel barely twitched… and yet the car never advanced more than 12 tiles before resetting the same GPS coordinates. The <strong>reward graph lied</strong>, my eyes didn’t.</p>
<p><strong>Take-away for Practitioners</strong></p>
<ul>
<li><p>Always <strong>render first</strong>, log second.</p>
</li>
<li><p>Store short clips, not just <code>.csv</code>—disk is cheaper than retraining.</p>
</li>
<li><p>If behaviour plateaus while reward climbs, you are <strong>overfitting the reward</strong>, not the task.</p>
</li>
</ul>
<p>Seeing is believing; graphs are just gossip with error bars.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767515022150/66143797-6ab5-4b97-a9bd-07fbb2f61e73.png" alt class="image--center mx-auto" /></p>
<p>Look at the numbers <strong>while the tyres were still on tarmac</strong>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Step</strong></td><td><strong>Tile Count</strong></td><td><strong>Car Location</strong></td><td><strong>Instant Reward</strong></td></tr>
</thead>
<tbody>
<tr>
<td>~340 k</td><td><strong>349</strong></td><td>grey road</td><td><strong>positive</strong> (+1 per new tile)</td></tr>
</tbody>
</table>
</div><p>The system <strong>was</strong> working exactly as advertised:<br />“Agent on road → new tile → reward goes up.”<br />No bug, no sparse signal, no cliff.</p>
<p>environment’s <strong>default reward</strong> for <em>“any new pixel”</em> doesn’t actually means <em>“good driving.</em><br />On the lawn the car gone into a <strong>12-tile circle</strong>, entering un<em>touched</em> pixels every 38 frames.<br />Tile counter kept climbing → <strong>reward still positive</strong> → algorithm logs another “success.”<br />By tile <strong>349</strong> the loop was closed; after that <strong>zero fresh tiles</strong> appear, only the −0.1 time-tax remains, so the <strong>total return slides downhill</strong> (353 → 280 reward count).<br /><strong>The feedback flips from “good job” to “you’re wasting time,” yet the policy stays glued to the circle.</strong> Why?</p>
<p>Because in the <strong>policy-space landscape</strong> that little ring is a <strong>local optimum</strong>—a nice, safe hill with smooth gradients and no walls.<br />The <strong>global optimum</strong> (full lap, 600+ tiles, finish line) is a higher peak, but it lies across a valley of rare actions: sharp braking, counter-steer, maybe a backwards flick to re-align.<br />PPO’s clipped surrogate objective refuses to leap that valley; it would rather <strong>fine-tune the steering angle</strong> that keeps the cosy 12-tile merry-go-round alive.</p>
<p>So the agent <strong>ignores the road</strong> not out of stupidity, but because the <strong>reward contract never mentions “road” at all</strong>—only “new tile new point.”<br />Grass tiles count just the same, and once the policy discovers they can be <strong>farmed infinitely</strong>, asphalt becomes irrelevant.<br />The decreasing return after 353 is the environment’s way of whispering “this isn’t the route,” yet the car keeps spinning—<strong>locally optimal, globally blind</strong>.</p>
<h3 id="heading-why-car-prefers-a-280-tax-spin-to-a-600-tile-triumph"><strong>Why Car Prefers a 280-Tax Spin to a 600-Tile Triumph</strong></h3>
<p>Imagine you’re the policy network for a second.<br />Every 0.1 s you pick a steering angle; PPO hands you a <strong>report card</strong> that averages the next few seconds of reward.</p>
<p>After hundreds of thousands of practice laps you discover:</p>
<ul>
<li><p><strong>Action “0.42-left”</strong> → safe loop, <strong>−0.1 per step</strong>, variance = 0</p>
</li>
<li><p><strong>Action “0.15-right”</strong> → car drifts toward unknown, <strong>−0.3 ± 20</strong>, variance = huge</p>
</li>
</ul>
<p>The agent discovers a 12-tile loop that gives a <strong>high return</strong> (+12 per lap, minus 0.1×40 = -4, so +8 net). Once the loop is closed, it can no longer gain new tiles, so the <strong>reward per frame turns negative</strong>. But the policy still <strong>stays in the loop</strong> because the <strong>gradient is flat</strong> — PPO’s clipped surrogate objective refuses to explore actions that might break the loop, even though they would lead to higher long-term rewards.</p>
<p>Mathematically, the second option has <strong>higher expected loss</strong> and <strong>explosive variance</strong>, so the gradient shouts: <em>“Don’t go there!”</em><br />The clip-range safety net (±20 %) then <strong>flattens the gradient to zero</strong> for every nearby action. Result: your steering weights <strong>stop updating</strong> even though the numerical return is sliding downhill.<br />You’re trapped in a <strong>flat valley of local optimum</strong>—safe, predictable, boring, and impossible to escape.</p>
<hr />
<h3 id="heading-but-grass-gave-1-at-the-startwhy-visit-grass-at-all"><strong>“But grass gave +1 at the start—why visit grass at all?”</strong></h3>
<p>The <strong>first</strong> grass pixel is just <strong>random exploration</strong>—a micro-wiggle that happens to land on a green tile the network has never seen.<br />That single <strong>lucky +1</strong> nudges the probability of the wiggle <strong>upward</strong>.<br />Because the track edge is <strong>rotationally symmetric</strong>, the same micro-wiggle scores another +1 38 frames later.<br />After a few dozen reinforcements the probability mass <strong>collapses</strong> onto the repeating sequence.<br />From that moment the agent <strong>almost never samples</strong> a departure action again; grass started as an accident, became a habit, and turned into a life sentence.</p>
<h1 id="heading-conclusion-kill-the-donut-not-the-experiment"><strong>🔚 Conclusion – Kill the Donut, Not the Experiment</strong></h1>
<p>This project started as a weekend hack and ended as a 1 000 000-step seminar on <strong>misaligned incentives</strong>. PPO did exactly what we asked it to do: maximise the sum of future rewards. The environment handed out those rewards for <strong>grazing fresh asphalt</strong>, not for <strong>arriving anywhere</strong>. Is it any surprise the agent became a professional figure-skater?</p>
<p>The takeaway is bigger than a toy car. In recommendation systems, trading bots, or real autonomous vehicles, the same law applies:</p>
<blockquote>
<p>If the metric is sloppy, the optimiser will be <em>precisely</em> sloppy.</p>
</blockquote>
<p>So render your agents, question your rewards, and publish the failures alongside the fireworks. The next time your policy starts spinning, don’t curse the algorithm—rewrite the contract.</p>
<p>Clone the notebook, swap in the progress-based reward, and let me know when your pixel-car finally <strong>drives past the donut shop</strong> and keeps on going.</p>
<h2 id="heading-key-takeaways">Key takeaways</h2>
<ol>
<li><p><strong>Watch before you tweet.</strong><br /> A pretty reward graph is worth nothing if the behavior is stuck in a jig. Render 30 s of video every few checkpoints; it takes two minutes and saves days of denial.</p>
</li>
<li><p><strong>A reward is a contract, not a suggestion.</strong><br /> The agent will find the <em>cheapest</em> path to maximize the signal—even if that path looks absurd to you. Write the contract carefully or prepare to renegotiate.</p>
</li>
<li><p><strong>Local optima are <em>features</em>, not bugs.</strong><br /> PPO (and any policy-gradient method) is <em>designed</em> to climb whatever hill it lands on. If the tallest hill in your landscape is a donut, you’ll get a world-class donut.</p>
</li>
<li><p><strong>Continuous control needs <em>progress</em>, not just <em>collection</em>.</strong>*<br /> “+1 when you see something new” works for grid-worlds; on a looping track it’s an invitation to tread-water. Tie the reward to <em>forward displacement</em> or <em>time-to-goal</em> and the loop disappears.</p>
</li>
<li><p><strong>Fail in public, learn in public.</strong><br /> I gained more from this 280-tile regression than from any benchmark I ever “solved.” Document the collapse, share the video, and the next researcher starts one step closer to the fix.</p>
</li>
</ol>
<p>Keep these in mind and your next agent might still crash—but at least it will crash <em>somewhere new</em>.</p>
]]></content:encoded></item></channel></rss>