<?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: Ishita Garg</title>
    <description>The latest articles on DEV Community by Ishita Garg (@ishita_garg).</description>
    <link>https://dev.to/ishita_garg</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%2F4039586%2F124484e9-0dbc-4969-a5cc-80a217265891.jpeg</url>
      <title>DEV Community: Ishita Garg</title>
      <link>https://dev.to/ishita_garg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ishita_garg"/>
    <language>en</language>
    <item>
      <title>Masked Self-Attention, Explained Through Avengers: Endgame</title>
      <dc:creator>Ishita Garg</dc:creator>
      <pubDate>Sat, 15 Aug 2026 07:25:23 +0000</pubDate>
      <link>https://dev.to/ishita_garg/masked-self-attention-explained-through-avengers-endgame-3d70</link>
      <guid>https://dev.to/ishita_garg/masked-self-attention-explained-through-avengers-endgame-3d70</guid>
      <description>&lt;p&gt;If you've studied transformers, you've run into this sentence a dozen times:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Masked self-attention prevents a token from attending to future positions."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fine. But &lt;em&gt;why&lt;/em&gt; does that matter, and &lt;em&gt;what actually happens&lt;/em&gt; inside the model when you mask something? I found the cleanest way to internalize it wasn't through more equations. It was through a movie I already knew scene-by-scene: &lt;strong&gt;Avengers: Endgame&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the full mapping, with the actual math and code underneath every metaphor.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Self-Attention = The Infinity Stones
&lt;/h2&gt;

&lt;p&gt;In a standard self-attention layer, every token in a sequence can look at &lt;em&gt;every other token&lt;/em&gt;, including ones ahead of it in the sequence. For each token, we compute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attention(Q, K, V) = softmax(QKᵀ / √d_k) V
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Q (query), K (key), and V (value) are just learned projections of the input. The &lt;code&gt;QKᵀ&lt;/code&gt; term produces a score between every pair of tokens - how much token &lt;em&gt;i&lt;/em&gt; should "attend to" token &lt;em&gt;j&lt;/em&gt;, for &lt;strong&gt;all&lt;/strong&gt; i and j, regardless of order.&lt;/p&gt;

&lt;p&gt;That's enormous power. Think of it like holding all six Infinity Stones at once: Space, Time, Mind, Power, Reality, Soul. Any token can reach into any part of the sequence, past, present, or future, and pull in whatever information helps it the most.&lt;/p&gt;

&lt;p&gt;Powerful. Also, in one very specific context, dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Data Leakage = Thanos's Snap
&lt;/h2&gt;

&lt;p&gt;Here's where it gets dangerous. Transformers used for &lt;strong&gt;autoregressive language modeling&lt;/strong&gt; (GPT-style models) are trained to predict the &lt;em&gt;next&lt;/em&gt; token given everything before it. The training objective looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(token_t | token_1, token_2, ..., token_t-1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine during training, the attention mechanism has full, unmasked access to the entire sequence, including &lt;code&gt;token_t&lt;/code&gt;, the exact word it's supposed to predict. The model doesn't need to learn anything. It just looks at the answer key sitting right there in the input and copies it.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;data leakage&lt;/strong&gt;, and it's a well-known failure mode any time a model has access to information it wouldn't have at inference time. During training you have the whole sequence sitting in memory; during real-world inference (actually generating text word by word) you obviously don't have the future yet. If your model trained &lt;em&gt;as if it did&lt;/em&gt;, it will collapse the moment it has to generate something real.&lt;/p&gt;

&lt;p&gt;Thanos does the same thing, structurally. He doesn't discover the future; he uses the Time Stone to rearrange the present so that his already-known outcome comes true. He's not solving anything. He's cheating the timeline. And just like a leaky model, that "solution" only holds up as long as the shortcut exists. The moment reality has to run forward on its own (his snap gets undone), everything falls apart, because nothing was actually &lt;em&gt;learned&lt;/em&gt;. It was just forced.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Masked Self-Attention = Iron Man's Counter-Snap
&lt;/h2&gt;

&lt;p&gt;The fix is almost insultingly simple, and that's what makes it elegant.&lt;/p&gt;

