<?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: Harshil Rami</title>
    <description>The latest articles on DEV Community by Harshil Rami (@harshil_rami_8533a7388ef7).</description>
    <link>https://dev.to/harshil_rami_8533a7388ef7</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3892935%2Fa5861571-b59d-4821-9c9a-87be902476c2.png</url>
      <title>DEV Community: Harshil Rami</title>
      <link>https://dev.to/harshil_rami_8533a7388ef7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harshil_rami_8533a7388ef7"/>
    <language>en</language>
    <item>
      <title>The Paper That Taught Neural Networks to Learn Backwards</title>
      <dc:creator>Harshil Rami</dc:creator>
      <pubDate>Sat, 09 May 2026 10:36:44 +0000</pubDate>
      <link>https://dev.to/harshil_rami_8533a7388ef7/the-paper-that-taught-neural-networks-to-learn-backwards-4kmn</link>
      <guid>https://dev.to/harshil_rami_8533a7388ef7/the-paper-that-taught-neural-networks-to-learn-backwards-4kmn</guid>
      <description>&lt;p&gt;Last week I read the 1958 Rosenblatt paper. The one that started everything. The Perceptron, the first learning machine, the idea that memory lives in connections and not addresses. And at the very end of that paper, almost as a footnote, Rosenblatt wrote that "some system, more advanced in principle than the perceptron, seems to be required."&lt;/p&gt;

&lt;p&gt;This is that system.&lt;/p&gt;

&lt;p&gt;Rumelhart, Hinton, and Williams. 1986. Four pages in Nature. And somewhere in those four pages, the answer to the question Rosenblatt had left open for twenty-eight years.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Actually Broken
&lt;/h2&gt;

&lt;p&gt;To understand why this paper matters, you need to understand what the Perceptron could not do. And the answer is not XOR, even though that is what everyone says. XOR is a symptom. The real problem was deeper.&lt;/p&gt;

&lt;p&gt;In Rosenblatt's Perceptron, the feature detectors, the A-units sitting between the input and the output, were connected randomly and then frozen. Nobody trained them. The only thing that learned was the final layer, the response units deciding which class to pick. Which means the Perceptron could only learn to combine features that already existed in the raw input. It could not discover new features on its own.&lt;/p&gt;

&lt;p&gt;Think about what this means. If you want the Perceptron to recognise faces, someone has to hand-engineer the features: edges, curves, symmetry. The network cannot figure out that symmetry is a useful thing to look for. It can only learn to weight the features you already gave it.&lt;/p&gt;

&lt;p&gt;Rumelhart, Hinton, and Williams called these "hidden units." Units that sit between the input and the output, that are not told what to do, that have to figure out on their own what they should represent. Training hidden units is the problem. And the reason nobody had solved it is that you cannot directly measure how wrong a hidden unit is. You only know how wrong the output is. The error signal exists at the top. The hidden units are somewhere in the middle.&lt;/p&gt;

&lt;p&gt;Backpropagation is the answer to one question: how do you take an error signal that exists only at the output, and use it to train units that you cannot directly observe?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: A Factory With Floors
&lt;/h2&gt;

&lt;p&gt;Before the math, the picture.&lt;/p&gt;

&lt;p&gt;Imagine a factory with multiple floors. Raw materials come in at the ground floor. Each floor transforms what it receives and passes the result upward. The finished product comes out at the top floor. You have a quality inspector at the top who compares the finished product to what was ordered and measures how wrong it is.&lt;/p&gt;

&lt;p&gt;Now the problem: the quality inspector knows the final product is wrong. But which floor made the mistake? And by exactly how much did each floor contribute to the wrongness?&lt;/p&gt;

&lt;p&gt;This is the problem backpropagation solves. And it solves it in two passes.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;forward pass&lt;/strong&gt; is the factory running normally. Input comes in at the bottom, each layer transforms it, output comes out at the top. Simple.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;backward pass&lt;/strong&gt; is blame flowing in reverse. The top floor gets blamed proportionally to how wrong the output was. It then passes blame to the floor below it, saying: here is how much each of you contributed to my mistake. Each floor does the same, passing blame further down, all the way to the bottom. Every connection in the network receives a precise measure of how much it contributed to the final error. Then every weight adjusts itself to be slightly less wrong next time.&lt;/p&gt;

&lt;p&gt;That is backpropagation. The forward pass computes what the network thinks. The backward pass computes who is responsible for being wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Forward Pass: Equations 1 and 2
&lt;/h2&gt;

&lt;p&gt;Let us get precise. In the paper, Rumelhart et al. define the network with two equations that govern how information moves forward through the network.&lt;/p&gt;

&lt;p&gt;The total input to unit j is a weighted sum of everything coming from below:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;x(j) = Σ y(i) · w(ij)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where y(i) is the output of unit i in the layer below, and w(ij) is the weight of the connection from i to j. Every unit below j sends its output upward, multiplied by the strength of its connection. Unit j adds them all up.&lt;/p&gt;

&lt;p&gt;Then the output of unit j is a non-linear function of that total input:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;y(j) = 1 / (1 + e^(-x(j)))&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the sigmoid function. It takes any real number and squashes it into a value between 0 and 1. The reason you need this non-linearity is critical: without it, stacking multiple layers is mathematically equivalent to having just one layer. The non-linearity is what allows each layer to do something genuinely different from the layer below it.&lt;/p&gt;

&lt;p&gt;The paper says any bounded differentiable function will work here. In 1986, they used sigmoid. Today we use ReLU, which is simply max(0, x). Simpler to compute, faster to train, does not suffer from the vanishing gradient problem that sigmoid creates in deep networks. But the principle is identical: a non-linearity that lets each layer transform its input in a way that the next layer cannot simply undo.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Error: Equation 3
&lt;/h2&gt;

&lt;p&gt;After the forward pass, you have the network's output. You compare it to what the output should have been. The error is defined as:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E = (1/2) Σ (y(j) - d(j))²&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where y(j) is what the network produced for output unit j, and d(j) is what it should have produced. The (1/2) out front is a convenience: when you differentiate this, the 2 from the square and the 1/2 cancel cleanly.&lt;/p&gt;

&lt;p&gt;This is mean squared error. Today we often use cross-entropy loss for classification problems because it has better gradient properties near zero and one. But the backpropagation algorithm works identically regardless of what loss function you choose. The only requirement is that the loss is differentiable with respect to the output.&lt;/p&gt;

&lt;p&gt;The goal is to minimise E. And the way to minimise E is gradient descent: find which direction in weight space makes E increase, and move in the opposite direction. To do this, you need the partial derivative of E with respect to every single weight in the network. This is what the backward pass computes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Backward Pass: Equations 4 Through 7
&lt;/h2&gt;

&lt;p&gt;This is the heart of the paper. And it is where most explanations lose people, because they introduce notation and concepts at the same time, leaving you holding two unfamiliar things at once. Let me do this differently.&lt;/p&gt;

&lt;p&gt;I am going to carry one concrete example through every step. The network predicted 0.9. The correct answer was 0. The network is very wrong. We need to figure out which weights caused this, and by exactly how much. That is the only question the backward pass is answering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before anything else: what is the chain rule doing here?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The chain rule from calculus says: if A affects B, and B affects C, then you can figure out how A affects C by multiplying how A affects B by how B affects C.&lt;/p&gt;

