<?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: Wesam Khallaf — Author of PyTorch From Ground Up</title>
    <description>The latest articles on DEV Community by Wesam Khallaf — Author of PyTorch From Ground Up (@pytorchfromgroundup).</description>
    <link>https://dev.to/pytorchfromgroundup</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4031668%2F3c62ffd1-c10e-4b87-87fd-851ba5562408.png</url>
      <title>DEV Community: Wesam Khallaf — Author of PyTorch From Ground Up</title>
      <link>https://dev.to/pytorchfromgroundup</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pytorchfromgroundup"/>
    <language>en</language>
    <item>
      <title>Backpropagation by Hand: Two Layers, a Pen, and Then Autograd Agrees</title>
      <dc:creator>Wesam Khallaf — Author of PyTorch From Ground Up</dc:creator>
      <pubDate>Sun, 23 Aug 2026 07:20:10 +0000</pubDate>
      <link>https://dev.to/pytorchfromgroundup/backpropagation-by-hand-two-layers-a-pen-and-then-autograd-agrees-13i6</link>
      <guid>https://dev.to/pytorchfromgroundup/backpropagation-by-hand-two-layers-a-pen-and-then-autograd-agrees-13i6</guid>
      <description>&lt;p&gt;in the last piece i did autograd, and the whole point of it was that you call &lt;code&gt;loss.backward()&lt;/code&gt; once and pytorch fills in every gradient for you, and a couple of people basically said the same thing back to me, ok that is nice but on a real network with more than one layer what is it actually doing in there. so this time i want to do the thing you will genuinely never do inside a training loop, i want to take a two-layer network and compute every gradient by hand, one node at a time, and then type the same thing into pytorch and watch autograd land on the exact same numbers. the one-neuron version was the warm-up. this is the one where the chain rule has to travel back through a hidden layer, and where the interesting failures live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The network, small enough to hold in your head
&lt;/h2&gt;

&lt;p&gt;Two layers. one input, a hidden neuron with a ReLU, then an output neuron, then a squared-error loss. that is it. i am going to write it as plain scalars first because the whole trick is visible that way and nothing is hiding inside a matrix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x ──▶ h1 = w1·x + b1 ──▶ a1 = ReLU(h1) ──▶ h2 = w2·a1 + b2 ──▶ L = (h2 − y)²
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;four parameters to find gradients for: &lt;code&gt;w1&lt;/code&gt;, &lt;code&gt;b1&lt;/code&gt;, &lt;code&gt;w2&lt;/code&gt;, &lt;code&gt;b2&lt;/code&gt;. and i am picking numbers so there is nothing to hide behind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x  = 1.0
w1 = 2.0    b1 = 0.0
w2 = 3.0    b2 = 1.0
y  = 2.0     (the target)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Forward pass, left to right, writing everything down
&lt;/h2&gt;

&lt;p&gt;you compute the answer first, and you keep every intermediate value, because the backward pass needs them. this is not optional bookkeeping, the local derivatives literally reuse these numbers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h1 = w1·x + b1 = 2·1 + 0   = 2.0
a1 = ReLU(h1)  = max(0, 2)  = 2.0
h2 = w2·a1 + b2 = 3·2 + 1   = 7.0
L  = (h2 − y)²  = (7 − 2)²   = 25.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;loss is 25. now the only question that matters is how each of the four parameters should move to make that 25 smaller, and that is exactly what a gradient is, &lt;code&gt;∂L/∂w1&lt;/code&gt; and so on, the slope of the&lt;br&gt;
loss with respect to that one number while everything else is held still.&lt;/p&gt;
&lt;h2&gt;
  
  
  Backward pass, one node at a time, right to left
&lt;/h2&gt;

&lt;p&gt;the entire method is this, and it does not get more complicated no matter how deep the net is: at each node you take the gradient arriving from the right, and you multiply it by that node's own&lt;br&gt;
local derivative, and you pass the result further left. that is the chain rule and there is nothing else to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;loss → h2.&lt;/strong&gt; &lt;code&gt;L = (h2 − y)²&lt;/code&gt;, so the derivative of the loss with respect to &lt;code&gt;h2&lt;/code&gt; is &lt;code&gt;2(h2 − y)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;∂L/∂h2 = 2·(7 − 2) = 10.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;h2 → the output parameters.&lt;/strong&gt; &lt;code&gt;h2 = w2·a1 + b2&lt;/code&gt;. the derivative of &lt;code&gt;h2&lt;/code&gt; with respect to &lt;code&gt;w2&lt;/code&gt; is just &lt;code&gt;a1&lt;/code&gt;, and with respect to &lt;code&gt;b2&lt;/code&gt; it is 1, so multiply each by the 10 coming in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;∂L/∂w2 = 10 · a1  = 10 · 2 = 20.0
∂L/∂b2 = 10 · 1   = 10.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so far identical in spirit to the one-neuron case. here is the part that only shows up once you have a hidden layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;h2 → a1, i.e. keep going left into the first layer.&lt;/strong&gt; &lt;code&gt;h2 = w2·a1 + b2&lt;/code&gt;, so the derivative of &lt;code&gt;h2&lt;/code&gt; with respect to &lt;code&gt;a1&lt;/code&gt; is &lt;code&gt;w2&lt;/code&gt;. the gradient does not stop at the output layer, it flows backthrough &lt;code&gt;w2&lt;/code&gt; into the hidden activation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;∂L/∂a1 = 10 · w2 = 10 · 3 = 30.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;a1 → h1, through the ReLU.&lt;/strong&gt; ReLU passes its input when it was positive and its derivative is 1 there, 0 when the input was negative. our &lt;code&gt;h1&lt;/code&gt; was 2, positive, so the gate is open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;∂a1/∂h1 = 1     (because h1 &amp;gt; 0)
∂L/∂h1  = 30 · 1 = 30.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;h1 → the first-layer parameters.&lt;/strong&gt; &lt;code&gt;h1 = w1·x + b1&lt;/code&gt;, derivative with respect to &lt;code&gt;w1&lt;/code&gt; is &lt;code&gt;x&lt;/code&gt;, with respect to &lt;code&gt;b1&lt;/code&gt; is 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;∂L/∂w1 = 30 · x = 30 · 1 = 30.0
∂L/∂b1 = 30 · 1 = 30.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;done, by hand, four gradients:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;∂L/∂w1 = 30    ∂L/∂b1 = 30    ∂L/∂w2 = 20    ∂L/∂b2 = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;notice the shape of what happened. the &lt;code&gt;10&lt;/code&gt; computed at the output got carried all the way back to the first layer, multiplied by &lt;code&gt;w2&lt;/code&gt; on the way through, then by the ReLU's &lt;code&gt;1&lt;/code&gt;, then by &lt;code&gt;x&lt;/code&gt;. every&lt;br&gt;
gradient in the network is that same number from the loss, multiplied by a chain of local slopes between it and the parameter. deeper nets are just longer chains.&lt;/p&gt;
&lt;h2&gt;
  
  
  Now ask PyTorch the same question
&lt;/h2&gt;

&lt;p&gt;same numbers, same five-ish lines, mark the four parameters as needing gradients, run forward, call backward once, print:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;x&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;w1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;b1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;w2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;b2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;y&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;h1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b1&lt;/span&gt;
&lt;span class="n"&gt;a1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;h2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;a1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b2&lt;/span&gt;
&lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="n"&gt;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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loss   = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# 25.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dL/dw1 = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 30.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dL/db1 = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 30.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dL/dw2 = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 20.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dL/db2 = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 10.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;all four match, and the loss matches. autograd traced the same chain of operations, stored the same intermediate values on the forward pass, and multiplied the same local derivatives on the way back.&lt;br&gt;
the only thing it did that you did not is bookkeeping, it just did it without asking you and it would do it the same way with ten million parameters instead of four.&lt;/p&gt;

&lt;p&gt;if you read nothing else in this article, run that block and put it next to the hand calculation above. the whole point is the moment where the pen and the computer agree.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that turns this into a debugging skill
&lt;/h2&gt;

&lt;p&gt;keep the network the same but break the hidden neuron on purpose. flip &lt;code&gt;w1&lt;/code&gt; to &lt;code&gt;-2&lt;/code&gt; and leave everything else. now the first layer produces a negative pre-activation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h1 = w1·x + b1 = -2·1 + 0 = -2.0
a1 = ReLU(-2)  = 0.0
h2 = w2·a1 + b2 = 3·0 + 1 = 1.0
L  = (1 − 2)²   = 1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;run backward on this one and look at what comes out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;w1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="c1"&gt;# the only change
&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;w2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;b2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;h1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b1&lt;/span&gt;
&lt;span class="n"&gt;a1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;h2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;a1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b2&lt;/span&gt;
&lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# 0.0 0.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;b2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# 0.0 -2.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;three of the four gradients are zero, and the loss is not, the network is wrong and mostly getting no signal about it. and because you just did the hand version you can say exactly why each zero&lt;br&gt;
happened instead of guessing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;w1&lt;/code&gt; and &lt;code&gt;b1&lt;/code&gt; are zero because the ReLU's derivative is 0 when its input was negative, and that 0 sits in the middle of the chain, so everything behind it gets multiplied down to nothing. that is&lt;br&gt;
the dead-ReLU problem and this is it in miniature, a neuron whose input is negative gets no gradient and stops learning, quietly, no error printed.&lt;br&gt;
but look at &lt;code&gt;w2&lt;/code&gt;, it is also zero, and the ReLU is not the reason. &lt;code&gt;∂L/∂w2 = ∂L/∂h2 · a1&lt;/code&gt;, and &lt;code&gt;a1&lt;/code&gt;is 0 here, so the weight that reads &lt;em&gt;from&lt;/em&gt; the dead neuron gets a zero gradient too, because you&lt;br&gt;
cannot learn how to weight an input that is always zero. so a single dead neuron in the hidden layer takes out both the parameters feeding it and the weight reading out of it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;b2&lt;/code&gt; is the survivor, &lt;code&gt;∂L/∂b2 = ∂L/∂h2 = -2&lt;/code&gt;, the output bias sits after the dead neuron so the zero never reaches it, and it keeps learning. that is the whole reason this is worth doing on two&lt;br&gt;
layers and not one, on one neuron you just see "gradient is zero", on two you see the zero spread along the chain and stop where the chain does.&lt;/p&gt;

&lt;p&gt;this is the payoff of the exercise. you will never backprop by hand in a real loop, autograd owns that. but the day a model just sits there and does not improve and you print a gradient and it is 0,&lt;br&gt;
you will not be staring at it, you will go straight to asking what on the path back is multiplying by zero, a dead ReLU, a &lt;code&gt;detach&lt;/code&gt; you forgot about, an activation that saturated, and you will know that&lt;br&gt;
a zero at one node zeros everything behind it but not in front of it, because you have already watched it happen on four parameters you could hold in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that may be confusing at first glance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;the gradient does not stop at the layer that produced the loss.&lt;/strong&gt; the most common beginner mental model is that each layer computes its own gradient locally and independently. it does not. the &lt;code&gt;10&lt;/code&gt;&lt;br&gt;
from the output travelled all the way to &lt;code&gt;w1&lt;/code&gt;. every layer's gradient depends on every layer after it, that dependence is the &lt;code&gt;· w2&lt;/code&gt; step, and it is why the order is strictly right to left and why you cannot parallelise the backward pass across depth the way you can the forward one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gradients accumulate, they do not overwrite.&lt;/strong&gt; if you run &lt;code&gt;loss.backward()&lt;/code&gt; twice without clearing,&lt;br&gt;
the second set of numbers gets added onto the first and you will read &lt;code&gt;60, 60, 40, 20&lt;/code&gt; on the second call and wonder what happened. that is deliberate, it is what lets you sum gradients over several mini-batches, but it means &lt;code&gt;optimizer.zero_grad()&lt;/code&gt; before every backward is load-bearing, not boilerplate. i wrote about why that is in the autograd piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it before you close the tab
&lt;/h2&gt;

&lt;p&gt;take the healthy network again, &lt;code&gt;x=1, w1=2, b1=0, w2=3, b2=1, y=2&lt;/code&gt;, and this time add a second hidden step of your own, say multiply &lt;code&gt;a1&lt;/code&gt; by another weight &lt;code&gt;w3 = 0.5&lt;/code&gt; before the output. write out&lt;br&gt;
the full chain for &lt;code&gt;∂L/∂w1&lt;/code&gt; on paper, there is just one more factor in it now, then check against autograd. it takes a few minutes and it is worth more than rereading this.&lt;/p&gt;