&lt;p&gt;Before the softmax step, we take the raw attention score matrix and set every score that corresponds to a "future" position to negative infinity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scores[i][j] = -∞   for all j &amp;gt; i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we apply softmax. Since &lt;code&gt;softmax(-∞) = 0&lt;/code&gt;, every future position's contribution vanishes completely. Token &lt;em&gt;i&lt;/em&gt; is left attending only to positions &lt;code&gt;0&lt;/code&gt; through &lt;code&gt;i&lt;/code&gt;, itself and everything before it. Nothing after.&lt;/p&gt;

&lt;p&gt;This is done with a &lt;strong&gt;causal mask&lt;/strong&gt;, and visually, it looks like this for a 5-token sequence, where &lt;strong&gt;v&lt;/strong&gt; means "visible / allowed to attend" and &lt;strong&gt;x&lt;/strong&gt; means "blocked, this is a future position":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        tok0  tok1  tok2  tok3  tok4
tok0  [  v     x     x     x     x  ]
tok1  [  v     v     x     x     x  ]
tok2  [  v     v     v     x     x  ]
tok3  [  v     v     v     v     x  ]
tok4  [  v     v     v     v     v  ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row can only "see" columns up to and including its own position. This is the lower-triangular structure you'll see referred to constantly in transformer papers and code (&lt;code&gt;tf.linalg.band_part&lt;/code&gt;, &lt;code&gt;torch.triu&lt;/code&gt;, "causal mask," "look-ahead mask", all the same idea, just implemented differently depending on the framework).&lt;/p&gt;

&lt;p&gt;The important part, and the reason the Endgame analogy actually holds up structurally and not just narratively: &lt;strong&gt;masking doesn't introduce a new mechanism.&lt;/strong&gt; It's the exact same Q, K, V computation, the exact same softmax. You're not adding new machinery, you're constraining the existing one. Iron Man doesn't get a seventh stone. He uses the same six, wielded correctly, to undo what an unconstrained use of that same power caused.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Code
&lt;/h2&gt;

&lt;p&gt;Here's a minimal, from-scratch implementation of masked self-attention in TensorFlow:&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;tensorflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;masked_self_attention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Q, K, V: tensors of shape (batch, seq_len, d_k)
    Returns: attention output of shape (batch, seq_len, d_k), attention weights
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;d_k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="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;tf&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="c1"&gt;# Raw attention scores: how much each token attends to every other token
&lt;/span&gt;    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matmul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transpose_b&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="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# shape: (batch, seq_len, seq_len)
&lt;/span&gt;
    &lt;span class="c1"&gt;# Build the causal mask: 1 where j &amp;gt; i (future positions)
&lt;/span&gt;    &lt;span class="n"&gt;seq_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&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;causal_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;band_part&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tf&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="n"&gt;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seq_len&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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Set future positions to -inf before softmax
&lt;/span&gt;    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;causal_mask&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# After softmax, masked positions collapse to ~0: future is erased
&lt;/span&gt;    &lt;span class="n"&gt;attn_weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matmul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attn_weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;attn_weights&lt;/span&gt;


&lt;span class="c1"&gt;# Quick sanity check
&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_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;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="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="n"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&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;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&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;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;V&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&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;seq_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;masked_self_attention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running that print statement gives:&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;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;   &lt;span class="mf"&gt;0.&lt;/span&gt;   &lt;span class="mf"&gt;0.&lt;/span&gt;   &lt;span class="mf"&gt;0.&lt;/span&gt;   &lt;span class="mf"&gt;0.&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.52&lt;/span&gt; &lt;span class="mf"&gt;0.48&lt;/span&gt; &lt;span class="mf"&gt;0.&lt;/span&gt;   &lt;span class="mf"&gt;0.&lt;/span&gt;   &lt;span class="mf"&gt;0.&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.36&lt;/span&gt; &lt;span class="mf"&gt;0.36&lt;/span&gt; &lt;span class="mf"&gt;0.28&lt;/span&gt; &lt;span class="mf"&gt;0.&lt;/span&gt;   &lt;span class="mf"&gt;0.&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.25&lt;/span&gt; &lt;span class="mf"&gt;0.27&lt;/span&gt; &lt;span class="mf"&gt;0.21&lt;/span&gt; &lt;span class="mf"&gt;0.27&lt;/span&gt; &lt;span class="mf"&gt;0.&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.2&lt;/span&gt;  &lt;span class="mf"&gt;0.2&lt;/span&gt;  &lt;span class="mf"&gt;0.19&lt;/span&gt; &lt;span class="mf"&gt;0.22&lt;/span&gt; &lt;span class="mf"&gt;0.18&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the causal mask made real. Every entry above the diagonal is exactly &lt;code&gt;0&lt;/code&gt;, exactly matching the &lt;code&gt;x&lt;/code&gt; positions in the diagram above. Token 0 can only attend to itself, so its full weight (&lt;code&gt;1.0&lt;/code&gt;) goes there. Token 4, the last one, can attend everywhere, so it's the only row with all five positions filled in. Theory and output line up exactly.&lt;/p&gt;

&lt;p&gt;A quick note if you run this yourself: the exact decimal values (like &lt;code&gt;0.52&lt;/code&gt;, &lt;code&gt;0.48&lt;/code&gt; in row 2) will likely be slightly different each time, since &lt;code&gt;Q&lt;/code&gt; and &lt;code&gt;K&lt;/code&gt; are randomly initialized and &lt;code&gt;tf.random.set_seed()&lt;/code&gt; alone doesn't always guarantee identical values across every rerun or environment. That's expected and fine. What's fixed and guaranteed, every single time, is the zero pattern in the upper triangle. That's the mask doing its job, not the randomness. If you want fully reproducible numbers, pass an explicit &lt;code&gt;seed&lt;/code&gt; argument to each &lt;code&gt;tf.random.uniform()&lt;/code&gt; call individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why This Matters Beyond the Metaphor
&lt;/h2&gt;

&lt;p&gt;This isn't just a training-time implementation detail. It's the reason autoregressive generation works at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;At inference time&lt;/strong&gt;, the model generates one token at a time, and by construction it only ever has access to past tokens. Masking during training makes sure the model &lt;em&gt;never learns to rely on information it won't have later&lt;/em&gt;. Train/inference consistency is the whole point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoder vs. decoder difference&lt;/strong&gt;: this is exactly why BERT (encoder-only, bidirectional) doesn't use causal masking. It's trained with masked &lt;em&gt;language modeling&lt;/em&gt; (a different kind of masking, hiding random tokens, not future tokens), and it's allowed to see the full sequence in both directions. GPT-style decoders use causal masking because their whole objective depends on next-token prediction being &lt;em&gt;honest&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's cheap&lt;/strong&gt;: masking adds essentially zero computational cost. It's one matrix operation and a softmax that was happening anyway. The "fix" for a fundamental correctness problem is a few lines of code, not a redesign.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. The Takeaway
&lt;/h2&gt;

&lt;p&gt;Sometimes the fastest way to actually &lt;em&gt;retain&lt;/em&gt; a technical concept is to hang it on a structure you already have memorized. The Endgame analogy isn't just decoration here; the structural parallel is genuinely tight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same underlying power (attention / the Stones)&lt;/li&gt;
&lt;li&gt;Misused, it breaks the thing it's supposed to build (data leakage / the Snap)&lt;/li&gt;
&lt;li&gt;The fix isn't new machinery, it's the same mechanism, constrained correctly (masking / the counter-snap)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Causality isn't a footnote in transformer architecture. It's the constraint that makes autoregressive generation &lt;em&gt;honest&lt;/em&gt;, the difference between a model that has learned to predict the future and one that just memorized it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, I write about ML/DL concepts and the projects I'm building. Check out more at &lt;a href="https://dev.to/ishita_garg"&gt;dev.to/ishita_garg&lt;/a&gt;. Feedback and corrections welcome, always happy to be told where the analogy breaks down.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>nlp</category>
      <category>transformers</category>
    </item>
    <item>
      <title>I Compared 3 Ways to Do Transfer Learning - Here's What Actually Reduced Overfitting</title>
      <dc:creator>Ishita Garg</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:22:35 +0000</pubDate>
      <link>https://dev.to/ishita_garg/i-compared-3-ways-to-do-transfer-learning-heres-what-actually-reduced-overfitting-3m2g</link>
      <guid>https://dev.to/ishita_garg/i-compared-3-ways-to-do-transfer-learning-heres-what-actually-reduced-overfitting-3m2g</guid>
      <description>&lt;p&gt;If you've trained a deep learning model from scratch, you already know the two problems that hit you immediately: you need a &lt;em&gt;lot&lt;/em&gt; of labeled data, and even if you have it, training takes forever. I ran into both while working on a cats vs. dogs image classifier, and it's what pushed me to actually understand transfer learning instead of just using it as a buzzword.&lt;/p&gt;

&lt;p&gt;This post walks through four experiments I ran on the same dataset - a CNN I built and trained entirely from scratch, followed by three transfer learning approaches using VGG16 - to see how much transfer learning actually helps, and why the &lt;em&gt;order&lt;/em&gt; you apply these techniques matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just train your own model?
&lt;/h2&gt;

&lt;p&gt;Deep learning models are data-hungry. To train something reliable from scratch, you typically need thousands of labeled images - and labeling isn't free. Someone (or something) has to go through every image and mark whether it's a cat or a dog, which costs time and, at scale, money.&lt;/p&gt;

&lt;p&gt;Even with enough data, training a CNN from scratch on a reasonably large dataset takes significant compute time. Both of these - data scarcity and training cost - are exactly what transfer learning is designed to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is transfer learning, actually?
&lt;/h2&gt;

&lt;p&gt;The simplest way I can put it: transfer learning means taking a model that already learned something useful from one problem, and reusing that knowledge on a different but related problem.&lt;/p&gt;

&lt;p&gt;It's the same idea as learning to ride a bicycle before a motorcycle - the balance and coordination your brain already built don't get thrown away, they get reused. Or if you play violin, picking up guitar is easier because you already understand musical notes, rhythm, and practice discipline.&lt;/p&gt;

&lt;p&gt;In deep learning, this looks like: take a CNN that's already been trained on a huge, general image dataset (like ImageNet, ~1.4 million images across 1000 categories), and reuse its learned visual understanding on your own, much smaller dataset.&lt;/p&gt;

&lt;p&gt;Why does this actually work? Because convolutional layers learn hierarchically. Early layers pick up primitive features - edges, colors, simple textures - which are common across &lt;em&gt;almost any&lt;/em&gt; real-world image. Later layers combine those into more complex, task-specific patterns. So the early general knowledge doesn't need to be relearned every time; only the final, task-specific classification layers need to adapt to your actual problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  My experiment: Cats vs. Dogs, from scratch vs. transfer learning
&lt;/h2&gt;

&lt;p&gt;I used the &lt;a href="https://www.kaggle.com/datasets/salader/dogsvscats" rel="noopener noreferrer"&gt;Cats vs Dogs dataset from Kaggle&lt;/a&gt; and ran four experiments: first a CNN trained entirely from scratch as my baseline, then three transfer learning approaches, each building on lessons from the last.&lt;/p&gt;

&lt;h3&gt;
  
  
  Baseline: A CNN trained from scratch (not transfer learning)
&lt;/h3&gt;

&lt;p&gt;Before touching transfer learning at all, I trained my own CNN architecture from scratch, with no pretrained weights involved - this is the point of comparison for everything that follows.&lt;/p&gt;

&lt;p&gt;My first version had no regularization. Result: &lt;strong&gt;99% training accuracy, but only 78% validation accuracy&lt;/strong&gt;, with validation loss climbing past 1.3 as training progressed. That gap is a textbook overfitting signature - the model was memorizing training images rather than learning generalizable features.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpml03ngvgcrnhat9kwqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpml03ngvgcrnhat9kwqd.png" alt=" " width="556" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F05xtgmo0prwhvniod0vd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F05xtgmo0prwhvniod0vd.png" alt=" " width="547" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding dropout and batch normalization changed the picture: &lt;strong&gt;87% train / 76% val&lt;/strong&gt;, with validation loss staying controlled instead of diverging. Notice train accuracy actually &lt;em&gt;dropped&lt;/em&gt; - that's expected and healthy. The model gave up some memorization in exchange for learning patterns that generalize better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F87vab6fo0q45xwjxr8yk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F87vab6fo0q45xwjxr8yk.png" alt=" " width="556" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even with regularization, a meaningful train-val gap remained. That gap is what pushed me toward transfer learning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transfer Learning Approach 1: VGG16 Feature Extraction
&lt;/h3&gt;

&lt;p&gt;Here's where transfer learning actually starts. I used VGG16 - a CNN pretrained on ImageNet - as a frozen feature extractor. I removed VGG16's original classification head, froze its convolutional base entirely, and attached my own dense layers on top:&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;relu&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dense&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;activation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sigmoid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;98% train / 89% val accuracy&lt;/strong&gt; - a big jump over the from-scratch baseline. But the train-val gap widened again, since the dense head was learning fast on top of already-excellent frozen features, with nothing to keep it in check.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvv3vmpcuf861zxcszxiz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvv3vmpcuf861zxcszxiz.png" alt=" " width="556" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Transfer Learning Approach 2: Feature Extraction + Data Augmentation
&lt;/h3&gt;

&lt;p&gt;This was the most interesting result of the whole project. I applied the same frozen-VGG16 setup, but added image augmentation (rotation, flips, zoom) during training.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;93% train / 92% val accuracy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look closely - train accuracy actually went &lt;em&gt;down&lt;/em&gt; compared to the previous approach (98% → 93%), but validation accuracy went &lt;em&gt;up&lt;/em&gt; (89% → 92%), and the train-val gap nearly disappeared entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2o5kz1hvxy1gcvqah2b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2o5kz1hvxy1gcvqah2b.png" alt=" " width="556" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the clearest illustration I've seen of the difference between memorization and generalization. Augmentation forced the model to work harder on each training example (since it never sees the exact same image twice), which cost it some raw training accuracy - but that trade bought real generalization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transfer Learning Approach 3: Fine-tuning
&lt;/h3&gt;

&lt;p&gt;For the final experiment, I unfroze VGG16's last convolutional block (block5) and trained it jointly with the dense head, using a much lower learning rate than before.&lt;/p&gt;

&lt;p&gt;Two details mattered a lot here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I only unfroze block5 &lt;em&gt;after&lt;/em&gt; the dense head was already well-trained from the earlier approaches. Unfreezing pretrained layers before the head is trained would send large, noisy gradients backward and disrupt the pretrained weights - essentially undoing the benefit of transfer learning.&lt;/li&gt;
&lt;li&gt;The low learning rate is deliberate. Fine-tuning pretrained weights with a normal learning rate risks catastrophic forgetting - wiping out the general knowledge the model already had.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: &lt;strong&gt;99% train / 95% val accuracy&lt;/strong&gt; - the best of all four experiments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkh9lsz58lmmp243kg8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkh9lsz58lmmp243kg8x.png" alt=" " width="556" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Train Acc&lt;/th&gt;
&lt;th&gt;Val Acc&lt;/th&gt;
&lt;th&gt;Key takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom CNN (dropout + batch norm)&lt;/td&gt;
&lt;td&gt;From scratch&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;Overfitting still visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VGG16 Feature Extraction&lt;/td&gt;
&lt;td&gt;Transfer learning&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;td&gt;Big jump, but wider train-val gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature Extraction + Data Augmentation&lt;/td&gt;
&lt;td&gt;Transfer learning&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;Overfitting nearly eliminated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fine-tuning (unfreezing block5)&lt;/td&gt;
&lt;td&gt;Transfer learning&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;Best result overall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I'd try next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Swap VGG16 for a more modern backbone (ResNet50, EfficientNetB0) and compare&lt;/li&gt;
&lt;li&gt;Test the same pipeline with far less data, to see transfer learning's advantage more starkly in a genuinely low-data setting&lt;/li&gt;
&lt;li&gt;Try discriminative learning rates - smaller LR for early unfrozen layers, larger for later ones - instead of one flat rate during fine-tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;All four notebooks, plus the results and graphs above, are on GitHub:&lt;br&gt;
👉 &lt;a href="https://github.com/Ishitag04/transfer-learning-cats-vs-dogs" rel="noopener noreferrer"&gt;transfer-learning-cats-vs-dogs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're learning deep learning too, I'd genuinely love to hear what you'd try differently - drop a comment below.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