&lt;p&gt;In our network, a weight affects a unit's total input. That total input affects the unit's output through the sigmoid. That output flows upward and eventually affects the final error. The chain rule lets us connect the first link (weight) to the last link (error) by multiplying all the steps in between. This is all we are doing, four times in a row, layer by layer, going backwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: How wrong is the output, and in which direction?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start at the top. The output unit produced 0.9. The correct answer was 0. The first thing we need is a precise measure of how the error changes when the output changes.&lt;/p&gt;

&lt;p&gt;The answer is simply: prediction minus target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;∂E / ∂y = y - d&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In our example: 0.9 minus 0 equals 0.9. Large and positive. This tells us: if the output goes up even slightly, the error gets worse. The network needs to push this output down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: How much did the total input to that unit contribute to the error?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The output of a unit is the sigmoid of its total input. So before we can ask "which weights caused this," we need to go one step back: how sensitive is the error to the total input arriving at the output unit?&lt;/p&gt;

&lt;p&gt;This is where the chain rule enters. The error depends on the output, and the output depends on the total input. So:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;∂E / ∂x = (∂E / ∂y) · (∂y / ∂x)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The second term, how does the sigmoid output change when its input changes, has a beautiful closed form. The derivative of the sigmoid is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;∂y / ∂x = y · (1 - y)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In our example the output was 0.9, so this is 0.9 times 0.1 which equals 0.09. Putting it together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;∂E / ∂x = 0.9 × 0.09 = 0.081&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This combined number is what Rumelhart et al. call the error signal for this unit. It captures both how wrong the output was and how sharply the sigmoid was responding at that point.&lt;/p&gt;

&lt;p&gt;One thing worth pausing on. Notice what happens when the sigmoid output is very close to 0 or very close to 1. The term y · (1 - y) becomes very small. A 0.99 output gives 0.99 times 0.01 which is 0.0099. This means the error signal almost vanishes when units are saturated near the extremes of the sigmoid. Blame barely reaches the weights below. This is the vanishing gradient problem, and it is why deep networks trained with sigmoid struggled for decades until ReLU replaced it. ReLU does not saturate: its derivative is simply 1 for any positive input. The blame flows through cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Which weights caused the error, and by how much?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now we have the error signal at the output unit. We need to turn this into a gradient for each weight connecting into that unit.&lt;/p&gt;

&lt;p&gt;The total input to a unit is just a weighted sum of the outputs from the layer below. So if we change one weight by a tiny amount, the total input changes by exactly the output of the unit that weight came from. Nothing more. This means:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;∂E / ∂w = (∂E / ∂x) · y&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In concrete terms: if the error signal at the output unit is 0.081, and the hidden unit connected to it had an output of 0.6, then the gradient for that weight is 0.081 times 0.6 which equals 0.049. This weight needs to decrease by an amount proportional to 0.049 to reduce the error.&lt;/p&gt;

&lt;p&gt;The elegance here is striking. The gradient for any weight is just two numbers multiplied together: what the layer above says about the error, and what the layer below actually produced. That is it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Passing blame to the hidden layer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You now have gradients for all the weights in the final layer. But you need to do the same thing for every hidden layer below. To do that, you need to know the error signal for each hidden unit, the same way you computed it for the output unit in step 1.&lt;/p&gt;

&lt;p&gt;A hidden unit connects upward to multiple output units. Each of those connections contributed to the final error. So the total blame assigned to a hidden unit is the sum of blame from every unit it connects to above it, weighted by the strength of each connection:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;∂E / ∂y(hidden) = Σ (∂E / ∂x(above)) · w&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a hidden unit connects to three output units with weights 0.5, 0.3, and 0.8, and those output units have error signals 0.081, 0.04, and 0.02, then the blame reaching the hidden unit is:&lt;/p&gt;

&lt;p&gt;(0.081 × 0.5) + (0.04 × 0.3) + (0.02 × 0.8) = 0.0405 + 0.012 + 0.016 = 0.069&lt;/p&gt;

&lt;p&gt;And this is the key to the whole algorithm. Once you have this blame signal at the hidden unit, you repeat steps 2 and 3 exactly as before: multiply by the sigmoid derivative to get the error signal, then multiply by the outputs from the layer below to get weight gradients.&lt;/p&gt;

&lt;p&gt;The algorithm cascades backwards through the entire network, one layer at a time. Every weight in the network receives a precise gradient telling it exactly how much it contributed to the final error. This is why it is called backpropagation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Weight Update: Equations 8 and 9
&lt;/h2&gt;

&lt;p&gt;Once you have the gradients, you update the weights. The simplest version is vanilla gradient descent:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Δw = -ε · (∂E / ∂w)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where ε is the learning rate, a small number like 0.01 that controls how large each step is. The negative sign is because you want to move in the direction that reduces E, which is the opposite of the gradient direction.&lt;/p&gt;

&lt;p&gt;But Rumelhart et al. immediately pointed out the problem with vanilla gradient descent: it is slow, and it oscillates. If the gradient keeps pointing in the same direction, you want to pick up speed. If it keeps reversing, you want to slow down.&lt;/p&gt;