&lt;p&gt;then the mean one, set &lt;code&gt;b1 = -5&lt;/code&gt; so the hidden ReLU dies, predict which of the four gradients go to zero &lt;em&gt;before&lt;/em&gt; you run it, and see if you called it right. if you can predict the zeros you have&lt;br&gt;
actually got the chain rule, if you cannot you have more to gain from running it than from any amount of reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;what is the worst "model just would not learn and the gradient was zero" you have hit?&lt;/strong&gt; for me the first real one was a dead ReLU exactly like this, a whole layer of them, and it cost me most of a day before i thought to print a gradient. i am curious whether that is the usual first one or whether everyone finds their own way to multiply by zero.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is one chapter's worth of an idea from my book,&lt;/em&gt; &lt;strong&gt;PyTorch From Ground Up&lt;/strong&gt;, &lt;em&gt;which builds everything from tensors upward so nothing stays vague. If it helped:&lt;br&gt;
&lt;a href="https://github.com/pytorch-from-ground-up/book_code/blob/main/PyTorch-From-Ground-Up-8-Free-Chapters.pdf" rel="noopener noreferrer"&gt;8 chapters are free, no email required&lt;/a&gt;, there's a &lt;a href="https://payhip.com/b/7ukxh" rel="noopener noreferrer"&gt;free one-page tensor cheat-sheet here&lt;/a&gt;, every example runs in&lt;br&gt;
the &lt;a href="https://github.com/pytorch-from-ground-up/book_code" rel="noopener noreferrer"&gt;companion notebooks on GitHub&lt;/a&gt;, and the full book is on &lt;a href="https://leanpub.com/pytorchfromgroundup" rel="noopener noreferrer"&gt;Leanpub&lt;/a&gt; or in&lt;br&gt;
&lt;a href="https://www.amazon.com/dp/B0H8WMCV33" rel="noopener noreferrer"&gt;paperback and Kindle on Amazon&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  More in this series
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How Training Actually Works&lt;/strong&gt;, the part where the training loop stops being magic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/pytorchfromgroundup/pytorch-autograd-explained-what-backward-actually-does-4acg"&gt;PyTorch Autograd Explained: What &lt;code&gt;.backward()&lt;/code&gt; Actually Does&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shape mechanics underneath all of it, worth having solid first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o"&gt;Reshape vs View in PyTorch&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606"&gt;PyTorch Broadcasting Explained&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/pytorchfromgroundup/pytorch-permute-vs-transpose-whats-the-difference-and-the-reshape-bug-that-scrambles-your-7ke"&gt;&lt;code&gt;permute&lt;/code&gt; vs &lt;code&gt;transpose&lt;/code&gt;: What's the Difference (and the &lt;code&gt;reshape&lt;/code&gt;&lt;br&gt;
Bug That Scrambles Your Images)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/pytorchfromgroundup/what-does-keepdim-do-in-pytorch-and-the-silent-bug-when-you-forget-it-17p1"&gt;What Does &lt;code&gt;keepdim&lt;/code&gt; Do in PyTorch?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a"&gt;What Does &lt;code&gt;unsqueeze&lt;/code&gt; Do in PyTorch?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coming next in &lt;em&gt;How Training Actually Works&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What an optimizer actually does with the gradient &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pytorch</category>
      <category>python</category>
      <category>machinelearning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>PyTorch Autograd Explained: What .backward() Actually Does</title>
      <dc:creator>Wesam Khallaf — Author of PyTorch From Ground Up</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:50:08 +0000</pubDate>
      <link>https://dev.to/pytorchfromgroundup/pytorch-autograd-explained-what-backward-actually-does-4acg</link>
      <guid>https://dev.to/pytorchfromgroundup/pytorch-autograd-explained-what-backward-actually-does-4acg</guid>
      <description>&lt;p&gt;most &lt;strong&gt;&lt;em&gt;autograd&lt;/em&gt;&lt;/strong&gt; tutorials show you &lt;strong&gt;&lt;em&gt;requires_grad&lt;/em&gt;&lt;/strong&gt;, then .&lt;em&gt;backward&lt;/em&gt;(), then .&lt;em&gt;grad&lt;/em&gt;, and then they hand you a training loop, and you come out of it able to run autograd and still not able to say what it did, and i think that is backwards. so in this one you do the derivative first, by hand, on a tiny network that fits on one page, and only once you have your own number do we run pytorch and check it against .grad. the two agree to the decimal. and that is the point where autograd stops being magic, because you already did the thing it does, you just did it slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what a gradient actually is
&lt;/h2&gt;

&lt;p&gt;Skip this section if you are comfortable with it, but most confusion about autograd is really confusion about what it is producing.&lt;/p&gt;

&lt;p&gt;Imagine standing on a hillside in fog. You cannot see the bottom, but you can feel that the ground tilts. Step in the direction it tilts downward and you get lower. That is a gradient: the slope of the ground under your feet.&lt;/p&gt;

&lt;p&gt;Now put that on a graph. The horizontal axis is one adjustable number inside the model, a parameter. The vertical axis is the loss. The curve shows how the loss changes as the parameter&lt;br&gt;
changes, and somewhere on it there is a lowest point.&lt;/p&gt;

&lt;p&gt;Take &lt;code&gt;y = x²&lt;/code&gt;. Its slope at position &lt;code&gt;x&lt;/code&gt; is &lt;code&gt;2x&lt;/code&gt;. At &lt;code&gt;x = 3&lt;/code&gt; the slope is 6, steep and positive, so the loss climbs to the right and you should step left. At &lt;code&gt;x = -2&lt;/code&gt; it is -4, so you step right. At &lt;code&gt;x = 0&lt;/code&gt; it is 0, and you have arrived.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;y&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# tensor(6.)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PyTorch produced the slope without being told the formula. That is autograd, and the rest of this article is how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning on the tape
&lt;/h2&gt;

&lt;p&gt;Tensors do not track gradients by default. That would be wasted work on your input data, which is never adjusted. You opt in with &lt;code&gt;requires_grad=True&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requires_grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this moment every operation involving &lt;code&gt;x&lt;/code&gt; gets recorded. Think of it as a receipt. Each multiplication, addition and power you apply gets written down, along with enough information to reverse it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The computation graph
&lt;/h2&gt;

&lt;p&gt;The receipt has a proper name, the computation graph. It is a chain of nodes where each node is an operation and each edge carries a tensor from one operation into the next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;        &lt;span class="c1"&gt;# node: power
&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;     &lt;span class="c1"&gt;# nodes: multiply, then add
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines of Python, four values, three operation nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x (leaf)      **2         ×2          +1        z
  3.0    →    9.0    →   18.0   →    19.0   →  19.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the forward pass. It runs left to right, computes the answer, and as a side effect builds the graph. Nothing has been differentiated yet. The graph exists purely so that something can walk it backwards.&lt;/p&gt;

&lt;p&gt;Two things are worth noticing here. First, &lt;code&gt;x&lt;/code&gt; is a &lt;strong&gt;leaf&lt;/strong&gt; — you created it directly rather than computing it from something else. Second, every result node remembers the operation that produced it. You can see this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad_fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# &amp;lt;AddBackward0 object at ...&amp;gt;
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad_fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# &amp;lt;PowBackward0 object at ...&amp;gt;
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad_fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# None  ← leaves have no history
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;grad_fn&lt;/code&gt; is the recording. &lt;code&gt;AddBackward0&lt;/code&gt; is not the addition, it is the &lt;em&gt;instruction for reversing&lt;/em&gt; the addition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling .backward()
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;z&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# tensor(12.)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it by hand. &lt;code&gt;z = 2x² + 1&lt;/code&gt;, so &lt;code&gt;dz/dx = 4x&lt;/code&gt;, and at &lt;code&gt;x = 3&lt;/code&gt; that is 12. Correct.&lt;/p&gt;

&lt;p&gt;But the interesting part is not that the answer is right, it is how it was produced, because PyTorch never formed the expression &lt;code&gt;4x&lt;/code&gt; at all. It walked the graph right to left and at each node multiplied the incoming gradient by that node's own local slope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;z = 19          +1              ×2              **2            x.grad
start           slope 1         slope 2         slope 2x = 6
grad 1     →    grad 1     →    grad 2     →    grad 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading right to left: the &lt;code&gt;+1&lt;/code&gt; node has slope 1, so the gradient passes through unchanged. The &lt;code&gt;×2&lt;/code&gt; node has slope 2, so the gradient doubles. The &lt;code&gt;x²&lt;/code&gt; node has slope &lt;code&gt;2x&lt;/code&gt;, which at &lt;code&gt;x = 3&lt;/code&gt; is 6, so the gradient multiplies by 6. Altogether &lt;code&gt;1 × 2 × 6 = 12&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the whole of backpropagation. It is the chain rule, applied one node at a time, right to left, automatically. Nobody ever writes down &lt;code&gt;4x&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This also explains why the forward pass has to happen first, and why it has to store its intermediate values. The &lt;code&gt;x²&lt;/code&gt; node's local slope is &lt;code&gt;2x&lt;/code&gt;, which needs the &lt;em&gt;value&lt;/em&gt; of &lt;code&gt;x&lt;/code&gt; that went in. Every node keeps whatever it needs to compute its own derivative later. This is where the memory goes during training, and it is why a batch that fits in memory for inference can still run you out of memory when training.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now do it on an actual network, by hand
&lt;/h2&gt;

&lt;p&gt;Everything above is one variable. Here is the smallest thing that is honestly a neural network:&lt;br&gt;
one input, one weight, one bias, a ReLU, and a squared-error loss. Five operations. Enough to show every part of the chain rule, small enough to hold in your head.&lt;/p&gt;

&lt;p&gt;Numbers: &lt;code&gt;x = 2.0&lt;/code&gt;, &lt;code&gt;w = 3.0&lt;/code&gt;, &lt;code&gt;b = -1.0&lt;/code&gt;, target &lt;code&gt;y = 2.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward pass&lt;/strong&gt;, left to right, writing down everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h = w·x + b     = 3·2 + (-1)  = 5.0
a = ReLU(h)     = max(0, 5)   = 5.0
L = (a - y)²    = (5 - 2)²    = 9.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loss is 9. Now the question that matters: how should &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; change to make it smaller?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backward pass&lt;/strong&gt;, one node at a time, right to left. At each node, local derivative times the&lt;br&gt;
gradient arriving from the right.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Loss to &lt;code&gt;a&lt;/code&gt;.&lt;/em&gt; &lt;code&gt;L = (a - y)²&lt;/code&gt;, so &lt;code&gt;∂L/∂a = 2(a - y) = 2(5 - 2) = 6.0&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;a&lt;/code&gt; to &lt;code&gt;h&lt;/code&gt;, through the ReLU.&lt;/em&gt; ReLU's derivative is 1 if its input was positive and 0 if it was&lt;br&gt;
negative. Our &lt;code&gt;h&lt;/code&gt; was 5, positive, so &lt;code&gt;∂a/∂h = 1.0&lt;/code&gt; and therefore&lt;br&gt;
&lt;code&gt;∂L/∂h = 6.0 × 1.0 = 6.0&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;h&lt;/code&gt; to &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;.&lt;/em&gt; Since &lt;code&gt;h = w·x + b&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;∂h/∂w = x = 2.0    →    ∂L/∂w = 6.0 × 2.0 = 12.0
∂h/∂b = 1.0        →    ∂L/∂b = 6.0 × 1.0 = 6.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So by hand: &lt;code&gt;∂L/∂w = 12&lt;/code&gt; and &lt;code&gt;∂L/∂b = 6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now ask PyTorch the same question:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="n"&gt;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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loss  = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# 9.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dL/dw = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 12.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dL/db = &lt;/span&gt;&lt;span class="si"&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 6.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exact match, all three. Autograd traced the same five operations and applied the same chain rule. It just did it without asking you.&lt;/p&gt;

&lt;p&gt;If you read nothing else here, run that block and compare it to the hand calculation above. The whole point of the exercise is the moment the numbers agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the ReLU is closed
&lt;/h2&gt;

&lt;p&gt;Change one number. Keep &lt;code&gt;w = 3&lt;/code&gt; and &lt;code&gt;b = -1&lt;/code&gt;, but feed &lt;code&gt;x = -1.0&lt;/code&gt;. Now &lt;code&gt;h = 3·(-1) + (-1) = -4&lt;/code&gt;, which is negative, so ReLU outputs 0 and its local derivative is also 0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;          &lt;span class="c1"&gt;# -4.0
&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;#  0.0
&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;    &lt;span class="c1"&gt;# (0 - 2)² = 4.0
&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# 0.0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# 0.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loss is 4, so the network is wrong, and yet both gradients are exactly zero. No learning&lt;br&gt;
signal reaches &lt;code&gt;w&lt;/code&gt; or &lt;code&gt;b&lt;/code&gt; at all.&lt;/p&gt;

&lt;p&gt;This follows straight from the hand calculation. &lt;code&gt;∂a/∂h = 0&lt;/code&gt;, and every gradient behind that node gets multiplied by zero on its way through. The ReLU is a gate, and this one is shut.&lt;/p&gt;

&lt;p&gt;That is the dying-ReLU problem, and it is worth meeting it here rather than three months later in a model with fifty layers. When a neuron's input is negative for every example in your data, its gradient is permanently zero and it never learns again. &lt;code&gt;LeakyReLU&lt;/code&gt; exists because it lets a small gradient through instead.&lt;/p&gt;

&lt;p&gt;More generally: when a model stops learning and you cannot see why, the question to ask is what is multiplying the gradient by zero on the way back.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three mechanics that catch nearly everyone
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Only leaves keep their &lt;code&gt;.grad&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Intermediate results do not store their gradient. It is computed, used to keep the chain going, and thrown away, because keeping every intermediate gradient in a real model would be enormous. If you want one, ask before calling backward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retain_grad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;# keep y's gradient too
&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;z&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# tensor(60.)   dz/dx = 15x² = 60
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# tensor(5.)    dz/dy = 5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;retain_grad()&lt;/code&gt;, &lt;code&gt;y.grad&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt; and you get a warning rather than an error, which is why people spend twenty minutes confused by it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Gradients accumulate, they do not overwrite
&lt;/h3&gt;

&lt;p&gt;This one causes more silently broken training loops than anything else 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="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;y&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;step &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  grad = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;step 0  grad = 6.0
step 1  grad = 12.0
step 2  grad = 18.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gradient of &lt;code&gt;x²&lt;/code&gt; at &lt;code&gt;x = 3&lt;/code&gt; is 6, every time. It reads 12 and then 18 because &lt;code&gt;.grad&lt;/code&gt; is added to, not replaced.&lt;/p&gt;

&lt;p&gt;This is deliberate. It is what lets you accumulate gradients over several mini-batches and take one larger step, which is how people train with an effective batch size their GPU cannot hold.&lt;br&gt;
But it means that unless you clear it, every step of your training loop is stepping on a sum of all previous gradients, and your loss curve will do something strange that is very hard to diagnose from the outside.&lt;/p&gt;

&lt;p&gt;The fix is &lt;code&gt;x.grad.zero_()&lt;/code&gt;, or in a real loop, &lt;code&gt;optimizer.zero_grad()&lt;/code&gt; before every &lt;code&gt;loss.backward()&lt;/code&gt;. That line is not boilerplate. It is load-bearing.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. &lt;code&gt;no_grad&lt;/code&gt; and &lt;code&gt;detach&lt;/code&gt; — switching the recorder off
&lt;/h3&gt;

&lt;p&gt;During evaluation you do not need gradients, and building the graph costs both time and memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requires_grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the block no graph is built. This is why every evaluation loop you have ever copied is wrapped in &lt;code&gt;torch.no_grad()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.detach()&lt;/code&gt; is the narrower tool. It gives you a tensor that shares the same data but has no connection to the graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;y_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# tensor(9.)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requires_grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it when you want a value without dragging its history along — logging, or using a model's output as a target that should not be differentiated through. It is the mechanism behind stop-gradient tricks and target networks, and it is also, occasionally, the reason your gradient is unexpectedly zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  One call, every parameter
&lt;/h2&gt;

&lt;p&gt;Everything so far had one or two parameters so the arithmetic stayed visible. Nothing changes at scale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requires_grad&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;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;            &lt;span class="c1"&gt;# 2·3 + 1 = 7
&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;      &lt;span class="c1"&gt;# (7 - 5)² = 4
&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# tensor(12.)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# tensor(4.)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;backward()&lt;/code&gt; filled in both. In a model with eleven million parameters, the same single call fills in all eleven million, because the graph reaches every one of them and the chain rule multiplies along every path. Nothing about the mechanism is different. There are just more nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The summary worth keeping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;requires_grad=True&lt;/code&gt; marks a tensor as something you want the gradient of. Model parameters
get it automatically; your data does not need it.&lt;/li&gt;
&lt;li&gt;The forward pass computes the answer and records a graph as a side effect. Each node stores what it needs to reverse itself later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.backward()&lt;/code&gt; walks that graph in reverse, multiplying the incoming gradient by each node's
local derivative. That is the chain rule, and it is all backpropagation is.&lt;/li&gt;
&lt;li&gt;Gradients land in &lt;code&gt;.grad&lt;/code&gt; on leaf tensors only. Use &lt;code&gt;retain_grad()&lt;/code&gt; for intermediates.&lt;/li&gt;
&lt;li&gt;Gradients accumulate. &lt;code&gt;zero_grad()&lt;/code&gt; before every backward, always.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;torch.no_grad()&lt;/code&gt; for evaluation, &lt;code&gt;.detach()&lt;/code&gt; for a single value without its history.&lt;/li&gt;
&lt;li&gt;A zero gradient means something on the path back multiplied by zero. A closed ReLU is the
usual suspect.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try this before you close the tab
&lt;/h2&gt;

&lt;p&gt;Take &lt;code&gt;x = 4.0&lt;/code&gt; with &lt;code&gt;requires_grad=True&lt;/code&gt;, compute &lt;code&gt;y = x**3 - 2*x&lt;/code&gt;, call &lt;code&gt;backward()&lt;/code&gt;, and print&lt;br&gt;
&lt;code&gt;x.grad&lt;/code&gt;. Then work out &lt;code&gt;3x² - 2&lt;/code&gt; at &lt;code&gt;x = 4&lt;/code&gt; on paper and check whether they agree. It takes&lt;br&gt;
about ninety seconds and it is worth more than rereading this article.&lt;/p&gt;

&lt;p&gt;Then the harder version: put a second weight after the ReLU in the tiny network above, &lt;code&gt;w2 =&lt;br&gt;
0.5&lt;/code&gt;, and write out the full chain rule for &lt;code&gt;∂L/∂w&lt;/code&gt; before you run it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is one chapter's worth of an idea from my book,&lt;/em&gt; &lt;strong&gt;PyTorch From Ground Up&lt;/strong&gt;, &lt;em&gt;which builds&lt;br&gt;
everything from tensors upward so nothing stays vague. If it helped: &lt;a href="https://github.com/pytorch-from-ground-up/book_code/blob/main/PyTorch-From-Ground-Up-8-Free-Chapters.pdf" rel="noopener noreferrer"&gt;8 chapters are free, no email required&lt;/a&gt;,&lt;br&gt;
there's a &lt;a href="https://payhip.com/b/7ukxh" rel="noopener noreferrer"&gt;free one-page tensor cheat-sheet here&lt;/a&gt;, every example runs&lt;br&gt;
in the &lt;a href="https://github.com/pytorch-from-ground-up/book_code" rel="noopener noreferrer"&gt;companion notebooks on GitHub&lt;/a&gt;, and&lt;br&gt;
the full book is on &lt;a href="https://leanpub.com/pytorchfromgroundup" rel="noopener noreferrer"&gt;Leanpub&lt;/a&gt; or in&lt;br&gt;
&lt;a href="https://www.amazon.com/dp/B0H8WMCV33" rel="noopener noreferrer"&gt;paperback and Kindle on Amazon&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  More in this series
&lt;/h3&gt;

&lt;p&gt;Shape mechanics, the part that has to be solid before any of this makes sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o"&gt;Reshape vs View in PyTorch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606"&gt;PyTorch Broadcasting Explained&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your CNN's Advantage Is One Assumption — and I Measured What Happens When It Breaks</title>
      <dc:creator>Wesam Khallaf — Author of PyTorch From Ground Up</dc:creator>
      <pubDate>Sat, 08 Aug 2026 12:19:03 +0000</pubDate>
      <link>https://dev.to/pytorchfromgroundup/your-cnns-advantage-is-one-assumption-and-i-measured-what-happens-when-it-breaks-490d</link>
      <guid>https://dev.to/pytorchfromgroundup/your-cnns-advantage-is-one-assumption-and-i-measured-what-happens-when-it-breaks-490d</guid>
      <description>&lt;p&gt;A small convolutional network beats a plain flatten-and-feed-it-forward network by &lt;strong&gt;7.0 points&lt;/strong&gt; on CIFAR-10. That's convolutions, pooling, normalisation and skip connections doing honest work.&lt;/p&gt;

&lt;p&gt;Then I shuffled the rows of every image, destroying no information at all, and that 7.0-point margin fell to &lt;strong&gt;0.3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Same architecture. Same data, in a strict sense I'll defend in a moment. Almost the entire advantage, gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;Take one fixed permutation of the 32 row indices. Apply it to every image in the training set and every image in the test set — the &lt;em&gt;same&lt;/em&gt; permutation, every time.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;g&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="nc"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;manual_seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;row_perm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randperm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shuffle_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;        &lt;span class="c1"&gt;# x: (C, H, W)
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;row_perm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_perm&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# [15, 9, 8, 1, 4, 12, 30, 7]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole intervention. Then train two models twice each — once on natural images, once on shuffled ones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Params&lt;/th&gt;
&lt;th&gt;Natural rows&lt;/th&gt;
&lt;th&gt;Shuffled rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flatten → 512 → 10 (MLP)&lt;/td&gt;
&lt;td&gt;1,578,506&lt;/td&gt;
&lt;td&gt;51.4%&lt;/td&gt;
&lt;td&gt;51.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small CNN&lt;/td&gt;
&lt;td&gt;94,538&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;58.4%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;52.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CNN's margin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+7.0 pts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.3 pts&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The baseline is a real fully-connected network, not a single linear layer — &lt;code&gt;Flatten → Linear(3072, 512) → ReLU → Linear(512, 10)&lt;/code&gt;. It has the capacity to learn anything the CNN can; what it lacks is any reason to look at pixels near each other.&lt;/p&gt;

&lt;p&gt;Two things in that table are worth sitting with. The CNN wins the natural case with &lt;strong&gt;sixteen times fewer parameters&lt;/strong&gt; — that's the prior paying for itself. And in the shuffled case it doesn't just lose its lead; it drops 6.4 points in absolute terms, down to roughly where the linear model already was.&lt;/p&gt;

&lt;h2&gt;
  
  
  "You destroyed the data" — no, and this is the important part
&lt;/h2&gt;

&lt;p&gt;This is the objection everyone raises, so let's take it seriously, because the experiment is worthless if the objection holds.&lt;/p&gt;

&lt;p&gt;A fixed permutation is a &lt;strong&gt;bijection&lt;/strong&gt;. Nothing is added, nothing is removed, nothing is averaged or blurred:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sh&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shuffle_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                     &lt;span class="c1"&gt;# torch.Size([3, 32, 32])
&lt;/span&gt;
&lt;span class="c1"&gt;# every value still present, exactly once
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;# True
&lt;/span&gt;
&lt;span class="c1"&gt;# and it's perfectly reversible
&lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_perm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row_perm&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:],&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;              &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every pixel is still there, with its original value. The transformation is invertible, so no information has been lost in any sense that information theory would recognise. What changed is &lt;em&gt;which pixels sit next to which&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And here's the control experiment that settles it: &lt;strong&gt;the fully-connected model scores the same either way&lt;/strong&gt; — 51.4% natural, 51.7% shuffled, a difference well inside run-to-run noise. If the shuffle had damaged the data, the linear model would have suffered too. It didn't, because it never used the layout in the first place — after &lt;code&gt;flatten()&lt;/code&gt;, position 400 is just position 400, and a fixed permutation of the input columns is something the first layer absorbs by permuting its own weights. The network is free to relearn the identical function; only the column labels moved.&lt;/p&gt;