&lt;p&gt;Their solution adds momentum:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Δw(t) = -ε · (∂E / ∂w(t)) + α · Δw(t-1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The second term carries forward a fraction α of the previous weight update. If the gradient has been pointing in the same direction for several steps, the weight updates accumulate and the learning accelerates. If the gradient reverses, the momentum term and the gradient term partially cancel, dampening the oscillation.&lt;/p&gt;

&lt;p&gt;This is the ancestor of every modern optimizer. Adam, RMSprop, AdaGrad, they are all elaborate answers to the same question Rumelhart and Hinton were asking in 1986: how do you make gradient descent faster and more stable? Momentum was the first answer. It is still inside every optimizer you use today.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Experiments Actually Proved
&lt;/h2&gt;

&lt;p&gt;This is the part I want to spend time on, because most blogs about backprop skip it entirely and just talk about the algorithm. The experiments in this paper are where the real idea lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment 1: Symmetry detection.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The task: given a binary input vector, is it symmetrical about its midpoint? This cannot be solved without hidden units, and the paper proves it. You cannot add up individual inputs and get symmetry. You need to compare positions across the midpoint, which requires an intermediate representation.&lt;/p&gt;

&lt;p&gt;Rumelhart et al. trained a network with two hidden units on this task. After training, they inspected what the hidden units had learned. The weights were symmetric about the midpoint with opposite signs. For a symmetric input, both hidden units received zero net input and stayed off, causing the output unit (which had a positive bias) to fire, signalling symmetry. For any non-symmetric input, at least one hidden unit fired and suppressed the output.&lt;/p&gt;

&lt;p&gt;The network was never told "look for symmetry across the midpoint." It discovered that structure because discovering it was the most efficient way to reduce the error. That is representation learning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment 2: The family tree.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one is remarkable. Two isomorphic family trees, one English and one Italian, 104 relationships expressed as triples: (person, relationship, person). The network was trained on 100 of the 104 triples and asked to complete the fourth term when given the first two.&lt;/p&gt;

&lt;p&gt;The network had to compress information about 24 people and 12 relationships into 6 hidden units per group. After training, Rumelhart et al. examined what those 6 units had learned to encode. Unit 1 distinguished English from Italian people. Unit 2 encoded which generation a person belonged to. Unit 6 encoded which branch of the family they came from.&lt;/p&gt;

&lt;p&gt;Nobody told the network that nationality, generation, and family branch were relevant features. The network discovered them because they were the most compact and useful way to represent the information it needed to produce correct outputs. And because it had learned these structural features, it generalised correctly to the 4 triples it had never seen during training.&lt;/p&gt;

&lt;p&gt;This is the proof of concept for representation learning. It is the same thing that happens when a modern neural network learns that edges are useful in layer 1, textures in layer 2, and object parts in layer 3. Nobody tells it to look for those things. It discovers them because they are the most efficient path to reducing error.&lt;/p&gt;




&lt;h2&gt;
  
  
  What They Admitted the Paper Cannot Do
&lt;/h2&gt;

&lt;p&gt;Here is the thing about this paper that I respect enormously. The last two paragraphs contain a list of everything they knew was wrong.&lt;/p&gt;

&lt;p&gt;The error surface may contain local minima. Gradient descent is not guaranteed to find the global minimum. They say that in practice the network rarely gets badly stuck, and that adding more connections than strictly necessary tends to smooth out the landscape. This is still true. Overparameterised networks generalise better than theory predicts. Nobody fully understands why.&lt;/p&gt;

&lt;p&gt;The learning procedure is not biologically plausible. Backpropagation requires exact weight symmetry between the forward and backward passes, precise storage of intermediate activations, and a global error signal propagated backwards. Brains do none of these things in any obvious way. Rumelhart and Hinton knew this in 1986 and said so plainly. They hoped studying backpropagation would eventually lead to something more biologically realistic. Forty years later, that search is still ongoing.&lt;/p&gt;

&lt;p&gt;And the scaling problem. The paper does not dwell on it, but the footnote is there. The procedure works on the tasks they tried. It is not obvious how it scales. The answer turned out to be: it scales remarkably well with more data and more compute, in ways nobody in 1986 could have anticipated. But the doubt was honest and appropriate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Modern Version: What loss.backward() Is Actually Doing
&lt;/h2&gt;

&lt;p&gt;When you write this in PyTorch:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;# A simple two-layer network, the same structure Rumelhart et al. used
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hidden_size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# weights w_ij, equation 1
&lt;/span&gt;    &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sigmoid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;                          &lt;span class="c1"&gt;# equation 2 - they used sigmoid
&lt;/span&gt;    &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hidden_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sigmoid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Equation 3: mean squared error
&lt;/span&gt;&lt;span class="n"&gt;criterion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MSELoss&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Equation 9: SGD with momentum, directly from the paper
&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SGD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;momentum&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Training loop
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataloader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Forward pass: equations 1 and 2 running layer by layer
&lt;/span&gt;    &lt;span class="n"&gt;outputs&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="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Equation 3: compute error
&lt;/span&gt;    &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Backward pass: equations 4 through 7, computed automatically
&lt;/span&gt;    &lt;span class="c1"&gt;# This is the entire backward pass of the paper, done in one line
&lt;/span&gt;    &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Equation 9: update weights using accumulated gradients
&lt;/span&gt;    &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every single line maps directly to something in the 1986 paper. &lt;code&gt;nn.Linear&lt;/code&gt; is equation 1. &lt;code&gt;nn.Sigmoid&lt;/code&gt; is equation 2. &lt;code&gt;nn.MSELoss&lt;/code&gt; is equation 3. &lt;code&gt;loss.backward()&lt;/code&gt; is equations 4 through 7, the entire backward pass, computed automatically using autograd. &lt;code&gt;optimizer.step()&lt;/code&gt; with momentum is equation 9.&lt;/p&gt;

&lt;p&gt;The difference is that today we would swap sigmoid for ReLU (faster, avoids vanishing gradients), MSELoss for CrossEntropyLoss for classification tasks (better gradient signal), and SGD with momentum for Adam (adaptive learning rates per parameter). But the underlying algorithm, forward pass, compute error, backward pass, update weights, is identical to what Rumelhart, Hinton, and Williams described in four pages in 1986.&lt;/p&gt;

&lt;p&gt;One more thing worth noting. The paper says they accumulated gradients over all training cases before updating weights. Today we call that batch gradient descent. The alternative they mention, updating after every case, is stochastic gradient descent. Modern training uses mini-batch gradient descent, a middle ground they could not fully explore in 1986 because they did not have the compute. The insight was already there. The scale was not.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Paper Actually Did
&lt;/h2&gt;

&lt;p&gt;The Perceptron told us that connections can learn. But it could only learn to weight features you already knew about.&lt;/p&gt;

&lt;p&gt;Backpropagation told us that hidden units can learn too. And when hidden units learn, they discover features nobody designed. Nationality. Generation. Symmetry. Edges. Textures. Syntax. Meaning. Every layer of every deep network you have ever used is discovering features that its designers did not explicitly specify, using a procedure that is recognisably the same four equations from this paper.&lt;/p&gt;

&lt;p&gt;The Perceptron answered Rosenblatt's question about how memory influences behavior. Backpropagation answered the harder question: how does a system figure out what to remember in the first place.&lt;/p&gt;

&lt;p&gt;Everything since, every architecture, every optimizer, every training trick, is an elaboration on the answer Rumelhart, Hinton, and Williams put into four pages in Nature in October 1986.&lt;/p&gt;

&lt;p&gt;They did not know it would scale to a trillion parameters. They did not know it would learn to write and reason and generate images. They just knew it could learn that Colin's aunt is Sophia, without being told to look for aunts.&lt;/p&gt;

&lt;p&gt;That was enough to change everything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part of my series reading foundational AI papers from scratch. Next up: Gradient-Based Learning Applied to Document Recognition, LeCun et al., 1998. The paper that took what Rumelhart and Hinton built and asked: what if the architecture itself could encode structure?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>backpropogation</category>
    </item>
    <item>
      <title>Blog 3: Adaptive Learning Rate Methods (Part 1)</title>
      <dc:creator>Harshil Rami</dc:creator>
      <pubDate>Thu, 23 Apr 2026 18:21:51 +0000</pubDate>
      <link>https://dev.to/harshil_rami_8533a7388ef7/blog-3-adaptive-learning-rate-methods-part-1-2jc2</link>
      <guid>https://dev.to/harshil_rami_8533a7388ef7/blog-3-adaptive-learning-rate-methods-part-1-2jc2</guid>
      <description>&lt;h3&gt;
  
  
  &lt;em&gt;When one learning rate isn't enough — per-parameter scaling and the decay problem&lt;/em&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Momentum gave the optimizer a memory across time.&lt;br&gt;
Adaptive methods give it a memory across parameters.&lt;br&gt;
Both are necessary. Neither is sufficient alone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem with a single learning rate
&lt;/h2&gt;

&lt;p&gt;Every optimizer we've covered so far shares one architectural assumption: a single scalar &lt;em&gt;η&lt;/em&gt; governs every parameter in the model.&lt;/p&gt;

&lt;p&gt;This seems reasonable until you think about what parameters actually experience during training.&lt;/p&gt;

&lt;p&gt;Consider a language model's embedding table. It contains one vector per token in the vocabulary — perhaps 50,000 vectors, each of dimension 512. In any given mini-batch of 64 sequences, you might see 2,000 unique tokens. The remaining 48,000 tokens receive &lt;strong&gt;zero gradient&lt;/strong&gt; for that entire step. When they do appear, their gradient signals are sparse, noisy, and infrequent.&lt;/p&gt;

&lt;p&gt;Now consider the final projection layer — a dense 512×50,000 matrix. Every forward pass touches every row. Gradients are dense, consistent, and arrive every single step.&lt;/p&gt;

&lt;p&gt;Both layers are updated with the same &lt;em&gt;η&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is the problem. Parameters that receive rare, informative signal should move aggressively when that signal arrives — their updates are precious. Parameters that receive dense, consistent signal should move conservatively — there's no rush, and overshooting is costly.&lt;/p&gt;

&lt;p&gt;A global learning rate can't serve both regimes. Set it high and the dense layers oscillate. Set it low and the sparse layers barely move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AdaGrad's answer&lt;/strong&gt;: let each parameter maintain its own effective learning rate, derived automatically from its gradient history.&lt;/p&gt;

&lt;h2&gt;
  
  
  AdaGrad: accumulate, then scale
&lt;/h2&gt;

&lt;p&gt;AdaGrad (Adaptive Gradient Algorithm — Duchi, Hazan &amp;amp; Singer, 2011) introduces a per-parameter accumulator &lt;em&gt;Gₜ&lt;/em&gt; that tracks the sum of squared gradients seen so far.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gₜ = Gₜ₋₁ + (∇L(θₜ))²        # element-wise square, accumulated
θₜ₊₁ = θₜ − (η / √(Gₜ + ε)) · ∇L(θₜ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;em&gt;ε&lt;/em&gt; (epsilon) is a small constant (typically 1e-8) added for numerical stability — it prevents division by zero when a parameter has received no gradient.&lt;/p&gt;

&lt;p&gt;The update is &lt;strong&gt;element-wise&lt;/strong&gt;: each parameter &lt;em&gt;θᵢ&lt;/em&gt; has its own &lt;em&gt;Gᵢ&lt;/em&gt;, and divides its gradient by &lt;em&gt;√Gᵢ&lt;/em&gt;. No parameter borrows from another's accumulator.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this achieves
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sparse parameters&lt;/strong&gt; — infrequent updates, small accumulated &lt;em&gt;G&lt;/em&gt;. Dividing by &lt;em&gt;√G&lt;/em&gt; yields a large effective learning rate. When signal finally arrives, AdaGrad takes a proportionally large step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dense parameters&lt;/strong&gt; — frequent updates, large accumulated &lt;em&gt;G&lt;/em&gt;. Dividing by &lt;em&gt;√G&lt;/em&gt; yields a small effective learning rate. Updates are conservative; the optimizer doesn't overfit to any single gradient.&lt;/p&gt;

&lt;p&gt;AdaGrad is essentially performing an automatic, online normalization of learning rates. You no longer need to hand-tune separate learning rates for different parameter groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where AdaGrad shines
&lt;/h3&gt;

&lt;p&gt;This mechanism is particularly powerful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sparse features&lt;/strong&gt; in NLP (word embeddings, bag-of-words models)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendation systems&lt;/strong&gt; with millions of item/user embeddings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convex optimization&lt;/strong&gt; problems where the accumulated curvature information is always relevant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In fact, for convex problems, AdaGrad has provably optimal regret bounds in the online learning setting. This theoretical grounding is part of why it was so influential.&lt;/p&gt;

&lt;h3&gt;
  
  
  The learning rate as a function of time
&lt;/h3&gt;

&lt;p&gt;It helps to think of AdaGrad as replacing the fixed learning rate &lt;em&gt;η&lt;/em&gt; with an effective per-parameter rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ηᵢ_eff(t) = η / √(Σₛ₌₁ᵗ gᵢ,ₛ²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a &lt;strong&gt;monotonically decreasing&lt;/strong&gt; function of time. Every gradient update, regardless of size, increases &lt;em&gt;Gᵢ&lt;/em&gt;, which decreases &lt;em&gt;ηᵢ_eff&lt;/em&gt;. The learning rate can only go down. It never recovers.&lt;/p&gt;

&lt;p&gt;That property is exactly AdaGrad's fatal flaw.&lt;/p&gt;

&lt;h2&gt;
  
  
  AdaGrad's fatal flaw: the dying learning rate
&lt;/h2&gt;

&lt;p&gt;In practice, AdaGrad's accumulator grows without bound. After enough training steps, &lt;em&gt;Gₜ&lt;/em&gt; becomes so large that &lt;em&gt;η/√Gₜ&lt;/em&gt; shrinks toward zero for every parameter — including the ones that still need to learn.&lt;/p&gt;

&lt;p&gt;This is not a tuning problem. It is structural. The accumulator is a sum, not an average, and sums only increase.&lt;/p&gt;

&lt;p&gt;The consequences are severe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Training effectively &lt;strong&gt;stops&lt;/strong&gt; after a certain number of steps, even if the model hasn't converged.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;non-stationary&lt;/strong&gt; loss surfaces (which all deep learning surfaces are — the loss landscape shifts as other parameters update), old gradient information from early training becomes misleading. Parameters that moved quickly early on get permanently penalized for it, even if the relevant gradients now point in a completely different direction.&lt;/li&gt;
&lt;li&gt;The "optimal for convex problems" guarantee doesn't transfer to deep learning, where the curvature landscape changes throughout training.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AdaGrad is excellent for shallow, convex problems with sparse features. For deep networks trained over many epochs, it usually fails in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is exactly the problem RMSProp was designed to fix.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  RMSProp: forget the distant past
&lt;/h2&gt;

&lt;p&gt;RMSProp (Root Mean Square Propagation — Hinton, 2012, unpublished but widely cited from his Coursera lectures) makes one targeted change to AdaGrad: &lt;strong&gt;replace the cumulative sum with an exponentially weighted moving average.&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;Eₜ = ρ · Eₜ₋₁ + (1 − ρ) · (∇L(θₜ))²     # running average of squared gradients
θₜ₊₁ = θₜ − (η / √(Eₜ + ε)) · ∇L(θₜ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;em&gt;ρ&lt;/em&gt; (rho) is the decay coefficient — typically 0.9 or 0.99. It controls how much weight is given to recent gradients vs. historical ones.&lt;/p&gt;

&lt;p&gt;This is the exponential moving average (EMA) pattern — the same mechanism used in momentum. The difference here is that it's applied to &lt;strong&gt;squared gradients&lt;/strong&gt; rather than the gradients themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this works
&lt;/h3&gt;

&lt;p&gt;With exponential decay, the effective window of "remembered" gradient history is roughly &lt;em&gt;1/(1−ρ)&lt;/em&gt; steps. At &lt;em&gt;ρ = 0.9&lt;/em&gt;, that's ~10 recent steps. At &lt;em&gt;ρ = 0.99&lt;/em&gt;, ~100 steps.&lt;/p&gt;

&lt;p&gt;Old gradients from many hundreds of steps ago contribute essentially nothing to &lt;em&gt;Eₜ&lt;/em&gt;. The accumulator doesn't grow forever — it's a sliding window. When the loss landscape shifts (as it always does in deep learning), the old curvature information fades out, and new curvature information takes over.&lt;/p&gt;

&lt;p&gt;This is the key property AdaGrad lacked: &lt;strong&gt;adaptability over time, not just over parameters.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The geometry being captured
&lt;/h3&gt;

&lt;p&gt;The denominator &lt;em&gt;√Eₜ&lt;/em&gt; is an estimate of the &lt;strong&gt;root mean square&lt;/strong&gt; of recent gradients for each parameter — hence the name. It approximates the local gradient scale without integrating all history.&lt;/p&gt;

&lt;p&gt;Parameters that have recently received large gradients get scaled down. Parameters that have recently been quiet get scaled up. The key word is &lt;em&gt;recently&lt;/em&gt; — unlike AdaGrad, this estimate is always locally relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  AdaGrad vs. RMSProp: direct comparison
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AdaGrad:  Gₜ = Gₜ₋₁ + g²          → cumulative sum, unbounded growth
RMSProp:  Eₜ = ρ·Eₜ₋₁ + (1−ρ)·g²  → exponential decay, bounded estimate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structural difference is one line. The practical difference is enormous.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;AdaGrad&lt;/th&gt;
&lt;th&gt;RMSProp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accumulator&lt;/td&gt;
&lt;td&gt;Cumulative sum&lt;/td&gt;
&lt;td&gt;Exponential moving average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning rate over time&lt;/td&gt;
&lt;td&gt;Monotonically decreasing&lt;/td&gt;
&lt;td&gt;Stationary (fluctuates around a value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory of past gradients&lt;/td&gt;
&lt;td&gt;All of history, equally weighted&lt;/td&gt;
&lt;td&gt;Recent history, exponentially weighted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suitable for&lt;/td&gt;
&lt;td&gt;Convex, sparse, shallow models&lt;/td&gt;
&lt;td&gt;Non-convex, deep networks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long training runs&lt;/td&gt;
&lt;td&gt;Fails (LR collapses)&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-stationary landscapes&lt;/td&gt;
&lt;td&gt;Fails (stale curvature)&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key hyperparameter&lt;/td&gt;
&lt;td&gt;&lt;em&gt;η&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;η&lt;/em&gt;, &lt;em&gt;ρ&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RMSProp became the default adaptive optimizer for deep learning before Adam existed, and many practitioners still reach for it when they want something lighter than Adam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AdaGrad
&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataloader&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;compute_gradient&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;model&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;G&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;grad&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;theta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;grad&lt;/span&gt;

&lt;span class="c1"&gt;# RMSProp
&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataloader&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;compute_gradient&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;model&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;E&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rho&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rho&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;grad&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;theta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;grad&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In PyTorch:&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;# AdaGrad
&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Adagrad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# RMSProp
&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RMSprop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Practical hyperparameter guidance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AdaGrad: &lt;em&gt;η = 0.01&lt;/em&gt; is a reasonable start. There's little else to tune — the accumulator handles the rest.&lt;/li&gt;
&lt;li&gt;RMSProp: &lt;em&gt;ρ = 0.9&lt;/em&gt; is standard. Use &lt;em&gt;ρ = 0.99&lt;/em&gt; for more stable but slower-adapting effective LR. &lt;em&gt;η&lt;/em&gt; typically needs to be smaller than you'd use with SGD — start at &lt;em&gt;1e-3&lt;/em&gt; or &lt;em&gt;1e-4&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Both methods are sensitive to &lt;em&gt;ε&lt;/em&gt;. In low-precision training (float16, bfloat16), the default &lt;em&gt;1e-8&lt;/em&gt; can cause numerical issues. Try &lt;em&gt;1e-6&lt;/em&gt; or &lt;em&gt;1e-5&lt;/em&gt; if you see NaN losses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's still missing
&lt;/h2&gt;

&lt;p&gt;RMSProp solves the dying learning rate. It gives us per-parameter adaptivity that stays relevant throughout training. It's a genuinely good optimizer.&lt;/p&gt;

&lt;p&gt;But look at the update rule again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;θₜ₊₁ = θₜ − (η / √(Eₜ + ε)) · ∇L(θₜ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no velocity term. No momentum. The update is still reactive — it responds to the &lt;em&gt;current&lt;/em&gt; gradient, scaled by recent gradient history, but it doesn't accumulate direction the way momentum does.&lt;/p&gt;

&lt;p&gt;We now have two powerful, independent ideas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Momentum&lt;/strong&gt; — smooth out gradient noise by accumulating velocity over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive scaling&lt;/strong&gt; — normalize updates by per-parameter gradient magnitude&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither knows about the other. Both help convergence. The obvious question is: what happens if you combine them?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's Blog 4.&lt;/strong&gt; Adam takes exactly this step — it maintains both a first-moment estimate (momentum over gradients) and a second-moment estimate (RMSProp-style scaling), applies bias corrections to both, and produces one of the most robust general-purpose optimizers ever designed. Adamax, Nadam, and AMSGrad follow as targeted improvements on specific failure modes Adam introduces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things to hold onto
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;AdaGrad&lt;/th&gt;
&lt;th&gt;RMSProp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core idea&lt;/td&gt;
&lt;td&gt;Accumulate squared gradients&lt;/td&gt;
&lt;td&gt;EMA of squared gradients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it fixes&lt;/td&gt;
&lt;td&gt;Uniform LR across parameters&lt;/td&gt;
&lt;td&gt;Uniform LR across parameters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it breaks&lt;/td&gt;
&lt;td&gt;Long-run training (LR → 0)&lt;/td&gt;
&lt;td&gt;Nothing fatal — but no momentum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best use case&lt;/td&gt;
&lt;td&gt;Convex, sparse, shallow&lt;/td&gt;
&lt;td&gt;Deep networks, non-stationary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Blog 4&lt;/strong&gt;, the two threads of this series converge. Adam combines the first moment (gradient direction, momentum-style) with the second moment (gradient magnitude, RMSProp-style) into a single update, then applies bias corrections to prevent cold-start distortion. Adamax extends the second moment to the L∞ norm. Nadam swaps standard momentum for Nesterov lookahead. AMSGrad addresses Adam's theoretical non-convergence issue.&lt;/p&gt;

&lt;p&gt;Each one is a targeted answer to a specific flaw in Adam's design. By the end of Blog 4, you'll have the full picture of why AdamW — not Adam — is the default for modern LLM training.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is Blog 3 of an 8-part series on optimization algorithms for deep learning.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>gradientdescent</category>
    </item>
    <item>
      <title>Blog 2: Momentum-Based Optimizers</title>
      <dc:creator>Harshil Rami</dc:creator>
      <pubDate>Wed, 22 Apr 2026 18:03:58 +0000</pubDate>
      <link>https://dev.to/harshil_rami_8533a7388ef7/blog-2-momentum-based-optimizers-2h98</link>
      <guid>https://dev.to/harshil_rami_8533a7388ef7/blog-2-momentum-based-optimizers-2h98</guid>
      <description>&lt;h3&gt;
  
  
  &lt;em&gt;Giving the optimizer a memory — and teaching it to look before it leaps&lt;/em&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;SGD knows where it is. Momentum knows where it's been. Nesterov knows where it's going.&lt;br&gt;
That single sentence is the entire story of this post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The ravine problem, visualized
&lt;/h2&gt;

&lt;p&gt;Let's be concrete about what SGD's zig-zagging actually looks like.&lt;/p&gt;

&lt;p&gt;Suppose your loss surface is an elongated valley — steep walls on the left and right, a gentle slope running toward the minimum far ahead. This is the classic &lt;strong&gt;ravine geometry&lt;/strong&gt;, and it's not an academic toy. It shows up naturally when your features have very different scales, when layers have different learning dynamics, or when you're in the early phases of training a deep network.&lt;/p&gt;

&lt;p&gt;SGD's update on this surface behaves as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Along the &lt;strong&gt;steep axis&lt;/strong&gt; (across the ravine): the gradient is large. SGD takes a big step, overshoots, corrects back, overshoots again. The updates oscillate violently.&lt;/li&gt;
&lt;li&gt;Along the &lt;strong&gt;shallow axis&lt;/strong&gt; (down the ravine, toward the minimum): the gradient is small. SGD takes tiny, tentative steps. Progress is glacial.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a path that looks like a snake moving sideways more than forward. You can shrink the learning rate to tame the oscillations on the steep axis, but that makes the shallow axis even slower. There's no single learning rate that handles both directions well.&lt;/p&gt;

&lt;p&gt;This is the fundamental limitation SGD leaves on the table, and it's what momentum is designed to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Momentum: giving the optimizer velocity
&lt;/h2&gt;

&lt;p&gt;The core idea behind momentum is borrowed directly from physics. Instead of updating parameters based on the current gradient alone, we maintain a &lt;strong&gt;velocity vector&lt;/strong&gt; &lt;em&gt;v&lt;/em&gt; that accumulates a running average of past gradients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vₜ = β · vₜ₋₁ + η · ∇L(θₜ)
θₜ₊₁ = θₜ − vₜ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;em&gt;β&lt;/em&gt; (beta) is the momentum coefficient — typically 0.9. Some formulations absorb the learning rate differently; the semantics are equivalent.&lt;/p&gt;

&lt;p&gt;Let's unpack what this actually does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the oscillating (steep) axis:&lt;/strong&gt;&lt;br&gt;
Gradients alternate sign — positive, negative, positive, negative. The velocity accumulates these with the decay factor &lt;em&gt;β&lt;/em&gt;. Because they cancel each other out over time, the velocity along this axis stays small. Oscillations are damped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the consistent (shallow) axis:&lt;/strong&gt;&lt;br&gt;
Gradients consistently point in the same direction — always slightly downhill. The velocity accumulates these constructively. Each step adds to the previous. The effective step size grows, and the optimizer accelerates.&lt;/p&gt;

&lt;p&gt;This is the momentum effect: &lt;strong&gt;dampening in oscillating directions, acceleration in consistent ones.&lt;/strong&gt; The optimizer builds up speed where the surface is consistently sloped and brakes naturally where the surface is ambiguous.&lt;/p&gt;
&lt;h3&gt;
  
  
  Effective learning rate under momentum
&lt;/h3&gt;

&lt;p&gt;With &lt;em&gt;β&lt;/em&gt; = 0.9, a gradient that persists in the same direction for many steps produces a velocity roughly &lt;em&gt;1/(1−β) = 10×&lt;/em&gt; the nominal learning rate. This is why momentum often requires a slightly lower learning rate than vanilla SGD — the effective step size is larger.&lt;/p&gt;

&lt;p&gt;More precisely, if the gradient is constant at &lt;em&gt;g&lt;/em&gt;, the velocity converges to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v* = η · g / (1 − β)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So momentum scales up the effective learning rate by &lt;em&gt;1/(1−β)&lt;/em&gt;. Set &lt;em&gt;β = 0.9&lt;/em&gt; → 10× amplification. Set &lt;em&gt;β = 0.99&lt;/em&gt; → 100×. This amplification is the source of both momentum's power and its instability if misconfigured.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ball analogy
&lt;/h3&gt;

&lt;p&gt;Momentum is often described as a ball rolling down a hill. The ball doesn't instantly respond to every local slope — it carries inertia. A small bump doesn't stop it; it takes a sustained uphill slope to decelerate it meaningfully.&lt;/p&gt;

&lt;p&gt;This analogy is accurate and useful, but it has a limit: the ball analogy suggests the optimizer might overshoot and roll up the other side of a valley. That's a real failure mode of high-momentum settings. If &lt;em&gt;β&lt;/em&gt; is too large, the optimizer can oscillate around minima rather than settling into them, or sail through a narrow good basin entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nesterov Accelerated Gradient: look before you leap
&lt;/h2&gt;

&lt;p&gt;Momentum is good. Nesterov Accelerated Gradient (NAG), proposed by Yurii Nesterov in 1983, makes one surgical improvement that turns out to matter significantly in practice.&lt;/p&gt;

&lt;p&gt;The problem with standard momentum: &lt;strong&gt;the gradient is evaluated at the current position, before applying the velocity.&lt;/strong&gt; By the time you apply the update, you're no longer at that position — you've already moved. You're using stale directional information.&lt;/p&gt;

&lt;p&gt;NAG fixes this with a simple conceptual shift: &lt;strong&gt;evaluate the gradient at the position you're about to arrive at&lt;/strong&gt;, then correct from there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;θ_lookahead = θₜ − β · vₜ₋₁          # project forward
vₜ = β · vₜ₋₁ + η · ∇L(θ_lookahead)  # gradient at projected position
θₜ₊₁ = θₜ − vₜ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The momentum step projects you forward to where you'll be &lt;em&gt;before&lt;/em&gt; the gradient correction. Then you evaluate the gradient there. This means the correction accounts for the momentum-driven position, not the pre-momentum position.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this helps
&lt;/h3&gt;

&lt;p&gt;Think of it this way. Standard momentum is like running toward a wall and only noticing the wall &lt;em&gt;after&lt;/em&gt; you've taken your full step. Nesterov is like looking ahead as you run and starting to slow down &lt;em&gt;before&lt;/em&gt; you hit the wall.&lt;/p&gt;

&lt;p&gt;In regions where the momentum is carrying you toward a steep uphill, NAG detects that uphill slope earlier and applies a corrective force sooner. The update is more anticipatory than reactive.&lt;/p&gt;

&lt;p&gt;In practice, NAG converges faster than standard momentum on convex problems — Nesterov's original theoretical analysis showed an &lt;em&gt;O(1/k²)&lt;/em&gt; convergence rate versus SGD's &lt;em&gt;O(1/k)&lt;/em&gt;, a meaningful gap. For non-convex deep learning loss surfaces, the improvement is empirical rather than provably guaranteed, but it's consistently observed.&lt;/p&gt;

&lt;h3&gt;
  
  
  NAG in the equivalent update form
&lt;/h3&gt;

&lt;p&gt;The two-equation NAG formulation above has an equivalent single-equation form that's more commonly implemented:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vₜ = β · vₜ₋₁ + ∇L(θₜ − β · vₜ₋₁)
θₜ₊₁ = θₜ − η · vₜ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are equivalent; the second form makes it clearer that the only change from standard momentum is &lt;em&gt;where the gradient is evaluated&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gradient Descent&lt;/th&gt;
&lt;th&gt;SGD&lt;/th&gt;
&lt;th&gt;Momentum&lt;/th&gt;
&lt;th&gt;NAG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gradient source&lt;/td&gt;
&lt;td&gt;Full dataset&lt;/td&gt;
&lt;td&gt;Single sample&lt;/td&gt;
&lt;td&gt;Mini-batch&lt;/td&gt;
&lt;td&gt;Mini-batch (at projected pos.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Velocity &lt;em&gt;vₜ&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Velocity &lt;em&gt;vₜ&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oscillation handling&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Dampens via averaging&lt;/td&gt;
&lt;td&gt;Dampens + anticipates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Convergence rate (convex)&lt;/td&gt;
&lt;td&gt;&lt;em&gt;O(1/k)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;O(1/√k)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;O(1/k)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;O(1/k²)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical &lt;em&gt;β&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;0.9–0.99&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The convergence rate column is worth reading carefully. SGD's &lt;em&gt;O(1/√k)&lt;/em&gt; is actually &lt;em&gt;worse&lt;/em&gt; than GD's &lt;em&gt;O(1/k)&lt;/em&gt; — the variance of stochastic gradients costs you a square root. Momentum restores GD-level rates. Nesterov goes further.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation and practical notes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Standard Momentum
&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataloader&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;compute_gradient&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;model&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;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;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="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;

&lt;span class="c1"&gt;# Nesterov Accelerated Gradient
&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataloader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Evaluate gradient at lookahead position
&lt;/span&gt;    &lt;span class="n"&gt;theta_lookahead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&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;compute_gradient&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;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;theta_lookahead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;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="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In PyTorch, both are available via &lt;code&gt;torch.optim.SGD&lt;/code&gt; with &lt;code&gt;momentum&lt;/code&gt; and &lt;code&gt;nesterov&lt;/code&gt; flags:&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;# Momentum
&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SGD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;momentum&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# NAG
&lt;/span&gt;&lt;span class="n"&gt;optimizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SGD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;momentum&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nesterov&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Practical hyperparameter guidance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;β = 0.9&lt;/em&gt; is the standard default. It works well across a wide range of architectures.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;β = 0.99&lt;/em&gt; gives stronger smoothing but risks slower response to genuine direction changes and can overshoot narrow minima.&lt;/li&gt;
&lt;li&gt;When switching from SGD to momentum, reduce the learning rate by roughly &lt;em&gt;1/(1−β)&lt;/em&gt; — so if &lt;em&gt;η = 0.1&lt;/em&gt; for SGD, try &lt;em&gt;η = 0.01&lt;/em&gt; with &lt;em&gt;β = 0.9&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;NAG is almost always preferable to plain momentum for the same computational cost. Default to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where momentum still falls short
&lt;/h2&gt;

&lt;p&gt;Momentum is a major step forward. But it inherits one fundamental limitation from SGD: &lt;strong&gt;a single global learning rate for all parameters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your model's parameters live in very different regimes. The embedding layer of a language model sees only a handful of tokens in each batch — its effective gradient is sparse and noisy. The final linear layer sees a dense, consistent gradient every step. Both are updated with the same &lt;em&gt;η&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is deeply suboptimal. Sparse parameters should move faster when they do receive signal. Dense parameters can afford more conservative updates to avoid oscillation.&lt;/p&gt;

&lt;p&gt;Momentum has no mechanism to learn this. It smooths over time but not over parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's exactly the problem Blog 3 solves.&lt;/strong&gt; AdaGrad will introduce per-parameter learning rates, scaling each update by the history of that parameter's gradient magnitude. RMSProp will fix AdaGrad's long-term decay problem. And the combination of per-parameter scaling with momentum will eventually give us Adam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things to hold onto
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Velocity accumulation&lt;/td&gt;
&lt;td&gt;Past gradients persist via &lt;em&gt;β&lt;/em&gt; decay&lt;/td&gt;
&lt;td&gt;Accelerates in consistent directions, dampens oscillations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookahead gradient (NAG)&lt;/td&gt;
&lt;td&gt;Gradient evaluated at projected position&lt;/td&gt;
&lt;td&gt;Earlier correction, better convergence rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective LR scaling&lt;/td&gt;
&lt;td&gt;Velocity → &lt;em&gt;η/(1−β)&lt;/em&gt; effective step&lt;/td&gt;
&lt;td&gt;Must tune LR down when adding momentum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Blog 3&lt;/strong&gt;, we shift from &lt;em&gt;when&lt;/em&gt; the optimizer has seen a gradient to &lt;em&gt;which parameters&lt;/em&gt; have seen large gradients. AdaGrad introduces a per-parameter accumulator — parameters that receive frequent, large gradients get smaller effective learning rates; sparse parameters get larger ones. RMSProp then fixes AdaGrad's fatal flaw: the accumulator grows without bound, eventually shrinking all learning rates to zero.&lt;/p&gt;

&lt;p&gt;If momentum gave the optimizer a memory across time, adaptive methods give it a memory across parameters. Both are necessary. Neither is sufficient alone.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is Blog 2 of an 8-part series on optimization algorithms for deep learning.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>gradientdescent</category>
    </item>
    <item>
      <title>Blog 1: Foundations of Gradient Descent</title>
      <dc:creator>Harshil Rami</dc:creator>
      <pubDate>Wed, 22 Apr 2026 17:54:43 +0000</pubDate>
      <link>https://dev.to/harshil_rami_8533a7388ef7/blog-1-foundations-of-gradient-descent-p6n</link>
      <guid>https://dev.to/harshil_rami_8533a7388ef7/blog-1-foundations-of-gradient-descent-p6n</guid>
      <description>&lt;h3&gt;
  
  
  &lt;em&gt;How neural networks learn — and why the obvious approach breaks immediately&lt;/em&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Every optimizer you'll ever use — Adam, AdamW, Lion, LAMB — is an answer to a problem that gradient descent creates. To understand why those answers exist, you need to feel the problem first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The loss surface is a landscape you can't see
&lt;/h2&gt;

&lt;p&gt;Imagine you're blindfolded, standing somewhere on a hilly terrain. Your only tool is a stick: you can poke the ground around you and measure the slope. Your goal is to reach the lowest valley.&lt;/p&gt;

&lt;p&gt;That's optimization.&lt;/p&gt;

&lt;p&gt;The "terrain" is your loss surface — a high-dimensional function &lt;em&gt;L(θ)&lt;/em&gt; mapping your model's parameters &lt;em&gt;θ&lt;/em&gt; to a scalar loss. You can't see the whole surface. You can only evaluate the gradient at your current position and take a step.&lt;/p&gt;

&lt;p&gt;The question every optimizer tries to answer: &lt;strong&gt;which direction, and how far?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gradient Descent: the right idea, the wrong scale
&lt;/h2&gt;

&lt;p&gt;Gradient Descent (GD) is the foundational answer. Given a loss function &lt;em&gt;L(θ)&lt;/em&gt;, we compute the gradient over the entire dataset and update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;θ ← θ − η · ∇L(θ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;em&gt;η&lt;/em&gt; (eta) is the learning rate — a scalar controlling step size.&lt;/p&gt;

&lt;p&gt;The update rule is clean. The gradient points in the direction of steepest ascent, so we move opposite to it. Mathematically, this is the direction of maximum local decrease in loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The intuition is correct. The implementation is catastrophically expensive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To compute &lt;code&gt;∇L(θ)&lt;/code&gt; exactly, you need to pass your entire dataset through the model. For ImageNet-scale data or modern LLM corpora, this means billions of examples per update. You'd compute one parameter update per epoch. On a 100M parameter model. That's not slow — it's dead on arrival.&lt;/p&gt;

&lt;p&gt;GD also has a subtle failure mode people underappreciate: when your dataset has redundant structure (and it almost always does), successive gradients are nearly identical. You're paying full-dataset cost for almost zero additional information after the first few passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stochastic Gradient Descent: embrace the noise
&lt;/h2&gt;

&lt;p&gt;The fix seems almost too simple: instead of computing the gradient over all &lt;em&gt;N&lt;/em&gt; samples, pick &lt;strong&gt;one sample at random&lt;/strong&gt; and update on that alone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;θ ← θ − η · ∇Lᵢ(θ)    for a randomly sampled i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is Stochastic Gradient Descent (SGD). The gradient estimate is now noisy — it's a single-sample approximation of the true gradient. But that noise turns out to be a feature, not a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why noisy updates help:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Escaping shallow local minima.&lt;/strong&gt; A noisy gradient doesn't always point exactly downhill. This stochasticity gives the optimizer a kind of thermal energy — it can jitter out of shallow basins that would trap a deterministic update.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better generalization (empirically).&lt;/strong&gt; The noise in SGD acts as implicit regularization. Models trained with SGD often generalize better than those trained with exact gradient methods, particularly in overparameterized regimes. There's a growing body of theory around this — the "flat minima" hypothesis suggests noisy SGD preferentially finds wider, flatter basins that transfer better to test data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Speed per effective update.&lt;/strong&gt; One SGD step is O(1) in data cost. You can make &lt;em&gt;N&lt;/em&gt; updates in the time GD makes one, seeing every sample along the way.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The cost of noise:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SGD's gradient estimate has high variance. Updates zig-zag erratically, especially in directions where the loss surface has high curvature along one axis and low curvature along another (the classic "ravine" geometry). The path to the minimum looks like a drunk person's walk rather than a confident descent.&lt;/p&gt;

&lt;p&gt;You can reduce the learning rate to smooth this out, but then you lose the speed advantage. You're always trading variance against convergence rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mini-Batch Gradient Descent: the practical compromise
&lt;/h2&gt;

&lt;p&gt;The resolution in practice is obvious in retrospect: &lt;strong&gt;use a small batch of &lt;em&gt;B&lt;/em&gt; samples&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;θ ← θ − η · (1/B) · Σᵢ∈Bₜ ∇Lᵢ(θ)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;em&gt;Bₜ&lt;/em&gt; is a randomly sampled mini-batch of size &lt;em&gt;B&lt;/em&gt; at step &lt;em&gt;t&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is Mini-Batch Gradient Descent (MBGD) — and when practitioners say "SGD" today, this is almost always what they mean. Typical batch sizes range from 32 to 512, though the right choice depends on your model, hardware, and regularization goals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What mini-batching buys you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variance reduction.&lt;/strong&gt; Averaging over &lt;em&gt;B&lt;/em&gt; samples reduces gradient variance by a factor of &lt;em&gt;B&lt;/em&gt; compared to single-sample SGD, without &lt;em&gt;B&lt;/em&gt;× the compute cost (thanks to parallelism on GPU/TPU).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware efficiency.&lt;/strong&gt; GPUs are throughput machines — they saturate at batch sizes that fully utilize memory bandwidth. A single-sample forward pass wastes most of your compute budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enough noise to generalize.&lt;/strong&gt; Mini-batch gradients are still noisy enough to provide the regularization benefits of SGD, unlike full-batch gradients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The batch size isn't free:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Larger batches reduce noise, which sounds good, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They can converge to &lt;strong&gt;sharper minima&lt;/strong&gt; with worse generalization (the "large-batch training problem" — Keskar et al., 2017).&lt;/li&gt;
&lt;li&gt;They require &lt;strong&gt;proportionally larger learning rates&lt;/strong&gt; to maintain the same effective update magnitude, but scaling LR linearly with batch size breaks down at large B.&lt;/li&gt;
&lt;li&gt;Beyond a critical batch size, you're paying compute cost without improving convergence speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This last point becomes the central tension in Blog 6, when we look at LARS and LAMB — optimizers specifically designed to handle very large batches in distributed LLM training.&lt;/p&gt;

&lt;h2&gt;
  
  
  The update rule in full
&lt;/h2&gt;

&lt;p&gt;Here's where we stand after mini-batch SGD. The complete 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;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="n"&gt;num_epochs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&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;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;get_batches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&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="n"&gt;B&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;compute_gradient&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;model&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;θ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;θ&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. It's what trains ResNets, early language models, and still forms the backbone of large-scale training in certain regimes (SGD with momentum remains competitive with Adam on image classification tasks).&lt;/p&gt;

&lt;p&gt;But watch what happens on a ravine-shaped loss surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The gradient along the short axis (high curvature) is large → big oscillating steps&lt;/li&gt;
&lt;li&gt;The gradient along the long axis (low curvature, toward the minimum) is small → slow progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The optimizer zig-zags across the ravine instead of marching down it. You need a very small learning rate to prevent divergence on the steep axis, which makes the shallow axis painfully slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is exactly the problem Blog 2 solves.&lt;/strong&gt; Momentum will give the optimizer memory — a velocity vector that accumulates in persistent directions and dampens oscillations. Nesterov will take that one step further, looking ahead before committing to the update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things to hold onto
&lt;/h2&gt;

&lt;p&gt;Before moving to momentum, here are the three tensions that the rest of this series is spent resolving:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What causes it&lt;/th&gt;
&lt;th&gt;Solved by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Slow, expensive updates&lt;/td&gt;
&lt;td&gt;Full-dataset gradient&lt;/td&gt;
&lt;td&gt;SGD / Mini-batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-variance, zig-zagging path&lt;/td&gt;
&lt;td&gt;Single/small-batch noise&lt;/td&gt;
&lt;td&gt;Momentum (Blog 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uniform learning rate for all params&lt;/td&gt;
&lt;td&gt;LR is a global scalar&lt;/td&gt;
&lt;td&gt;AdaGrad, RMSProp (Blog 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every optimizer from here on is a targeted intervention on one of these failure modes — or a combination of several at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key equations, plain-English summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Update rule&lt;/th&gt;
&lt;th&gt;One sentence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;θ ← θ − η · ∇L(θ)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exact gradient, entire dataset, one step per epoch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;θ ← θ − η · ∇Lᵢ(θ)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Noisy gradient, one sample, fast but erratic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MBGD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;θ ← θ − η · (1/B)·Σ∇Lᵢ(θ)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Averaged gradient, batch of B, the practical default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Blog 2&lt;/strong&gt;, we add velocity. Momentum accumulates past gradients into a running average, smoothing the zig-zagging path and accelerating convergence in consistent directions. Nesterov takes the lookahead step — evaluating the gradient at a projected future position rather than the current one.&lt;/p&gt;

&lt;p&gt;If SGD is someone walking blindfolded downhill, momentum is that same person carrying a ball that's already rolling. It takes more to change direction. That turns out to be exactly what you want.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is Blog 1 of an 8-part series on optimization algorithms for deep learning. Each post covers one family of optimizers, following a problem → limitation → next solution arc.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>gradientdescent</category>
    </item>
  </channel>
</rss>