&lt;p&gt;So the shuffle removes exactly one thing: the &lt;em&gt;usefulness&lt;/em&gt; of the assumption that neighbouring pixels are related. And that assumption turns out to be worth 6.7 of the CNN's 7.0 points.&lt;/p&gt;

&lt;p&gt;Which is the uncomfortable version of the finding: the convolutions, the pooling, the normalisation — all of it — were converting one true fact about photographs into 7 points of accuracy. Take the fact away and the machinery has nothing left to convert.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a convolution actually assumes
&lt;/h2&gt;

&lt;p&gt;Written out, a conv layer makes three claims about your data, none of which are claims about tensors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Locality.&lt;/strong&gt; A 3×3 kernel only ever sees a 3×3 neighbourhood. This is a bet that meaningful patterns are &lt;em&gt;local&lt;/em&gt; — that to recognise an edge you need nearby pixels and not distant ones. True of photographs. Not true of a shuffled photograph, where the pixels that formed an edge are now scattered across the image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weight sharing.&lt;/strong&gt; The same kernel slides over every position, so a feature detected in the top-left uses identical weights to the same feature bottom-right. This is a bet that &lt;em&gt;what&lt;/em&gt; a pattern is doesn't depend on &lt;em&gt;where&lt;/em&gt; it is. It's also where the parameter savings come from — and it's why the shuffled case doesn't merely lose the advantage but wastes capacity, since the kernel is now sharing weights across positions that have nothing in common.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hierarchy.&lt;/strong&gt; Stacked layers assume small patterns compose into larger ones: edges into corners, corners into shapes. A shuffled image has edges nowhere, so there is nothing to compose.&lt;/p&gt;

&lt;p&gt;All three are statements about the world, not the maths. When they hold, they're enormously valuable — a prior that good is worth more than a lot of data. When they don't hold, depth doesn't manufacture them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Roughly this, on CPU, in a few minutes:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torchvision&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;torchvision&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transforms&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;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Generator&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;manual_seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;row_perm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randperm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToTensor&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;shuffled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;row_perm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:])]&lt;/span&gt;

&lt;span class="n"&gt;train_nat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torchvision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CIFAR10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train&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;span class="n"&gt;download&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;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;train_shuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torchvision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CIFAR10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train&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;span class="n"&gt;download&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;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shuffled&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# ...and the same two for train=False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then train any small CNN and any &lt;code&gt;Flatten → Linear → ReLU → Linear&lt;/code&gt; on each of the four loaders. Four runs. Watch the CNN's lead evaporate while the fully-connected model shrugs.&lt;/p&gt;

&lt;p&gt;Two things to keep honest while you do it: build the permutation &lt;strong&gt;once&lt;/strong&gt;, outside the transform, or you'll get a different shuffle per image, which really does destroy information and proves nothing. And use the same seed and epoch count across all four runs, or you're measuring your own variance.&lt;/p&gt;

&lt;p&gt;My run, for the record: torch 2.13.0 on CPU, &lt;code&gt;manual_seed(0)&lt;/code&gt;, Adam at 1e-3, batch 128 train / 256 eval, 5 epochs, normalised to mean 0.5 / std 0.5, row permutation seeded 1234.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually matters
&lt;/h2&gt;

&lt;p&gt;This isn't a party trick. It's the question to ask before choosing any architecture: &lt;strong&gt;what is this thing assuming about my data, and is the assumption true?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tabular data.&lt;/strong&gt; Columns have no spatial relationship — &lt;code&gt;age&lt;/code&gt; next to &lt;code&gt;postcode&lt;/code&gt; next to &lt;code&gt;income&lt;/code&gt; is an arbitrary ordering you could permute at will. A CNN over columns is the shuffled experiment, permanently. This is a large part of why gradient-boosted trees still win on tabular problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spectrograms and time series.&lt;/strong&gt; These &lt;em&gt;do&lt;/em&gt; have local structure along at least one axis, so a 1-D convolution is a real prior rather than a fashion choice. Neighbouring time steps are genuinely related.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel order.&lt;/strong&gt; Permuting the &lt;em&gt;channel&lt;/em&gt; axis of an image is not the same operation and is nearly harmless, because a conv kernel spans all channels at once — there's no locality assumption along that axis to break. Worth knowing, because it's the first thing people try to use as a counter-example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attention.&lt;/strong&gt; Self-attention makes no locality assumption at all, which is exactly why transformers need positional encoding bolted on and why they need more data to reach the same place on images. Fewer assumptions is not automatically better; it means less free knowledge and more required evidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;One dataset, one small architecture, one seed, at 32×32, five epochs — nobody's state of the art. The &lt;em&gt;direction&lt;/em&gt; of this result is robust and it's the standard inductive-bias argument that the literature has made for years. The specific numbers are mine and yours will differ — the point of running it isn't the decimal places, it's watching a 7-point advantage turn into a rounding error while the data stays intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Architecture isn't magic and it isn't a leaderboard. It's a bet about the structure of your data, made before training starts. A convolution bets that nearby things belong together. On photographs that bet pays enormously. Break its truth without touching a single pixel value and the same architecture is worth about a third of a point.&lt;/p&gt;

&lt;p&gt;So when someone asks why your model isn't learning, the useful question often isn't "should I go deeper" — it's whether the architecture's assumptions were ever true of your data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This experiment is from Chapter 8 of **PyTorch From Ground Up, Volume 2&lt;/em&gt;&lt;em&gt;, which I'm writing now. The whole book works this way: no solution gets introduced until you've watched the failure it fixes — no convolution before a linear layer fails, no skip connection before a 56-layer network loses to a 20-layer one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it yourself:&lt;/strong&gt; &lt;a href="https://colab.research.google.com/github/pytorch-from-ground-up/book_code/blob/main/notebooks/volume-2-architectures/08-sequences-time-order.ipynb" rel="noopener noreferrer"&gt;open the Chapter 8 notebook in Colab&lt;/a&gt; — every code block from the chapter, in order, CPU-only, nothing to install. The four-model comparison is the slow cell, about 15 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume 2 isn't finished yet.&lt;/strong&gt; Volume 1 — &lt;em&gt;Foundations&lt;/em&gt;, the tensors-and-training-loop book underneath all of this — is out, and &lt;a href="https://github.com/pytorch-from-ground-up/book_code/blob/main/PyTorch-From-Ground-Up-8-Free-Chapters.pdf" rel="noopener noreferrer"&gt;8 of its chapters are free with no email required&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's an architecture choice that quietly bought you nothing?&lt;/strong&gt; I collect these — the ones that surprise me end up as experiments.&lt;/p&gt;

</description>
      <category>computervision</category>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>PyTorch `permute` vs `transpose`: What's the Difference (and the `reshape` Bug That Scrambles Your Images)</title>
      <dc:creator>Wesam Khallaf — Author of PyTorch From Ground Up</dc:creator>
      <pubDate>Sun, 02 Aug 2026 18:23:12 +0000</pubDate>
      <link>https://dev.to/pytorchfromgroundup/pytorch-permute-vs-transpose-whats-the-difference-and-the-reshape-bug-that-scrambles-your-7ke</link>
      <guid>https://dev.to/pytorchfromgroundup/pytorch-permute-vs-transpose-whats-the-difference-and-the-reshape-bug-that-scrambles-your-7ke</guid>
      <description>&lt;p&gt;You loaded an image, got a tensor shaped &lt;code&gt;(batch, height, width, channels)&lt;/code&gt;, and your convolution wants &lt;code&gt;(batch, channels, height, width)&lt;/code&gt;. Stack Overflow says &lt;code&gt;permute&lt;/code&gt;. Someone else says &lt;code&gt;transpose&lt;/code&gt;. And &lt;code&gt;reshape(2, 3, 28, 28)&lt;/code&gt; gives you the right shape too — so why is everyone making this complicated?&lt;/p&gt;

&lt;p&gt;Because two of those three are the same tool, and the third one silently destroys your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;transpose(dim0, dim1)&lt;/code&gt; swaps &lt;strong&gt;exactly two&lt;/strong&gt; dimensions. &lt;code&gt;permute(...)&lt;/code&gt; reorders &lt;strong&gt;all&lt;/strong&gt; of them in one call, and you must list every dimension. &lt;code&gt;transpose&lt;/code&gt; is a special case of &lt;code&gt;permute&lt;/code&gt;. Both return a &lt;strong&gt;view&lt;/strong&gt; — no data is copied, only the strides change — which also means both leave you with a non-contiguous tensor.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;reshape&lt;/code&gt; is not in this family at all. It reinterprets the flat memory under a new shape without moving anything, so it can produce the shape you asked for while completely scrambling what the numbers mean.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# torch.Size([3, 2, 4])  — swapped dims 0 and 1
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# torch.Size([4, 2, 3])  — full reorder
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;transpose&lt;/code&gt; — swap two axes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;transpose(dim0, dim1)&lt;/code&gt; takes two dimension indices and swaps them. Everything else stays put.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# torch.Size([2, 3, 4])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# torch.Size([3, 2, 4])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# torch.Size([2, 4, 3])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order of the two arguments doesn't matter — &lt;code&gt;t.transpose(0, 1)&lt;/code&gt; and &lt;code&gt;t.transpose(1, 0)&lt;/code&gt; are the same thing. A swap is a swap.&lt;/p&gt;

&lt;p&gt;On a 2-D tensor this is the matrix transpose you already know, and &lt;code&gt;.T&lt;/code&gt; is the shorthand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;# torch.Size([3, 2])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# torch.Size([3, 2])  — identical
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One caution on &lt;code&gt;.T&lt;/code&gt;:&lt;/strong&gt; on tensors with more than two dimensions, &lt;code&gt;.T&lt;/code&gt; reverses &lt;em&gt;every&lt;/em&gt; dimension, and modern PyTorch has deprecated that behaviour — it warns now and is slated to become an error. If you want the "transpose the last two axes" behaviour on a batch of matrices, use &lt;strong&gt;&lt;code&gt;.mT&lt;/code&gt;&lt;/strong&gt;, which is explicit and safe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# torch.Size([2, 4, 3]) — swaps the last two dims, batch untouched
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reserve plain &lt;code&gt;.T&lt;/code&gt; for 2-D matrices. On anything higher, say what you mean with &lt;code&gt;permute&lt;/code&gt; or &lt;code&gt;.mT&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;permute&lt;/code&gt; — state the whole new order
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;permute&lt;/code&gt; doesn't swap; it &lt;em&gt;rewrites the dimension order in full&lt;/em&gt;. You pass the old index of each dimension in the position you want it to end up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# torch.Size([4, 2, 3])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read &lt;code&gt;permute(2, 0, 1)&lt;/code&gt; as: &lt;strong&gt;"new dim 0 is old dim 2, new dim 1 is old dim 0, new dim 2 is old dim 1."&lt;/strong&gt; Not "move dim 2 somewhere" — you are writing out the destination order, left to right.&lt;/p&gt;

&lt;p&gt;Two consequences worth internalising:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You must list &lt;strong&gt;every&lt;/strong&gt; dimension. Miss one and you get &lt;code&gt;RuntimeError: number of dims don't match in permute&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Any reorder you can do with a chain of transposes, you can do in one &lt;code&gt;permute&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;via_permute&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;# torch.Size([3, 4, 2])
&lt;/span&gt;&lt;span class="n"&gt;via_transposes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;transpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# torch.Size([3, 4, 2])
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;via_permute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;via_transposes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same result. The &lt;code&gt;permute&lt;/code&gt; version says the destination shape out loud; the chained version makes the reader simulate two swaps in their head. Prefer &lt;code&gt;permute&lt;/code&gt; for anything beyond a single swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that actually bites: &lt;code&gt;permute&lt;/code&gt; is not &lt;code&gt;reshape&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here is the bug this article exists for. You have a batch of images in &lt;code&gt;(N, H, W, C)&lt;/code&gt; — the layout you get from OpenCV, PIL, TensorFlow, and most image files — and PyTorch convolutions need &lt;code&gt;(N, C, H, W)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both of these produce a tensor of the right shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&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="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# N=1, H=2, W=2, C=3
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# torch.Size([1, 3, 2, 2])  ✅
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# torch.Size([1, 3, 2, 2])  ⚠️ same shape!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same shape. No error. No warning. Now look at what's actually inside the first channel:&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# tensor([[0, 3],
#         [6, 9]])            ← the red value of each of the 4 pixels ✅
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# tensor([[0, 1],
#         [2, 3]])            ← pixel 0's R, G, B and then pixel 1's R ❌
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;permute&lt;/code&gt; version collected the red channel: pixels are stored as &lt;code&gt;(R,G,B)(R,G,B)…&lt;/code&gt;, so the reds are elements 0, 3, 6, 9. That's a real red channel.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;reshape&lt;/code&gt; version just took the first four numbers in memory and called them "channel 0" — that's one whole pixel plus a third of the next one. Your "red channel" is now a mixture of red, green and blue from different pixels. The tensor is the right shape, the model trains, the loss goes down a little, and the accuracy is quietly terrible. Nothing on screen ever tells you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; &lt;code&gt;permute&lt;/code&gt; moves &lt;em&gt;dimensions&lt;/em&gt;. &lt;code&gt;reshape&lt;/code&gt; re-cuts the &lt;em&gt;same flat sequence of numbers&lt;/em&gt; into new brackets. If you want to change what an axis &lt;strong&gt;means&lt;/strong&gt;, you need &lt;code&gt;permute&lt;/code&gt; — always.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want the full picture on how &lt;code&gt;reshape&lt;/code&gt; re-cuts memory, that's covered in &lt;a href="https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o"&gt;Reshape vs View in PyTorch&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real code: the two reorders you'll actually write
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Images — channels-last to channels-first:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# N, H, W, C  (from PIL / OpenCV)
&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# N, C, H, W  (what nn.Conv2d wants)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                           &lt;span class="c1"&gt;# torch.Size([2, 3, 28, 28])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Attention — splitting into heads.&lt;/strong&gt; This is the one place where &lt;code&gt;reshape&lt;/code&gt; and &lt;code&gt;permute&lt;/code&gt; correctly appear back-to-back, and seeing why makes the distinction click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;                  &lt;span class="c1"&gt;# batch, seq_len, embed_dim, heads
&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&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;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# torch.Size([2, 5, 8])
&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&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;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;H&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;H&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# torch.Size([2, 5, 2, 4])  — split, don't move
&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;# torch.Size([2, 2, 5, 4])  — move heads forward
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;reshape&lt;/code&gt; is legitimate here because it only &lt;em&gt;splits&lt;/em&gt; the last axis — the 8 embedding values were already laid out contiguously, so cutting them into 2 groups of 4 doesn't reorder anything. The &lt;code&gt;permute&lt;/code&gt; then genuinely moves the head axis in front of the sequence axis. Splitting an axis is &lt;code&gt;reshape&lt;/code&gt; work; moving an axis is &lt;code&gt;permute&lt;/code&gt; work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both of them break contiguity
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;transpose&lt;/code&gt; and &lt;code&gt;permute&lt;/code&gt; never touch the underlying data. They change the tensor's &lt;strong&gt;strides&lt;/strong&gt; — the step size PyTorch takes through memory for each dimension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stride&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_contiguous&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;            &lt;span class="c1"&gt;# (3, 1) True
&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stride&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_contiguous&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# torch.Size([3, 2]) (1, 3) False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's why they're free. It's also why the very next &lt;code&gt;.view()&lt;/code&gt; you call will blow up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# RuntimeError: view size is not compatible with input tensor's size and stride
# (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two fixes, and the choice matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;# works — copies quietly when it has to
&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contiguous&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# works — reorders memory first, then views
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;.reshape()&lt;/code&gt; by default. Reach for an explicit &lt;code&gt;.contiguous()&lt;/code&gt; when you're about to do many operations on the permuted tensor and want the memory laid out well once, up front, rather than copied repeatedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;reshape&lt;/code&gt; to convert NHWC → NCHW.&lt;/strong&gt; The headline bug. Right shape, scrambled channels, no error. Use &lt;code&gt;permute(0, 3, 1, 2)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading &lt;code&gt;permute(2, 0, 1)&lt;/code&gt; as instructions to move things.&lt;/strong&gt; It's a destination list: new dim &lt;em&gt;i&lt;/em&gt; is old dim &lt;code&gt;args[i]&lt;/code&gt;. Say it out loud before you trust it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting a dimension.&lt;/strong&gt; &lt;code&gt;permute&lt;/code&gt; needs all of them → &lt;code&gt;RuntimeError: number of dims don't match in permute&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;.T&lt;/code&gt; on 3-D+ tensors.&lt;/strong&gt; Deprecated (it reverses &lt;em&gt;all&lt;/em&gt; dims, which is almost never what you want). Use &lt;code&gt;.mT&lt;/code&gt; for a batch of matrices, or &lt;code&gt;permute&lt;/code&gt; to say it exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calling &lt;code&gt;.view()&lt;/code&gt; right after a permute.&lt;/strong&gt; Guaranteed contiguity error. &lt;code&gt;.reshape()&lt;/code&gt; or &lt;code&gt;.contiguous().view()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming a transpose changed your data.&lt;/strong&gt; It changed the strides only. &lt;code&gt;print(x.stride())&lt;/code&gt; next to &lt;code&gt;print(x.shape)&lt;/code&gt; when a shape bug won't make sense — the strides usually tell you the real story.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;transpose(a, b)&lt;/code&gt; swaps two dimensions; &lt;code&gt;permute(...)&lt;/code&gt; rewrites the whole dimension order and needs every index listed. They do the same kind of work, and &lt;code&gt;permute&lt;/code&gt; is the one to reach for whenever more than one swap is involved. Both are free view operations that leave the tensor non-contiguous, so the next &lt;code&gt;.view()&lt;/code&gt; will error — use &lt;code&gt;.reshape()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And the part that costs people a weekend: &lt;strong&gt;&lt;code&gt;reshape&lt;/code&gt; is not a reordering tool.&lt;/strong&gt; It can hand you a correctly-shaped tensor full of scrambled values, with nothing on screen to warn you. When an axis needs to &lt;em&gt;mean&lt;/em&gt; something different, permute it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;More in this series:&lt;/strong&gt; &lt;a href="https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o"&gt;Reshape vs View&lt;/a&gt; · &lt;a href="https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606"&gt;Broadcasting Explained&lt;/a&gt; · &lt;a href="https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a"&gt;What &lt;code&gt;unsqueeze&lt;/code&gt; Does&lt;/a&gt; · &lt;a href="https://dev.to/pytorchfromgroundup/what-does-keepdim-do-in-pytorch-and-the-silent-bug-when-you-forget-it-17p1"&gt;What &lt;code&gt;keepdim&lt;/code&gt; Does&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A question for you:&lt;/strong&gt; what's the shape bug that cost you the most time? I'm collecting the ones that hit hardest — drop it in the comments and I'll write up the fix.&lt;/p&gt;




&lt;p&gt;This is one idea from my book &lt;em&gt;PyTorch From Ground Up&lt;/em&gt;, which builds everything from tensors upward so nothing stays vague. If it helped, you can grab the &lt;a href="https://payhip.com/b/7ukxh" rel="noopener noreferrer"&gt;free PyTorch tensor cheat-sheet here&lt;/a&gt;, run every example from the book in the &lt;a href="https://github.com/pytorch-from-ground-up/book_code" rel="noopener noreferrer"&gt;companion notebooks on GitHub&lt;/a&gt;, get the &lt;a href="https://leanpub.com/pytorchfromgroundup" rel="noopener noreferrer"&gt;digital edition on Leanpub&lt;/a&gt;, or find the &lt;a href="https://www.amazon.com/dp/B0H8WMCV33" rel="noopener noreferrer"&gt;full paperback on Amazon here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>deeplearning</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Does `keepdim` Do in PyTorch? (And the Silent Bug When You Forget It)</title>
      <dc:creator>Wesam Khallaf — Author of PyTorch From Ground Up</dc:creator>
      <pubDate>Fri, 24 Jul 2026 08:26:45 +0000</pubDate>
      <link>https://dev.to/pytorchfromgroundup/what-does-keepdim-do-in-pytorch-and-the-silent-bug-when-you-forget-it-17p1</link>
      <guid>https://dev.to/pytorchfromgroundup/what-does-keepdim-do-in-pytorch-and-the-silent-bug-when-you-forget-it-17p1</guid>
      <description>&lt;p&gt;You wrote &lt;code&gt;x.sum(dim=1)&lt;/code&gt; to get a row total, divided the original tensor by it, and either got &lt;code&gt;RuntimeError: The size of tensor a (3) must match the size of tensor b (2)&lt;/code&gt; — or, worse, got no error at all and numbers that were quietly wrong. Someone told you to add &lt;code&gt;keepdim=True&lt;/code&gt; and it fixed itself. But nobody said &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Every reduction — &lt;code&gt;sum&lt;/code&gt;, &lt;code&gt;mean&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt; — collapses a dimension, and by default that dimension &lt;strong&gt;disappears&lt;/strong&gt; from the shape. &lt;code&gt;keepdim=True&lt;/code&gt; tells PyTorch to keep it as a size-1 axis instead. A &lt;code&gt;(2, 3)&lt;/code&gt; tensor reduced over &lt;code&gt;dim=1&lt;/code&gt; becomes &lt;code&gt;(2,)&lt;/code&gt; normally, but &lt;code&gt;(2, 1)&lt;/code&gt; with &lt;code&gt;keepdim=True&lt;/code&gt;. Same numbers either way — the only difference is a single &lt;code&gt;1&lt;/code&gt; in the shape. That &lt;code&gt;1&lt;/code&gt; is what lets the result &lt;strong&gt;broadcast&lt;/strong&gt; back against the original tensor.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;4.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# torch.Size([2])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&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;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# torch.Size([2, 1])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole feature. The interesting part is &lt;em&gt;why the missing &lt;code&gt;1&lt;/code&gt; breaks things&lt;/em&gt; — sometimes loudly, sometimes silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: what does &lt;code&gt;dim&lt;/code&gt; do in a reduction?
&lt;/h2&gt;

&lt;p&gt;Before &lt;code&gt;keepdim&lt;/code&gt; makes sense, you need the mental model for &lt;code&gt;dim&lt;/code&gt;. &lt;strong&gt;The dimension you name is the one that disappears.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;4.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;   &lt;span class="c1"&gt;# shape (2, 3)
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;          &lt;span class="c1"&gt;# tensor(21.)          — no dim: collapse everything to a scalar
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;# tensor([5., 7., 9.]) — collapse the rows, shape (3,)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;# tensor([ 6., 15.])   — collapse the columns, shape (2,)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dim=0&lt;/code&gt; adds &lt;em&gt;down&lt;/em&gt; each column (the size-2 axis vanishes, leaving &lt;code&gt;(3,)&lt;/code&gt;). &lt;code&gt;dim=1&lt;/code&gt; adds &lt;em&gt;across&lt;/em&gt; each row (the size-3 axis vanishes, leaving &lt;code&gt;(2,)&lt;/code&gt;). If you can predict the output shape before running, you understand reductions. The dim you name is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what does &lt;code&gt;keepdim&lt;/code&gt; actually do?
&lt;/h2&gt;

&lt;p&gt;By default the collapsed axis is deleted. &lt;code&gt;keepdim=True&lt;/code&gt; leaves it behind as a length-1 placeholder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;row_sum&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# shape (2,)
&lt;/span&gt;&lt;span class="n"&gt;row_sum_k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&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;span class="c1"&gt;# shape (2, 1)
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_sum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# tensor([ 6., 15.])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_sum_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# tensor([[ 6.],
#         [15.]])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical values. The &lt;code&gt;keepdim&lt;/code&gt; version is just wrapped so each row sum sits in its own row. That shape — &lt;code&gt;(2, 1)&lt;/code&gt; instead of &lt;code&gt;(2,)&lt;/code&gt; — is the entire point, because of what happens next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why size-1 matters: broadcasting back against the original
&lt;/h2&gt;

&lt;p&gt;The most common reason to reduce a tensor is to then &lt;em&gt;use that result on the original&lt;/em&gt; — normalize each row to sum to 1, subtract each column's mean, divide by a per-row max. Every one of those is &lt;code&gt;original (op) reduction&lt;/code&gt;, and for it to line up, the reduction has to broadcast.&lt;/p&gt;

&lt;p&gt;PyTorch broadcasting aligns shapes from the &lt;strong&gt;right&lt;/strong&gt; and treats a &lt;code&gt;1&lt;/code&gt; as "stretch me to fit." A &lt;code&gt;(2, 1)&lt;/code&gt; sum broadcasts cleanly against a &lt;code&gt;(2, 3)&lt;/code&gt; tensor: the &lt;code&gt;1&lt;/code&gt; stretches across the 3 columns. A bare &lt;code&gt;(2,)&lt;/code&gt; sum does not line up at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Correct: keepdim=True → (2, 1) broadcasts against (2, 3)
&lt;/span&gt;&lt;span class="n"&gt;normed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# tensor([[0.1667, 0.3333, 0.5000],
#         [0.2667, 0.3333, 0.4000]])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# tensor([1., 1.]) — every row sums to 1 ✅
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop the &lt;code&gt;keepdim&lt;/code&gt; and this exact line either crashes or lies to you. Which one you get depends on the shape — and that's the part almost every tutorial skips.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: the same bug fails two completely different ways
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Case 1 — a non-square tensor: it errors (the lucky case)
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;m&lt;/code&gt; is &lt;code&gt;(2, 3)&lt;/code&gt;. Without &lt;code&gt;keepdim&lt;/code&gt;, the row sum is &lt;code&gt;(2,)&lt;/code&gt;, and PyTorch tries to align &lt;code&gt;(2, 3)&lt;/code&gt; against &lt;code&gt;(2,)&lt;/code&gt; from the right: 3 vs 2. Mismatch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;4.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;          &lt;span class="c1"&gt;# (2, 3)
&lt;/span&gt;
&lt;span class="n"&gt;normed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# (2, 3) / (2,)
# RuntimeError: The size of tensor a (3) must match the size of
# tensor b (2) at non-singleton dimension 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;em&gt;good&lt;/em&gt; outcome. PyTorch stops you immediately, and the error text even names the two sizes that don't match. (If that message looks familiar, it's the same shape-mismatch error covered in &lt;a href="https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606"&gt;PyTorch Broadcasting Explained&lt;/a&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 2 — a square tensor: it runs, and it's silently wrong (the dangerous case)
&lt;/h3&gt;

&lt;p&gt;Now make the tensor square. The row-sum vector is length 3, the tensor has 3 columns, so the shapes &lt;em&gt;happen&lt;/em&gt; to broadcast — just not the way you meant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;4.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;8.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;9.&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;          &lt;span class="c1"&gt;# (3, 3)
&lt;/span&gt;
&lt;span class="n"&gt;wrong&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;# (3, 3) / (3,)  — no error!
&lt;/span&gt;&lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&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;span class="c1"&gt;# (3, 3) / (3, 1)
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrong&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="c1"&gt;# tensor([0.4250, 1.2500, 2.0750])  ← NOT 1s ❌
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;correct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# tensor([1., 1., 1.])              ← correct ✅
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exception. No warning. The row sums aren't 1 because the &lt;code&gt;(3,)&lt;/code&gt; vector aligned to the &lt;strong&gt;columns&lt;/strong&gt; instead of the rows — PyTorch divided the wrong way. Your data looks normalized, your code ran clean, and the mistake surfaces as a mysteriously bad model three hours later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is why you add &lt;code&gt;keepdim=True&lt;/code&gt; by reflex, not by debugging.&lt;/strong&gt; On a square batch it's the difference between correct and quietly-corrupted, with nothing on screen to tell you which one you got.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rule you can actually remember
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Whenever you reduce a tensor and then use the result in arithmetic with the original, pass &lt;code&gt;keepdim=True&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. If the reduction is the final answer (you just want the row totals and you're done), you don't need it. The moment the reduction feeds back into the original tensor, you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does &lt;code&gt;keepdim&lt;/code&gt; work on &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, and &lt;code&gt;argmax&lt;/code&gt; too?
&lt;/h2&gt;

&lt;p&gt;Yes — &lt;code&gt;max(dim)&lt;/code&gt; and &lt;code&gt;min(dim)&lt;/code&gt; return &lt;em&gt;two&lt;/em&gt; tensors (the values and their indices), and &lt;code&gt;keepdim&lt;/code&gt; keeps the axis on both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idxs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idxs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# torch.Size([3, 1]) torch.Size([3, 1])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is handy for a "divide each row by its own max" style normalization, where you need that &lt;code&gt;(3, 1)&lt;/code&gt; shape to broadcast. &lt;code&gt;argmax(dim, keepdim=True)&lt;/code&gt; works the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;keepdim&lt;/code&gt; on a square tensor.&lt;/strong&gt; The headline trap: it broadcasts to the wrong axis with no error. Rows-or-columns bugs on square data are almost always a missing &lt;code&gt;keepdim&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking &lt;code&gt;keepdim&lt;/code&gt; changes the numbers.&lt;/strong&gt; It never does. The values are identical; only the shape gains a &lt;code&gt;1&lt;/code&gt;. If your totals changed, something else is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reaching for &lt;code&gt;unsqueeze&lt;/code&gt; after the fact.&lt;/strong&gt; &lt;code&gt;x.sum(dim=1).unsqueeze(1)&lt;/code&gt; gives the same &lt;code&gt;(N, 1)&lt;/code&gt; shape, but &lt;code&gt;x.sum(dim=1, keepdim=True)&lt;/code&gt; says it in one step and can't get the axis position wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming "it ran" means it was right.&lt;/strong&gt; As Case 2 shows, running successfully is not the same as broadcasting correctly. Print &lt;code&gt;.shape&lt;/code&gt; on the reduction before you divide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;keepdim&lt;/code&gt; when you don't need it.&lt;/strong&gt; If the reduced value is your final output (not fed back into the original tensor), the trailing &lt;code&gt;1&lt;/code&gt; is just clutter — drop it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;keepdim=True&lt;/code&gt; keeps a reduced dimension as a size-1 axis instead of deleting it, so the result still broadcasts against the original tensor. Forget it and you get one of two failures: a shape-mismatch &lt;code&gt;RuntimeError&lt;/code&gt; on non-square tensors (annoying but safe), or a &lt;strong&gt;silent wrong answer&lt;/strong&gt; on square tensors (dangerous). The habit that saves you: any time you reduce and then combine with the original — normalizing rows, centering columns, scaling by a per-row max — pass &lt;code&gt;keepdim=True&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;This is one idea from my book &lt;em&gt;PyTorch From Ground Up&lt;/em&gt;, which builds everything from tensors upward so nothing stays vague. If it helped, you can grab the &lt;a href="https://payhip.com/b/7ukxh" rel="noopener noreferrer"&gt;free PyTorch tensor cheat-sheet here&lt;/a&gt;, run every example from the book in the &lt;a href="https://github.com/pytorch-from-ground-up/book_code" rel="noopener noreferrer"&gt;companion notebooks on GitHub&lt;/a&gt;, get the &lt;a href="https://leanpub.com/pytorchfromgroundup" rel="noopener noreferrer"&gt;digital edition on Leanpub&lt;/a&gt;, or find the &lt;a href="https://www.amazon.com/dp/B0H8WMCV33" rel="noopener noreferrer"&gt;full paperback on Amazon here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Does `unsqueeze` Do in PyTorch? (And Why Your Model Keeps Asking For It)</title>
      <dc:creator>Wesam Khallaf — Author of PyTorch From Ground Up</dc:creator>
      <pubDate>Sun, 19 Jul 2026 11:03:14 +0000</pubDate>
      <link>https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a</link>
      <guid>https://dev.to/pytorchfromgroundup/what-does-unsqueeze-do-in-pytorch-and-why-your-model-keeps-asking-for-it-181a</guid>
      <description>&lt;p&gt;You passed a single image to your model and got &lt;code&gt;ValueError: expected 4D input (got 3D input)&lt;/code&gt;. Someone on Stack Overflow said "just add &lt;code&gt;.unsqueeze(0)&lt;/code&gt;" — and it worked. But nobody explained &lt;em&gt;what&lt;/em&gt; it did, or how you'd know to reach for it next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;unsqueeze(dim)&lt;/code&gt; inserts a new axis of size 1 at position &lt;code&gt;dim&lt;/code&gt;. &lt;strong&gt;The data doesn't change at all — only the shape gains an extra &lt;code&gt;1&lt;/code&gt;.&lt;/strong&gt; A tensor of shape &lt;code&gt;(4,)&lt;/code&gt; becomes &lt;code&gt;(1, 4)&lt;/code&gt; with &lt;code&gt;unsqueeze(0)&lt;/code&gt; or &lt;code&gt;(4, 1)&lt;/code&gt; with &lt;code&gt;unsqueeze(1)&lt;/code&gt;. Its inverse, &lt;code&gt;squeeze(dim)&lt;/code&gt;, removes a size-1 axis. Both are free: no memory is allocated and no data is copied.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# shape (4,)
&lt;/span&gt;
&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# (1, 4) — one row
&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# (4, 1) — one column
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# torch.Size([1, 4]) torch.Size([4, 1])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same four numbers, three different shapes. That's the whole idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does PyTorch need size-1 dimensions at all?
&lt;/h2&gt;

&lt;p&gt;Because a shape isn't just a size — it's a &lt;em&gt;meaning&lt;/em&gt;. In PyTorch, shape &lt;code&gt;(4,)&lt;/code&gt; means "four numbers." Shape &lt;code&gt;(1, 4)&lt;/code&gt; means "one row of four." Shape &lt;code&gt;(4, 1)&lt;/code&gt; means "four rows of one." Those are three different claims about your data, and PyTorch takes them literally.&lt;/p&gt;

&lt;p&gt;That's why so many frustrating shape errors happen not because your data is the wrong size, but because it's missing a dimension — or has one too many. A model expects a batch of inputs and you hand it a single sample. A loss function wants a column and you give it a flat row. The fix, nearly every time, is one call.&lt;/p&gt;

&lt;p&gt;The number you pass is &lt;em&gt;where&lt;/em&gt; the new axis goes. Dimension 0 is the front, dimension 1 is next, and negative indexing works too — &lt;code&gt;unsqueeze(-1)&lt;/code&gt; always appends a new last axis no matter how many dimensions the tensor already has.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# torch.Size([1, 3, 4])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# torch.Size([3, 1, 4])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# torch.Size([3, 4, 1])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The &lt;code&gt;None&lt;/code&gt; shortcut (same thing, fewer characters)
&lt;/h2&gt;

&lt;p&gt;There's a terser spelling you'll see constantly in real codebases: put &lt;code&gt;None&lt;/code&gt; inside an index expression. Wherever you place &lt;code&gt;None&lt;/code&gt;, a new size-1 axis appears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;     &lt;span class="c1"&gt;# same as unsqueeze(0)
&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="c1"&gt;# same as unsqueeze(1)
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# torch.Size([1, 4]) torch.Size([4, 1])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both spellings do exactly the same thing. &lt;code&gt;None&lt;/code&gt; indexing is shorter; &lt;code&gt;unsqueeze&lt;/code&gt; is more explicit about where the axis lands. Use whichever reads more clearly — you'll meet both in the wild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does &lt;code&gt;squeeze&lt;/code&gt; do in PyTorch?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;squeeze&lt;/code&gt; is the inverse: it removes dimensions whose size is 1. With no argument it removes &lt;strong&gt;all&lt;/strong&gt; of them; with a &lt;code&gt;dim&lt;/code&gt; argument it removes only that one — and only if it really is size 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;# torch.Size([1, 3, 1, 4])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# torch.Size([3, 4])     — all 1s gone
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# torch.Size([3, 1, 4])  — only dim 0
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# torch.Size([1, 3, 1, 4]) — dim 1 is size 3, unchanged
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that last line. Squeezing dimension 1 did nothing, because dimension 1 has size 3, not 1. &lt;code&gt;squeeze&lt;/code&gt; only removes axes that are actually size 1, so it's safe to call even when you're not sure.&lt;/p&gt;

&lt;p&gt;Safe, but not always &lt;em&gt;wise&lt;/em&gt; — see the gotchas below.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: the batch dimension
&lt;/h2&gt;

&lt;p&gt;This is the case that sends most beginners to Google, so let's walk it end to end.&lt;/p&gt;

&lt;p&gt;Most PyTorch models expect a &lt;strong&gt;batch&lt;/strong&gt; — shape &lt;code&gt;(N, ...)&lt;/code&gt;, where &lt;code&gt;N&lt;/code&gt; is how many samples you're passing. A single CIFAR-style image is shape &lt;code&gt;(3, 32, 32)&lt;/code&gt;: three channels, 32 by 32 pixels. That's three dimensions, and a lot of layers want four.&lt;/p&gt;

&lt;p&gt;Here's the part that trips people up, because it's changed over the years: a bare &lt;code&gt;nn.Conv2d&lt;/code&gt; will actually accept your unbatched image just fine. Modern PyTorch added no-batch-dim support to many &lt;code&gt;nn&lt;/code&gt; modules, so this runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="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="n"&gt;conv&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;Conv2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kernel_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# one image — 3 dims
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# torch.Size([16, 30, 30]) — no error
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So why does everyone tell you to &lt;code&gt;unsqueeze&lt;/code&gt;? Because the moment there's a normalization layer in the stack, the tolerance ends. &lt;code&gt;BatchNorm2d&lt;/code&gt; computes statistics &lt;em&gt;across the batch&lt;/em&gt;, so it genuinely cannot work without a batch axis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="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;Conv2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kernel_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&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;BatchNorm2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&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;sample&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: expected 4D input (got 3D input)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it literally: it got 3 dimensions and wanted 4. Nothing is wrong with your image — it's just not in a batch. &lt;code&gt;unsqueeze(0)&lt;/code&gt; wraps it in a batch of exactly one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# (1, 3, 32, 32)
&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# torch.Size([1, 3, 32, 32])
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# torch.Size([1, 16, 30, 30])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson generalizes: whether a &lt;em&gt;single&lt;/em&gt; layer tolerates a missing batch dimension is a detail that varies by layer and by version. Whether your &lt;em&gt;model&lt;/em&gt; does is not worth gambling on. Add the batch axis and the question disappears.&lt;/p&gt;

&lt;p&gt;And on the way out, you often want that batch axis gone again, or a trailing singleton removed — a regression head that outputs &lt;code&gt;(32, 1)&lt;/code&gt; won't line up with targets shaped &lt;code&gt;(32,)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# model output
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# torch.Size([32])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unsqueeze&lt;/code&gt; going in, &lt;code&gt;squeeze&lt;/code&gt; coming out. That pairing shows up in almost every inference script you'll ever write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other big use: lining up shapes for broadcasting
&lt;/h2&gt;

&lt;p&gt;The second place &lt;code&gt;unsqueeze&lt;/code&gt; earns its keep is broadcasting. To subtract a per-channel mean from an image, the mean has to be shaped so it aligns with the channel axis — not the width axis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0.485&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.406&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# (3,)
&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# (C, H, W)
&lt;/span&gt;
&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RuntimeError: The size of tensor a (224) must match the size of tensor b (3)
at non-singleton dimension 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Broadcasting aligns shapes from the &lt;strong&gt;right&lt;/strong&gt;, so &lt;code&gt;(3,)&lt;/code&gt; tries to line up with the 224-wide last axis and fails. Give it two trailing size-1 axes and it lines up correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="c1"&gt;# (3, 1, 1)
&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;                     &lt;span class="c1"&gt;# broadcasts to (3, 224, 224)
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# torch.Size([3, 1, 1]) torch.Size([3, 224, 224])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;1&lt;/code&gt;s are placeholders that say "stretch me along this axis." That's what makes the subtraction apply per channel instead of per pixel column. If the right-to-left alignment rule is new to you, I wrote up &lt;a href="https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606"&gt;how PyTorch broadcasting works in full here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are &lt;code&gt;squeeze&lt;/code&gt; and &lt;code&gt;unsqueeze&lt;/code&gt; expensive?
&lt;/h2&gt;

&lt;p&gt;No — they're free. Both return a &lt;strong&gt;view&lt;/strong&gt; of the same data with adjusted strides. No memory is allocated, nothing is copied, so you can call them freely, even inside a hot 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="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stride&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                     &lt;span class="c1"&gt;# (4, 1)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical data pointer means no copy happened. &lt;code&gt;unsqueeze&lt;/code&gt; just added a stride entry. This is &lt;a href="https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o"&gt;the same principle behind &lt;code&gt;view&lt;/code&gt; and &lt;code&gt;reshape&lt;/code&gt;&lt;/a&gt;: any shape change expressible as a stride change is a view; anything else needs a copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bare &lt;code&gt;squeeze()&lt;/code&gt; eating your batch dimension.&lt;/strong&gt; If your batch size happens to be 1, &lt;code&gt;squeeze()&lt;/code&gt; with no argument silently deletes it along with every other singleton — and your shapes break several lines later, far from the cause. Prefer &lt;code&gt;squeeze(dim)&lt;/code&gt;, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing &lt;code&gt;unsqueeze(1)&lt;/code&gt; with &lt;code&gt;unsqueeze(-1)&lt;/code&gt;.&lt;/strong&gt; They're the same only for 1-D tensors. On a &lt;code&gt;(3, 4)&lt;/code&gt; tensor, &lt;code&gt;unsqueeze(1)&lt;/code&gt; gives &lt;code&gt;(3, 1, 4)&lt;/code&gt; while &lt;code&gt;unsqueeze(-1)&lt;/code&gt; gives &lt;code&gt;(3, 4, 1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expecting &lt;code&gt;squeeze(dim)&lt;/code&gt; to raise on a non-1 axis.&lt;/strong&gt; It doesn't — it quietly returns the tensor unchanged. Convenient, but it means a typo'd &lt;code&gt;dim&lt;/code&gt; fails silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reaching for &lt;code&gt;reshape&lt;/code&gt; where &lt;code&gt;unsqueeze&lt;/code&gt; is clearer.&lt;/strong&gt; &lt;code&gt;x.reshape(1, *x.shape)&lt;/code&gt; works, but &lt;code&gt;x.unsqueeze(0)&lt;/code&gt; says what you mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the batch axis at inference time.&lt;/strong&gt; Training works because your &lt;code&gt;DataLoader&lt;/code&gt; adds the batch dimension for you. Then you feed a single sample by hand and it breaks. That's not a new bug — it's the &lt;code&gt;DataLoader&lt;/code&gt; no longer doing you a favor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting that "it ran" means the shape was right.&lt;/strong&gt; Since some &lt;code&gt;nn&lt;/code&gt; modules accept unbatched input, a missing batch dimension can survive several layers before something downstream complains — or worse, quietly produce output of the wrong rank. Print &lt;code&gt;.shape&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;unsqueeze(dim)&lt;/code&gt; inserts a size-1 axis at &lt;code&gt;dim&lt;/code&gt;; &lt;code&gt;squeeze(dim)&lt;/code&gt; removes one. &lt;code&gt;v[None, :]&lt;/code&gt; is the same thing written inline. Both are free views — no copy. Use &lt;code&gt;unsqueeze(0)&lt;/code&gt; to add a batch dimension for a single sample, &lt;code&gt;unsqueeze(-1)&lt;/code&gt; or &lt;code&gt;[:, None, None]&lt;/code&gt; to align shapes for broadcasting, and &lt;code&gt;squeeze(-1)&lt;/code&gt; to drop a trailing singleton from model outputs.&lt;/p&gt;




&lt;p&gt;This is one idea from my book &lt;em&gt;PyTorch From Ground Up&lt;/em&gt;, which builds everything from tensors upward so nothing stays vague. If it helped, you can grab the &lt;a href="https://payhip.com/b/7ukxh" rel="noopener noreferrer"&gt;free PyTorch tensor cheat-sheet here&lt;/a&gt;, run every example from the book in the &lt;a href="https://github.com/pytorch-from-ground-up/book_code" rel="noopener noreferrer"&gt;companion notebooks on GitHub&lt;/a&gt;, or find the &lt;a href="https://www.amazon.com/dp/B0H8WMCV33" rel="noopener noreferrer"&gt;full paperback on Amazon here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>pytorch</category>
      <category>python</category>
      <category>beginners</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>Reshape vs View in PyTorch: What's the Difference (and When `view()` Breaks)</title>
      <dc:creator>Wesam Khallaf — Author of PyTorch From Ground Up</dc:creator>
      <pubDate>Fri, 17 Jul 2026 06:59:46 +0000</pubDate>
      <link>https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o</link>
      <guid>https://dev.to/pytorchfromgroundup/reshape-vs-view-in-pytorch-whats-the-difference-and-when-view-breaks-4a9o</guid>
      <description>&lt;p&gt;You called &lt;code&gt;.view()&lt;/code&gt; on a tensor, got a &lt;code&gt;RuntimeError: view size is not compatible with input tensor's size and stride&lt;/code&gt;, switched to &lt;code&gt;.reshape()&lt;/code&gt;, and it just... worked. So what's actually going on — and which one should you use?&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;reshape&lt;/code&gt; and &lt;code&gt;view&lt;/code&gt; both give you the same numbers under a new shape. The difference is one word: &lt;strong&gt;contiguity&lt;/strong&gt;. &lt;code&gt;view()&lt;/code&gt; returns a zero-copy view but &lt;em&gt;requires the tensor to be contiguous&lt;/em&gt; — if it isn't, it errors. &lt;code&gt;reshape()&lt;/code&gt; returns a view when it can and silently makes a copy when it can't, so it never errors on contiguity. &lt;strong&gt;Use &lt;code&gt;reshape()&lt;/code&gt; by default;&lt;/strong&gt; reach for &lt;code&gt;view()&lt;/code&gt; only when you specifically need to guarantee shared memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reshape vs view exists at all
&lt;/h2&gt;

&lt;p&gt;A tensor's data and its shape are two separate things. The data is a flat row of numbers sitting in memory; the shape is just the lens you look at it through. Reshaping changes the lens without touching the numbers — twelve elements can be seen as a 3×4 matrix, a 4×3, a 2×6, or a flat row of 12.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# 12 elements: 1..12
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only constraint is that the product of the new shape must equal the total number of elements (&lt;code&gt;numel()&lt;/code&gt;). Twelve elements can become &lt;code&gt;(3, 4)&lt;/code&gt; because 3 × 4 = 12, but not &lt;code&gt;(3, 5)&lt;/code&gt; because 3 × 5 = 15.&lt;/p&gt;

&lt;p&gt;When the tensor is contiguous — which it almost always is right after you create it — &lt;code&gt;view&lt;/code&gt; and &lt;code&gt;reshape&lt;/code&gt; do exactly the same thing: return a zero-cost view of the same memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# view — always free
&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# reshape — also free here
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data_ptr&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same data pointer means no copy happened. So far, no difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does "contiguous" mean in PyTorch?
&lt;/h2&gt;

&lt;p&gt;A tensor is &lt;strong&gt;contiguous&lt;/strong&gt; when its elements — read left-to-right along the last dimension, then the next dimension out, and so on — sit in that same order in memory. Most tensors are contiguous from birth. The operations that break contiguity are &lt;code&gt;transpose&lt;/code&gt;, &lt;code&gt;permute&lt;/code&gt;, and &lt;code&gt;expand&lt;/code&gt;: they change the &lt;em&gt;strides&lt;/em&gt; without moving the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_contiguous&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stride&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;          &lt;span class="c1"&gt;# (3, 1)
&lt;/span&gt;
&lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;                   &lt;span class="c1"&gt;# transpose
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_contiguous&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# False
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stride&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;         &lt;span class="c1"&gt;# (1, 3)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the transpose the strides are reversed. The numbers are still in the same flat row &lt;code&gt;0 1 2 3 4 5&lt;/code&gt;, but reading them in the new dimension order no longer walks through memory in order. That is non-contiguous — and it's exactly the situation where &lt;code&gt;view()&lt;/code&gt; breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  When &lt;code&gt;view()&lt;/code&gt; breaks (the error you googled)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;                       &lt;span class="c1"&gt;# non-contiguous
&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;# RuntimeError!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RuntimeError: view size is not compatible with input tensor's size and stride
(at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PyTorch is telling you the operation isn't safe as a zero-copy view. You have two fixes:&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;               &lt;span class="c1"&gt;# works — quietly copies
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contiguous&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;# also works — rearranges memory first, then views
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;reshape&lt;/code&gt; is almost always the right default. Call &lt;code&gt;.contiguous().view(...)&lt;/code&gt; only when you have a reason to force the copy explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch out: reshape doesn't guarantee a view
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;reshape&lt;/code&gt; returns a view &lt;em&gt;sometimes&lt;/em&gt; and a copy &lt;em&gt;other times&lt;/em&gt;, you cannot rely on it to share memory. If you need in-place edits to propagate back to the original, use &lt;code&gt;view&lt;/code&gt; — and if it errors, that's PyTorch telling you the operation genuinely can't be done without copying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;-1&lt;/code&gt; shortcut
&lt;/h2&gt;

&lt;p&gt;You can pass &lt;code&gt;-1&lt;/code&gt; for exactly one dimension and PyTorch infers its size from the total element count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# (4, 6)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# (8, 3)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (2, 3, 4)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At most one &lt;code&gt;-1&lt;/code&gt; per call. This is everywhere in real code — you know some dimensions and let PyTorch solve for the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: flatten before a linear layer
&lt;/h2&gt;

&lt;p&gt;The single most common reshape in practice is collapsing spatial dimensions before a fully-connected layer, while keeping the batch dimension intact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# N, C, H, W
&lt;/span&gt;&lt;span class="n"&gt;flat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# flatten from dim 1 onward
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;# torch.Size([32, 2352])
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flatten(1)&lt;/code&gt; keeps dimension 0 (the batch) and merges the rest into one. Note the trap: &lt;code&gt;flatten()&lt;/code&gt; with &lt;strong&gt;no argument&lt;/strong&gt; collapses &lt;em&gt;everything&lt;/em&gt;, batch included — inside a model's forward pass, always use &lt;code&gt;flatten(1)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calling &lt;code&gt;view()&lt;/code&gt; after &lt;code&gt;transpose&lt;/code&gt;/&lt;code&gt;permute&lt;/code&gt;.&lt;/strong&gt; That's the classic &lt;code&gt;RuntimeError&lt;/code&gt;. Use &lt;code&gt;reshape()&lt;/code&gt; or &lt;code&gt;.contiguous().view()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming &lt;code&gt;reshape&lt;/code&gt; shares memory.&lt;/strong&gt; Sometimes it copies. If you need a guaranteed view, use &lt;code&gt;view()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using bare &lt;code&gt;flatten()&lt;/code&gt; in a forward pass.&lt;/strong&gt; It eats the batch dimension. Use &lt;code&gt;flatten(1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong element count.&lt;/strong&gt; &lt;code&gt;reshape(3, 5)&lt;/code&gt; on 12 elements raises &lt;code&gt;RuntimeError: shape '[3, 5]' is invalid for input of size 12&lt;/code&gt;. The product must equal &lt;code&gt;numel()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;view&lt;/code&gt; and &lt;code&gt;reshape&lt;/code&gt; both reinterpret the same flat memory under a new shape. &lt;code&gt;view&lt;/code&gt; requires contiguity and always returns a true view; &lt;code&gt;reshape&lt;/code&gt; returns a view when it can and copies when it must, so it never errors on contiguity. Default to &lt;code&gt;reshape&lt;/code&gt;; use &lt;code&gt;view&lt;/code&gt; when you specifically need shared memory.&lt;/p&gt;




&lt;p&gt;This is one idea from my book &lt;em&gt;PyTorch From Ground Up&lt;/em&gt;, which builds everything from tensors upward so nothing stays vague. If it helped, you can grab the &lt;a href="https://payhip.com/b/7ukxh" rel="noopener noreferrer"&gt;free PyTorch tensor cheat-sheet here&lt;/a&gt;, run every example from the book in the &lt;a href="https://github.com/pytorch-from-ground-up/book_code" rel="noopener noreferrer"&gt;companion notebooks on GitHub&lt;/a&gt;, or find the &lt;a href="https://www.amazon.com/dp/B0H8WMCV33" rel="noopener noreferrer"&gt;full paperback on Amazon here&lt;/a&gt; or find a &lt;a href="https://leanpub.com/pytorchfromgroundup" rel="noopener noreferrer"&gt;digital copy of the book on leanpup here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>PyTorch Broadcasting Explained: The 3 Rules (and the Silent Bug That Bites Everyone)</title>
      <dc:creator>Wesam Khallaf — Author of PyTorch From Ground Up</dc:creator>
      <pubDate>Thu, 16 Jul 2026 08:09:38 +0000</pubDate>
      <link>https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606</link>
      <guid>https://dev.to/pytorchfromgroundup/pytorch-broadcasting-explained-the-3-rules-and-the-silent-bug-that-bites-everyone-3606</guid>
      <description>&lt;p&gt;You added two tensors of different shapes and PyTorch didn't complain — it just returned something bigger than you expected. Or you got a &lt;code&gt;RuntimeError&lt;/code&gt; about sizes that don't match and no idea which dimension is wrong. Both come down to one feature: broadcasting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;PyTorch broadcasting follows exactly three rules: &lt;strong&gt;align shapes from the right, each aligned pair of dimensions must be equal or one of them must be 1, and any size-1 dimension is virtually stretched to match the other.&lt;/strong&gt; If a pair is neither equal nor 1, you get a &lt;code&gt;RuntimeError&lt;/code&gt;. The stretch never copies memory — it's implemented with a stride of 0. Learn the three rules once and broadcasting stops surprising you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core example
&lt;/h2&gt;

&lt;p&gt;Add a column of three numbers to a row of four:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;   &lt;span class="c1"&gt;# shape (3, 1)
&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="c1"&gt;# shape (1, 4)
&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No loop, no manual copying. PyTorch stretched the column across four columns and the row down three rows, then added element by element. Critically, the stretch is &lt;em&gt;virtual&lt;/em&gt; — no memory is allocated for the repeated values, which is why broadcasting is fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3 broadcasting rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Align from the right.&lt;/strong&gt; Line the shapes up starting at the last dimension. If one tensor has fewer dimensions, pad it with 1s on the left.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each pair must match.&lt;/strong&gt; For each aligned pair, the sizes must be equal, or one of them must be 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size-1 dims stretch.&lt;/strong&gt; Any dimension of size 1 is virtually repeated to match the other tensor along that axis.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Walking the core example through the rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        dim0   dim1
a         3      1   -&amp;gt;  stretches to 4
b         1      4   -&amp;gt;  stretches to 3   (b padded to (1, 4))
result    3      4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shape takes the max of each aligned pair: &lt;code&gt;(3, 4)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked examples
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scalar + matrix.&lt;/strong&gt; A scalar has shape &lt;code&gt;()&lt;/code&gt; and pads to match anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# (3, 4)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Vector + matrix.&lt;/strong&gt; A &lt;code&gt;(4,)&lt;/code&gt; vector aligns with the last dim of a &lt;code&gt;(3, 4)&lt;/code&gt; matrix — it's implicitly treated as &lt;code&gt;(1, 4)&lt;/code&gt;, then stretched down three rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# (4,)
&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# (3, 4)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# (3, 4)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Per-channel image normalization.&lt;/strong&gt; Subtract a &lt;code&gt;(3, 1, 1)&lt;/code&gt; mean from a &lt;code&gt;(3, 224, 224)&lt;/code&gt; image — this is standard ImageNet normalization, and the size-1 dims are exactly what broadcasting knows how to stretch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;([.&lt;/span&gt;&lt;span class="mi"&gt;485&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;406&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# (3, 1, 1)
&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;             &lt;span class="c1"&gt;# (3, 224, 224)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why does broadcasting fail with a RuntimeError?
&lt;/h2&gt;

&lt;p&gt;If a pair of aligned dimensions is neither equal nor 1, PyTorch raises a &lt;code&gt;RuntimeError&lt;/code&gt; — it's protecting you from a silent mistake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;   &lt;span class="c1"&gt;# RuntimeError!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RuntimeError: The size of tensor a (3) must match the size of tensor b (5)
at non-singleton dimension 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is always the same: reshape one tensor so the mismatched dimension becomes size 1 (with &lt;code&gt;unsqueeze&lt;/code&gt; or &lt;code&gt;reshape&lt;/code&gt;), then let broadcasting do the rest. If the shapes genuinely shouldn't combine, the error just saved you from a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent broadcasting bug (this one has no error)
&lt;/h2&gt;

&lt;p&gt;The real danger isn't the &lt;code&gt;RuntimeError&lt;/code&gt; — that message tells you exactly what's wrong. The danger is when broadcasting &lt;em&gt;succeeds silently&lt;/em&gt; and hands you a bigger tensor than you meant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# (3,)
&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# (3, 1)
&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# (3, 3)  &amp;lt;-- probably wrong!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You likely wanted an element-wise &lt;code&gt;(3,)&lt;/code&gt; result. Instead &lt;code&gt;(3,)&lt;/code&gt; was padded to &lt;code&gt;(1, 3)&lt;/code&gt;, &lt;code&gt;(3, 1)&lt;/code&gt; stayed as is, and you got a 3×3 matrix. No error, no warning — just quietly wrong, and it compounds ten lines later. This is the number-one broadcasting bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broadcasting and memory: the stride-0 trick
&lt;/h2&gt;

&lt;p&gt;The stretch is virtual. Internally PyTorch uses a &lt;strong&gt;stride of 0&lt;/strong&gt; along the stretched dimension, so the same single row or column is reused for every position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# explicit broadcast
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# (3, 4)
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stride&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;      &lt;span class="c1"&gt;# (0, 1)  &amp;lt;- stride 0!
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;numel&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;       &lt;span class="c1"&gt;# 12 "virtual" elements
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;numel&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;       &lt;span class="c1"&gt;# 4 actual elements
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expanded tensor reports 12 elements but only 4 exist in memory. Stride 0 means "don't advance — reuse the same row." That's why broadcasting is fast even when the virtual expanded tensor would be huge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subtracting &lt;code&gt;(3,)&lt;/code&gt; and &lt;code&gt;(3, 1)&lt;/code&gt; expecting &lt;code&gt;(3,)&lt;/code&gt;.&lt;/strong&gt; You get &lt;code&gt;(3, 3)&lt;/code&gt;. Make both shapes match exactly for element-wise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mismatched non-1 dimensions.&lt;/strong&gt; &lt;code&gt;(3,) + (5,)&lt;/code&gt; raises a &lt;code&gt;RuntimeError&lt;/code&gt; at the non-singleton dimension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not knowing which dim stretched.&lt;/strong&gt; Print &lt;code&gt;.shape&lt;/code&gt; before &lt;em&gt;and&lt;/em&gt; after every broadcast until the habit is automatic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to add size-1 dims.&lt;/strong&gt; Per-channel ops need an explicit &lt;code&gt;unsqueeze&lt;/code&gt;/&lt;code&gt;None&lt;/code&gt; to line up the axes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A three-line habit that catches every broadcasting bug
&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;# 1. Print both shapes
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 2. Align from the right on paper:  a: 3,1   b: 1,4  -&amp;gt; out 3,4 — is that what you want?
# 3. If not, unsqueeze or reshape to control which dim stretches
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Print, align, decide. That habit catches broadcasting bugs before they turn into a wrong result downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Align from the right, each pair must be equal or one must be 1, and size-1 dims stretch for free. The output takes the max of each pair. A mismatch raises a &lt;code&gt;RuntimeError&lt;/code&gt;; the sneakier failure is a silent blow-up that returns the wrong shape — so print your shapes.&lt;/p&gt;




&lt;p&gt;This is one idea from my book &lt;em&gt;PyTorch From Ground Up&lt;/em&gt;, which builds everything from tensors upward so nothing stays vague. If it helped, you can grab the &lt;a href="https://payhip.com/b/7ukxh" rel="noopener noreferrer"&gt;free PyTorch tensor cheat-sheet here&lt;/a&gt;, run every example from the book in the &lt;a href="https://github.com/pytorch-from-ground-up/book_code" rel="noopener noreferrer"&gt;companion notebooks on GitHub&lt;/a&gt;, or find the &lt;a href="https://www.amazon.com/dp/B0H8WMCV33" rel="noopener noreferrer"&gt;full paperback on Amazon here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pytorch</category>
      <category>broadcasting</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
