<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://ariajin20.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://ariajin20.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-06-30T01:25:40+00:00</updated><id>https://ariajin20.github.io/feed.xml</id><title type="html">Floria Jin</title><subtitle>Personal website of Beixuan (Floria) Jin — junior AI researcher working on AI safety and alignment, causal ML, LLM, and Bayesian methods. </subtitle><entry><title type="html">Few-Shot Learning Notes on Food-101</title><link href="https://ariajin20.github.io/blog/2026/few-shot-food101-learning-note/" rel="alternate" type="text/html" title="Few-Shot Learning Notes on Food-101"/><published>2026-06-18T00:00:00+00:00</published><updated>2026-06-18T00:00:00+00:00</updated><id>https://ariajin20.github.io/blog/2026/few-shot-food101-learning-note</id><content type="html" xml:base="https://ariajin20.github.io/blog/2026/few-shot-food101-learning-note/"><![CDATA[<h1 id="few-shot-image-classification-on-food101--learning-note">Few-Shot Image Classification on Food101 — Learning Note</h1> <blockquote> <p><strong>Type:</strong> Project-learning note (paper/code reading &amp; line-by-line walkthrough) <strong>Source project:</strong> <a href="https://www.kaggle.com/code/adityakotari/few-shot-image-classification-on-food101">Few-Shot Image Classification on Food101 — Kaggle</a> <strong>About this note:</strong> This is my personal study note based on the tutorial above. The implementation code belongs to the original author; the structure, explanations, and the self-posed Q&amp;A throughout reflect my own understanding while working through it.</p> </blockquote> <hr/> <h2 id="1-what-is-few-shot-classification">1. What Is Few-Shot Classification?</h2> <ul> <li><strong>Dataset:</strong> a labeled <em>support set</em> + a <em>query set</em>.</li> <li><strong>Few-shot:</strong> the support set contains very few images per label (typically fewer than 10).</li> <li><strong>Notation — N-way K-shot:</strong> N different classes, K examples per class.</li> <li><strong>Classification method:</strong> <em>metric-based</em>.</li> </ul> <p><strong>The metric-based procedure:</strong></p> <ol> <li>Use a CNN to project both support and query images into a feature space.</li> <li>Classify query images by comparing them to the support images. If, in the feature space, an image is <em>closer</em> to class A than to class B, we guess it is A.</li> </ol> <p><strong>Two core challenges:</strong></p> <ol> <li><strong>Find a good feature space.</strong> A CNN is essentially a function that takes an image as input and outputs a <em>representation</em> (an embedding) of that image in a given feature space.</li> <li><strong>Find a good way to compare representations</strong> in that feature space — this is the job of <strong>Prototypical Networks</strong>.</li> </ol> <blockquote> <p><strong>Q: What is the relationship between a feature space and a representation?</strong></p> <p>The feature space comes first. It is <em>not</em> hand-designed; it is determined by the CNN’s architecture and parameters. The logic chain is:</p> <ul> <li>You design/choose a CNN architecture (e.g., the last layer outputs a 512-dim vector) → this <em>implicitly defines</em> a 512-dim feature space.</li> <li>You train the CNN (adjust its parameters) → this determines the <em>semantic structure</em> of that space, i.e., which regions mean what. How well it is trained directly determines the quality of the space.</li> <li>You feed an image into the trained CNN → it outputs a 512-dim vector → that vector is the image’s <em>representation</em> in this feature space.</li> </ul> <p><strong>CNN defines the space → training shapes the space → a representation is produced only after an image is fed in.</strong></p> <p>Analogy: the feature space is a <em>map</em>; the representation is the image’s <em>coordinates</em> on that map. The CNN’s job is to “locate” each image — to compute its latitude/longitude. Images of the same class should land in nearby positions; images of different classes should land far apart.</p> </blockquote> <hr/> <h2 id="2-code-walkthrough">2. Code Walkthrough</h2> <h3 id="21-introducing-the-prototypical-network">2.1 Introducing the Prototypical Network</h3> <p>Initialize <code class="language-plaintext highlighter-rouge">PrototypicalNetworks</code> with a <strong>backbone</strong>: a ResNet18 pretrained on ImageNet, with its head chopped off and replaced by a <code class="language-plaintext highlighter-rouge">Flatten</code> layer.</p> <blockquote> <p><strong>Q: What is the relationship between a backbone and a feature extractor?</strong></p> <p>Here they are the <em>same thing</em>, just named from different angles:</p> <ul> <li><strong>Feature extractor</strong> — the <em>functional</em> name: its job is to extract features from an image and output a feature vector.</li> <li><strong>Backbone</strong> — the <em>architectural</em> name: it is the model’s “spine” / main network, the core part.</li> </ul> <p>In this code, the backbone is a ResNet18 with its <strong>classification head removed</strong>. Originally, ResNet18’s last layer is a classification layer (e.g., outputting probabilities over 1000 classes); here that head is chopped off and replaced by a <code class="language-plaintext highlighter-rouge">Flatten</code> layer so the network directly outputs a 512-dim feature vector. It no longer classifies — it purely extracts features. So: we use a (headless) ResNet18 as the backbone, which <em>is</em> our feature extractor, turning images into 512-dim vectors. Prototypical Networks then take these vectors to compute prototypes and compare distances.</p> </blockquote> <h3 id="22-why-forward-takes-three-inputs">2.2 Why <code class="language-plaintext highlighter-rouge">forward()</code> Takes Three Inputs</h3> <p>The <code class="language-plaintext highlighter-rouge">forward</code> method here does not take a single input tensor — it takes <strong>three</strong>.</p> <p><code class="language-plaintext highlighter-rouge">forward</code> is the model’s <em>forward-pass</em> function: you feed data in, the model processes it and produces an output. In PyTorch, <strong>every model has a <code class="language-plaintext highlighter-rouge">forward()</code> method</strong> that defines how incoming data flows step by step to the output.</p> <p>The key contrast is between a normal classifier and a Prototypical Network:</p> <ul> <li><strong>Normal classifier <code class="language-plaintext highlighter-rouge">forward</code>:</strong> input one image → output a predicted class. Only one input needed.</li> <li><strong>Prototypical Network <code class="language-plaintext highlighter-rouge">forward</code>:</strong> needs three inputs — <ul> <li><code class="language-plaintext highlighter-rouge">support images</code></li> <li><code class="language-plaintext highlighter-rouge">support labels</code> (tells the model which image belongs to which class)</li> <li><code class="language-plaintext highlighter-rouge">query images</code> (the images to be classified)</li> </ul> </li> </ul> <p>Why three? Because the classification logic is “compare distances.” The model must first see the support images <em>and</em> their labels to compute each class’s prototype, before it can decide which class a query image belongs to. Without the support set, it has nothing to compare against. In other words, calling this model is not as simple as passing in a single image — you must also tell it “what the references are” (support images + labels) before it can predict on the query images.</p> <h3 id="23-the-model-line-by-line">2.3 The Model, Line by Line</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PrototypicalNetworks</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">backbone</span><span class="p">:</span> <span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">(</span><span class="n">PrototypicalNetworks</span><span class="p">,</span> <span class="n">self</span><span class="p">).</span><span class="nf">__init__</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="n">backbone</span> <span class="o">=</span> <span class="n">backbone</span>
</code></pre></div></div> <p>Defines a Prototypical Networks model that inherits from PyTorch’s <strong><code class="language-plaintext highlighter-rouge">nn.Module</code></strong>. Initialization does just one thing: store the passed-in backbone (the feature extractor). All later feature extraction relies on it.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">z_support</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">backbone</span><span class="p">.</span><span class="nf">forward</span><span class="p">(</span><span class="n">support_images</span><span class="p">)</span>
<span class="n">z_query</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">backbone</span><span class="p">.</span><span class="nf">forward</span><span class="p">(</span><span class="n">query_images</span><span class="p">)</span>
</code></pre></div></div> <p>Feed the support images and query images separately into the backbone, each producing <strong>feature vectors</strong>. For example, 5 input images → 5 vectors of 512 dims each. <code class="language-plaintext highlighter-rouge">z_support</code> and <code class="language-plaintext highlighter-rouge">z_query</code> are the two sets of embeddings.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">n_way</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="nf">unique</span><span class="p">(</span><span class="n">support_labels</span><span class="p">))</span>
<span class="n">z_proto</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">(</span>
    <span class="p">[</span>
        <span class="n">z_support</span><span class="p">[</span><span class="n">torch</span><span class="p">.</span><span class="nf">nonzero</span><span class="p">(</span><span class="n">support_labels</span> <span class="o">==</span> <span class="n">label</span><span class="p">)].</span><span class="nf">mean</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">label</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">n_way</span><span class="p">)</span>
    <span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div> <ul> <li><code class="language-plaintext highlighter-rouge">torch.unique(support_labels)</code> finds how many distinct classes are in the support set — e.g., 3 classes (pug, labrador, Saint-Bernard) → <code class="language-plaintext highlighter-rouge">n_way = 3</code>.</li> <li>For each class, <code class="language-plaintext highlighter-rouge">support_labels == label</code> selects the embeddings of all images in that class, and <code class="language-plaintext highlighter-rouge">.mean(0)</code> averages them to obtain that class’s <strong>prototype</strong>.</li> <li><code class="language-plaintext highlighter-rouge">torch.cat</code> stacks all class prototypes into a matrix. The final <code class="language-plaintext highlighter-rouge">z_proto</code> has shape <code class="language-plaintext highlighter-rouge">(n_way, 512)</code>, where each row is one class’s prototype vector.</li> </ul> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dists</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cdist</span><span class="p">(</span><span class="n">z_query</span><span class="p">,</span> <span class="n">z_proto</span><span class="p">)</span>
<span class="n">scores</span> <span class="o">=</span> <span class="o">-</span><span class="n">dists</span>
<span class="k">return</span> <span class="n">scores</span>
</code></pre></div></div> <ul> <li><code class="language-plaintext highlighter-rouge">torch.cdist</code> computes the Euclidean distance from each query embedding to each prototype, giving a distance matrix. E.g., 10 queries, 3 classes → <code class="language-plaintext highlighter-rouge">dists</code> is <code class="language-plaintext highlighter-rouge">(10, 3)</code>.</li> <li><code class="language-plaintext highlighter-rouge">scores = -dists</code>: negate the distances to turn them into scores. <strong>Smaller distance means more similar</strong>; after negation, a larger value means more similar — aligning with the usual “higher score is better” convention, so you can directly apply <code class="language-plaintext highlighter-rouge">softmax</code> or <code class="language-plaintext highlighter-rouge">argmax</code> afterward to pick the top-scoring class.</li> </ul> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">convolutional_network</span> <span class="o">=</span> <span class="nf">resnet18</span><span class="p">(</span><span class="n">pretrained</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">convolutional_network</span><span class="p">.</span><span class="n">fc</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="nc">Flatten</span><span class="p">()</span>
<span class="n">model</span> <span class="o">=</span> <span class="nc">PrototypicalNetworks</span><span class="p">(</span><span class="n">convolutional_network</span><span class="p">).</span><span class="nf">cuda</span><span class="p">()</span>
</code></pre></div></div> <ul> <li>Load a ResNet18 pretrained on ImageNet.</li> <li>Replace the classification head <code class="language-plaintext highlighter-rouge">fc</code> with <code class="language-plaintext highlighter-rouge">Flatten</code>, so the network outputs a 512-dim feature vector instead of probabilities over 1000 classes.</li> <li>Use this modified ResNet18 as the backbone to create the Prototypical Networks model; <code class="language-plaintext highlighter-rouge">.cuda()</code> moves the model to the GPU.</li> </ul> <hr/> <h2 id="3-reading-the-models-printed-structure">3. Reading the Model’s Printed Structure</h2> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(1): BasicBlock(
        (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (relu): ReLU(inplace=True)
        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
    (fc): Flatten(start_dim=1, end_dim=-1)
  )
)
</code></pre></div></div> <p>Reading the network structure:</p> <ul> <li><strong><code class="language-plaintext highlighter-rouge">BasicBlock</code></strong> — the basic building unit of ResNet18: the standard flow of conv → norm → activation → conv → norm. The <code class="language-plaintext highlighter-rouge">512</code> indicates this layer processes feature maps with 512 channels.</li> <li><strong><code class="language-plaintext highlighter-rouge">avgpool: AdaptiveAvgPool2d(output_size=(1, 1))</code></strong> — global average pooling. Regardless of the preceding feature map’s size, it compresses to <code class="language-plaintext highlighter-rouge">(512, 1, 1)</code> — one number per channel. This step turns a 2D feature map into a 512-dim vector.</li> <li><strong><code class="language-plaintext highlighter-rouge">fc: Flatten(start_dim=1, end_dim=-1)</code></strong> — this is the layer you swapped in. Originally it was a fully connected layer <code class="language-plaintext highlighter-rouge">Linear(512, 1000)</code> outputting scores over 1000 classes. Now it is <code class="language-plaintext highlighter-rouge">Flatten</code>, which merely reshapes <code class="language-plaintext highlighter-rouge">(512, 1, 1)</code> into <code class="language-plaintext highlighter-rouge">(512,)</code> without doing any computation.</li> </ul> <p><strong>How to verify the head was successfully chopped off?</strong> Look at the <code class="language-plaintext highlighter-rouge">fc</code> line. If it shows <code class="language-plaintext highlighter-rouge">Flatten</code> instead of <code class="language-plaintext highlighter-rouge">Linear(512, 1000)</code>, the classification head has been successfully replaced. The model now outputs a 512-dim feature vector — exactly the embedding Prototypical Networks need.</p> <hr/> <h2 id="4-building-a-dataloader-for-few-shot-tasks">4. Building a DataLoader for Few-Shot Tasks</h2> <blockquote> <p><strong>Problem:</strong> A standard PyTorch DataLoader feeds in batches of images <em>without</em> considering labels or whether an image belongs to the support set or the query set. Given this, two key operations are required:</p> <ul> <li>Images must be <strong>evenly distributed across a given number of classes</strong>.</li> <li>The batch must be <strong>split between support and query sets</strong>.</li> </ul> </blockquote> <p>A PyTorch DataLoader has three components, each with one job:</p> <ul> <li><strong>Sampler — “which ones to take?”</strong> Decides which data indices go into each batch. Here a custom sampler is used: first randomly pick <code class="language-plaintext highlighter-rouge">n_way</code> classes, then take <code class="language-plaintext highlighter-rouge">n_shot + n_query</code> images from each class, ensuring each class is evenly represented.</li> <li><strong>Dataset — “how to take them?”</strong> Given the indices from the Sampler, it actually loads the data: reads image files, applies preprocessing (cropping, normalization, etc.), and returns images and labels.</li> <li><strong>Collate Fn — “how to assemble them?”</strong> Reorganizes the fetched data: splits it into support and query groups, re-indexes labels, and packs everything into the format the model expects.</li> </ul> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">N_WAY</span> <span class="o">=</span> <span class="mi">5</span>  <span class="c1"># Number of classes in a task
</span><span class="n">N_SHOT</span> <span class="o">=</span> <span class="mi">5</span>  <span class="c1"># Number of images per class in the support set
</span><span class="n">N_QUERY</span> <span class="o">=</span> <span class="mi">10</span>  <span class="c1"># Number of images per class in the query set
</span><span class="n">N_EVALUATION_TASKS</span> <span class="o">=</span> <span class="mi">100</span>

<span class="n">test_set</span><span class="p">.</span><span class="n">get_labels</span> <span class="o">=</span> <span class="k">lambda</span><span class="p">:</span> <span class="n">test_set</span><span class="p">.</span><span class="n">_labels</span>

<span class="n">test_sampler</span> <span class="o">=</span> <span class="nc">TaskSampler</span><span class="p">(</span>
    <span class="n">test_set</span><span class="p">,</span> <span class="n">n_way</span><span class="o">=</span><span class="n">N_WAY</span><span class="p">,</span> <span class="n">n_shot</span><span class="o">=</span><span class="n">N_SHOT</span><span class="p">,</span> <span class="n">n_query</span><span class="o">=</span><span class="n">N_QUERY</span><span class="p">,</span> <span class="n">n_tasks</span><span class="o">=</span><span class="n">N_EVALUATION_TASKS</span>
<span class="p">)</span>

<span class="n">test_loader</span> <span class="o">=</span> <span class="nc">DataLoader</span><span class="p">(</span>
    <span class="n">test_set</span><span class="p">,</span>
    <span class="n">batch_sampler</span><span class="o">=</span><span class="n">test_sampler</span><span class="p">,</span>
    <span class="n">num_workers</span><span class="o">=</span><span class="mi">12</span><span class="p">,</span>
    <span class="n">pin_memory</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">collate_fn</span><span class="o">=</span><span class="n">test_sampler</span><span class="p">.</span><span class="n">episodic_collate_fn</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div> <p><strong>Built into PyTorch:</strong></p> <ul> <li><code class="language-plaintext highlighter-rouge">DataLoader</code> — PyTorch’s native data loader.</li> <li><code class="language-plaintext highlighter-rouge">num_workers=12</code> — PyTorch’s built-in multi-process loading parameter.</li> <li><code class="language-plaintext highlighter-rouge">pin_memory=True</code> — PyTorch’s built-in GPU-memory optimization parameter.</li> </ul> <p><strong>Custom:</strong></p> <ul> <li><code class="language-plaintext highlighter-rouge">TaskSampler</code> — a custom sampler that samples classes and images evenly according to the few-shot task requirements. PyTorch’s built-in samplers only grab randomly or sequentially; they don’t understand the “pick classes first, then pick images” logic.</li> <li><code class="language-plaintext highlighter-rouge">test_sampler.episodic_collate_fn</code> — a custom collate function attached to the <code class="language-plaintext highlighter-rouge">TaskSampler</code>. It splits a batch into support/query groups and re-indexes labels. PyTorch’s default <code class="language-plaintext highlighter-rouge">collate_fn</code> only stacks data into a tensor; it doesn’t know how to split.</li> <li><code class="language-plaintext highlighter-rouge">test_set.get_labels = lambda: test_set._labels</code> — temporarily adds a method to the dataset so the <code class="language-plaintext highlighter-rouge">TaskSampler</code> can access all image labels, letting it know which images belong to which class so it can sample by class.</li> </ul> <p><strong>Present but not really “custom”:</strong></p> <ul> <li><code class="language-plaintext highlighter-rouge">test_set</code> — the dataset object. Although it may be created with a library like easy-set, it follows PyTorch’s <code class="language-plaintext highlighter-rouge">Dataset</code> interface, so it isn’t custom logic.</li> <li><code class="language-plaintext highlighter-rouge">N_WAY, N_SHOT, N_QUERY, N_EVALUATION_TASKS</code> — just configuration numbers: 5 classes, 5 support images each, 10 query images each, 100 evaluation tasks total.</li> </ul> <p><strong>Takeaway:</strong> the <code class="language-plaintext highlighter-rouge">DataLoader</code> is PyTorch’s shell, but the Sampler and Collate Fn inside it are self-written — these two are what turn ordinary data loading into few-shot <em>task</em> loading.</p> <hr/> <h2 id="5-model-evaluation">5. Model Evaluation</h2> <h3 id="51-evaluate_on_one_task">5.1 <code class="language-plaintext highlighter-rouge">evaluate_on_one_task</code></h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">evaluate_on_one_task</span><span class="p">(</span>
    <span class="n">support_images</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span>
    <span class="n">support_labels</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span>
    <span class="n">query_images</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">]:</span>
</code></pre></div></div> <ul> <li>Function purpose: given one few-shot task’s support and query, return the predictions for the query.</li> <li>Three inputs: support images, support labels, query images.</li> <li>The <code class="language-plaintext highlighter-rouge">: torch.Tensor</code> is a Python <em>type hint</em>, indicating that the argument should be a <code class="language-plaintext highlighter-rouge">torch.Tensor</code> — a multi-dimensional array, similar to a NumPy <code class="language-plaintext highlighter-rouge">ndarray</code>.</li> </ul> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">y_pred</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">max</span><span class="p">(</span>
            <span class="nf">model</span><span class="p">(</span><span class="n">support_images</span><span class="p">.</span><span class="nf">cuda</span><span class="p">(),</span> <span class="n">support_labels</span><span class="p">.</span><span class="nf">cuda</span><span class="p">(),</span> <span class="n">query_images</span><span class="p">.</span><span class="nf">cuda</span><span class="p">())</span>
            <span class="p">.</span><span class="nf">detach</span><span class="p">()</span>
            <span class="p">.</span><span class="n">data</span><span class="p">,</span>
            <span class="mi">1</span><span class="p">,</span>
        <span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">y_pred</span>
</code></pre></div></div> <ul> <li><code class="language-plaintext highlighter-rouge">support_images.cuda(), support_labels.cuda(), query_images.cuda()</code> — move all three inputs to the GPU, since the model is on the GPU and the data must be there too in order to compute.</li> <li><code class="language-plaintext highlighter-rouge">model(...)</code> — feed into the Prototypical Networks; internally it extracts features → computes prototypes → computes distances → returns a score matrix of shape <code class="language-plaintext highlighter-rouge">(num_queries, num_classes)</code>, e.g., <code class="language-plaintext highlighter-rouge">(50, 5)</code>, where each row is one query’s scores over the 5 classes.</li> <li><code class="language-plaintext highlighter-rouge">.detach().data</code> — detach the result from the computation graph; evaluation needs no gradients, saving memory.</li> <li><code class="language-plaintext highlighter-rouge">torch.max(..., 1)</code> — take the max along dim 1 (the class dimension), returning a tuple <code class="language-plaintext highlighter-rouge">(max_value, index_of_max)</code>.</li> <li><code class="language-plaintext highlighter-rouge">[1]</code> — keep only the index, i.e., the predicted class id. E.g., if a query’s scores are <code class="language-plaintext highlighter-rouge">[-1.2, -0.3, -2.5, -4.0, -1.8]</code>, the max is <code class="language-plaintext highlighter-rouge">-0.3</code> at index 1, so the prediction is class 1.</li> <li>Finally returns <code class="language-plaintext highlighter-rouge">y_pred</code>, a tensor containing the predicted classes for all queries.</li> </ul> <blockquote> <p><strong>Q: What is <code class="language-plaintext highlighter-rouge">.detach().data</code>?</strong></p> <p>During computation, PyTorch by default records every operation, forming a <em>computation graph</em> so that gradients can be backpropagated and parameters updated during training. But at evaluation time we only want a prediction — no parameter updates — so this graph is unnecessary. <code class="language-plaintext highlighter-rouge">.detach().data</code> tells PyTorch: I only want the numeric values, drop the computation history so it doesn’t waste memory. There is already a <code class="language-plaintext highlighter-rouge">torch.no_grad()</code> outside, which serves a similar purpose (also not recording the graph), so adding <code class="language-plaintext highlighter-rouge">.detach().data</code> here is double insurance, ensuring <code class="language-plaintext highlighter-rouge">y_pred</code> is a clean, history-free pure-data tensor.</p> </blockquote> <h3 id="52-evaluate">5.2 <code class="language-plaintext highlighter-rouge">evaluate</code></h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">evaluate</span><span class="p">(</span><span class="n">data_loader</span><span class="p">:</span> <span class="n">DataLoader</span><span class="p">):</span>
    <span class="n">total_predictions</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">correct_predictions</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">f1_scores</span> <span class="o">=</span> <span class="p">[]</span>
</code></pre></div></div> <p>Purpose: iterate over all test tasks and compute overall accuracy and F1. First, initialize three counters.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span><span class="p">.</span><span class="nf">eval</span><span class="p">()</span>
    <span class="k">with</span> <span class="n">torch</span><span class="p">.</span><span class="nf">no_grad</span><span class="p">():</span>
</code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">model.eval()</code> switches the model to evaluation mode, so layers like BatchNorm and Dropout use evaluation behavior rather than training behavior. <code class="language-plaintext highlighter-rouge">torch.no_grad()</code> tells PyTorch not to record the computation graph, saving memory since evaluation needs no backpropagation.</p> <blockquote> <p><strong>Q: Are <code class="language-plaintext highlighter-rouge">model.eval()</code> and <code class="language-plaintext highlighter-rouge">torch.no_grad()</code> built-in or custom?</strong></p> <p>Both are built into PyTorch.</p> <ul> <li><code class="language-plaintext highlighter-rouge">model</code> is an object you created, but the <code class="language-plaintext highlighter-rouge">.eval()</code> method is <em>inherited</em> from <code class="language-plaintext highlighter-rouge">nn.Module</code>. Because <code class="language-plaintext highlighter-rouge">PrototypicalNetworks</code> inherits from <code class="language-plaintext highlighter-rouge">nn.Module</code>, it automatically has <code class="language-plaintext highlighter-rouge">.eval()</code> — no need to define it yourself.</li> <li><code class="language-plaintext highlighter-rouge">torch.no_grad()</code> is a framework-level tool; you call <code class="language-plaintext highlighter-rouge">torch.no_grad()</code> directly, independent of your own model definition.</li> </ul> </blockquote> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">episode_index</span><span class="p">,</span> <span class="p">(</span>
            <span class="n">support_images</span><span class="p">,</span>
            <span class="n">support_labels</span><span class="p">,</span>
            <span class="n">query_images</span><span class="p">,</span>
            <span class="n">query_labels</span><span class="p">,</span>
            <span class="n">class_ids</span><span class="p">,</span>
        <span class="p">)</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">data_loader</span><span class="p">):</span>
</code></pre></div></div> <p>Iterate over the DataLoader, taking one batch — i.e., one few-shot task — at a time. Unpack the five items mentioned earlier: support images, support labels, query images, query labels, and the true class mapping.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">y_pred</span> <span class="o">=</span> <span class="nf">evaluate_on_one_task</span><span class="p">(</span>
                <span class="n">support_images</span><span class="p">,</span> <span class="n">support_labels</span><span class="p">,</span> <span class="n">query_images</span><span class="p">,</span>
            <span class="p">)</span>
</code></pre></div></div> <p>Call the function above to get predictions for all queries in this task.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">total_predictions</span> <span class="o">+=</span> <span class="nf">len</span><span class="p">(</span><span class="n">query_labels</span><span class="p">)</span>
            <span class="n">correct_predictions</span> <span class="o">+=</span> <span class="p">(</span><span class="n">y_pred</span> <span class="o">==</span> <span class="n">query_labels</span><span class="p">.</span><span class="nf">cuda</span><span class="p">()).</span><span class="nf">sum</span><span class="p">().</span><span class="nf">item</span><span class="p">()</span>
            <span class="n">f1_scores</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="nf">f1_score</span><span class="p">(</span><span class="n">query_labels</span><span class="p">.</span><span class="nf">cpu</span><span class="p">(),</span> <span class="n">y_pred</span><span class="p">.</span><span class="nf">cpu</span><span class="p">(),</span> <span class="n">average</span><span class="o">=</span><span class="sh">'</span><span class="s">macro</span><span class="sh">'</span><span class="p">))</span>
</code></pre></div></div> <p>Three lines, one job each:</p> <ul> <li>Accumulate the total number of predictions.</li> <li><code class="language-plaintext highlighter-rouge">y_pred == query_labels.cuda()</code> compares predictions with true labels element-wise, producing a True/False tensor; <code class="language-plaintext highlighter-rouge">.sum()</code> counts the correct ones; <code class="language-plaintext highlighter-rouge">.item()</code> converts to a Python number, added to the correct count.</li> <li>Use sklearn’s <code class="language-plaintext highlighter-rouge">f1_score</code> to compute this task’s macro F1 (the average of per-class F1). Note that sklearn only handles CPU data, so <code class="language-plaintext highlighter-rouge">.cpu()</code> is needed to move the tensors back.</li> </ul> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">accuracy</span> <span class="o">=</span> <span class="p">(</span><span class="n">correct_predictions</span><span class="o">/</span><span class="n">total_predictions</span><span class="p">)</span>
    <span class="n">avg_f1</span> <span class="o">=</span> <span class="nf">sum</span><span class="p">(</span><span class="n">f1_scores</span><span class="p">)</span><span class="o">/</span><span class="nf">len</span><span class="p">(</span><span class="n">f1_scores</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span>
        <span class="sa">f</span><span class="sh">"</span><span class="s">Model tested on </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">data_loader</span><span class="p">)</span><span class="si">}</span><span class="s"> tasks. Accuracy: </span><span class="si">{</span><span class="n">accuracy</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="o">%</span><span class="si">}</span><span class="s">, F1: </span><span class="si">{</span><span class="n">avg_f1</span><span class="si">}</span><span class="sh">"</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">accuracy</span>
</code></pre></div></div> <p>After the loop, compute overall accuracy (correct / total) and average F1 (mean of all tasks’ F1), print the results, and return the accuracy.</p> <hr/> <h2 id="6-meta-training-meta-learning">6. Meta-Training (Meta-Learning)</h2> <p><strong>Definition:</strong> Meta-learning refers to a <em>training strategy</em>, not a change of model. (We keep the same model, so the weights stay pretrained on ImageNet.) Previously we used the pretrained ResNet directly, without extensive training on few-shot tasks. Now we will train this model on a large number of few-shot tasks so it learns <em>how to do few-shot classification</em> itself.</p> <p>Concretely: prepare 40,000 few-shot tasks, each one being “here are a few support images, predict which class the queries are.” The model repeatedly does these tasks; when it gets one wrong, it computes a loss and updates parameters (mainly the ResNet backbone’s parameters), so that the features it extracts become better suited to few-shot classification — embeddings of the same class get closer, and those of different classes get farther apart.</p> <p>That is why it is called <em>meta-learning</em>: ordinary learning learns “how to recognize cats and dogs,” while meta-learning learns <strong>“how to quickly learn to recognize new things from a few samples”</strong> — <em>learning to learn</em>.</p> <h3 id="61-loss--optimizer">6.1 Loss + Optimizer</h3> <ul> <li><strong>Loss:</strong> cross-entropy (predict the query-set labels based on information from the support set and the ground truth).</li> <li><strong>Optimizer:</strong> Adam.</li> <li><strong>Custom <code class="language-plaintext highlighter-rouge">fit</code> method:</strong> <ul> <li>Input: a classification task (support set + query set).</li> <li>Output: the predicted labels of the query set based on the support set.</li> </ul> </li> </ul> <p><strong>Meta-training loop:</strong> <code class="language-plaintext highlighter-rouge">fit</code> → loss → update model parameters.</p> <h3 id="62-implementation">6.2 Implementation</h3> <p><strong>a. Define <code class="language-plaintext highlighter-rouge">fit</code></strong></p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">fit</span><span class="p">(</span>
    <span class="n">support_images</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span>
    <span class="n">support_labels</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span>
    <span class="n">query_images</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span>
    <span class="n">query_labels</span><span class="p">:</span> <span class="n">torch</span><span class="p">.</span><span class="n">Tensor</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="n">optimizer</span><span class="p">.</span><span class="nf">zero_grad</span><span class="p">()</span>
    <span class="n">classification_scores</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span>
        <span class="n">support_images</span><span class="p">.</span><span class="nf">cuda</span><span class="p">(),</span> <span class="n">support_labels</span><span class="p">.</span><span class="nf">cuda</span><span class="p">(),</span> <span class="n">query_images</span><span class="p">.</span><span class="nf">cuda</span><span class="p">()</span>
    <span class="p">)</span>

    <span class="n">loss</span> <span class="o">=</span> <span class="nf">criterion</span><span class="p">(</span><span class="n">classification_scores</span><span class="p">,</span> <span class="n">query_labels</span><span class="p">.</span><span class="nf">cuda</span><span class="p">())</span>
    <span class="n">loss</span><span class="p">.</span><span class="nf">backward</span><span class="p">()</span>
    <span class="n">optimizer</span><span class="p">.</span><span class="nf">step</span><span class="p">()</span>

    <span class="k">return</span> <span class="n">loss</span><span class="p">.</span><span class="nf">item</span><span class="p">()</span>
</code></pre></div></div> <ul> <li><code class="language-plaintext highlighter-rouge">optimizer.zero_grad()</code> — zero out gradients. PyTorch <em>accumulates</em> gradients by default, so you must clear them before each training step to prevent the previous step’s gradients from affecting this one.</li> <li><code class="language-plaintext highlighter-rouge">classification_scores = model(...)</code> — forward pass: feed the data into the model to get the queries’ scores over each class. Same as in evaluation, but this time PyTorch <em>records</em> the computation graph because gradients will be computed next.</li> <li><code class="language-plaintext highlighter-rouge">loss = criterion(classification_scores, query_labels.cuda())</code> — compute the loss by comparing the model’s scores against the queries’ true labels. <code class="language-plaintext highlighter-rouge">criterion</code> is typically <code class="language-plaintext highlighter-rouge">CrossEntropyLoss</code>; the further the scores are from the correct answer, the larger the loss.</li> <li><code class="language-plaintext highlighter-rouge">loss.backward()</code> — backpropagation: trace back through the computation graph from the loss to compute each parameter’s gradient, i.e., “which direction and how much to adjust each parameter to reduce the loss.”</li> <li><code class="language-plaintext highlighter-rouge">optimizer.step()</code> — update parameters: using the computed gradients, actually adjust the backbone (ResNet) parameters so feature extraction improves.</li> <li><code class="language-plaintext highlighter-rouge">return loss.item()</code> — return this task’s loss as a number; <code class="language-plaintext highlighter-rouge">.item()</code> converts from tensor to a plain Python number, convenient for logging and printing.</li> </ul> <p><strong>Note:</strong> <code class="language-plaintext highlighter-rouge">evaluate_on_one_task</code> only predicts and does <em>not</em> update parameters; <code class="language-plaintext highlighter-rouge">fit</code> predicts <em>and</em> updates parameters based on the result. One is like taking an exam; the other is like doing practice problems and learning from the mistakes.</p> <p><strong>b. Run the parameter updates</strong></p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">log_update_frequency</span> <span class="o">=</span> <span class="mi">100</span>
<span class="n">acc_update_frequency</span> <span class="o">=</span> <span class="mi">500</span>
<span class="n">all_loss</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">all_acc</span> <span class="o">=</span> <span class="p">[]</span>
</code></pre></div></div> <p>Initialize configuration: update the loss shown on the progress bar every 100 tasks; measure accuracy every 500 tasks (though this is commented out below). The two lists record all losses and accuracies.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span><span class="p">.</span><span class="nf">train</span><span class="p">()</span>
</code></pre></div></div> <p>Switch the model to training mode — the opposite of the earlier <code class="language-plaintext highlighter-rouge">model.eval()</code>. Layers like BatchNorm and Dropout will use training behavior.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="nf">tqdm</span><span class="p">(</span><span class="nf">enumerate</span><span class="p">(</span><span class="n">train_loader</span><span class="p">),</span> <span class="n">total</span><span class="o">=</span><span class="nf">len</span><span class="p">(</span><span class="n">train_loader</span><span class="p">))</span> <span class="k">as</span> <span class="n">tqdm_train</span><span class="p">:</span>
</code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">tqdm</code> is a progress-bar library wrapped around the DataLoader so you can see training progress, e.g., “3000/40000 tasks done.”</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">episode_index</span><span class="p">,</span> <span class="p">(</span>
        <span class="n">support_images</span><span class="p">,</span>
        <span class="n">support_labels</span><span class="p">,</span>
        <span class="n">query_images</span><span class="p">,</span>
        <span class="n">query_labels</span><span class="p">,</span>
        <span class="n">_</span><span class="p">,</span>
    <span class="p">)</span> <span class="ow">in</span> <span class="n">tqdm_train</span><span class="p">:</span>
</code></pre></div></div> <p>Iterate over each few-shot task and unpack the five items. The last one <code class="language-plaintext highlighter-rouge">_</code> is the true class mapping, unused during training, so it is ignored with an underscore.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">loss_value</span> <span class="o">=</span> <span class="nf">fit</span><span class="p">(</span><span class="n">support_images</span><span class="p">,</span> <span class="n">support_labels</span><span class="p">,</span> <span class="n">query_images</span><span class="p">,</span> <span class="n">query_labels</span><span class="p">)</span>
        <span class="n">all_loss</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">loss_value</span><span class="p">)</span>
</code></pre></div></div> <p>For this task, call <code class="language-plaintext highlighter-rouge">fit</code> to do one forward pass + loss + backpropagation + parameter update, then record the returned loss value.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">episode_index</span> <span class="o">%</span> <span class="n">log_update_frequency</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="n">tqdm_train</span><span class="p">.</span><span class="nf">set_postfix</span><span class="p">(</span><span class="n">loss</span><span class="o">=</span><span class="nf">sliding_average</span><span class="p">(</span><span class="n">all_loss</span><span class="p">,</span> <span class="n">log_update_frequency</span><span class="p">))</span>
</code></pre></div></div> <p>Every 100 tasks, display the average loss over the last 100 tasks on the right side of the progress bar. <code class="language-plaintext highlighter-rouge">sliding_average</code> computes a moving average, which is more stable than a single loss reading and shows whether the loss is trending downward overall.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># if episode_index % acc_update_frequency == 0:
</span>        <span class="c1">#     all_acc.append(evaluate(test_loader))
</span>        <span class="c1">#     model.train()
</span></code></pre></div></div> <p>This block is commented out. It would have paused training every 500 tasks to run a test-set evaluation, then switched back to training mode and continued. It was likely commented out to save time. Note that <code class="language-plaintext highlighter-rouge">evaluate</code> calls <code class="language-plaintext highlighter-rouge">model.eval()</code> internally, so after evaluating you must call <code class="language-plaintext highlighter-rouge">model.train()</code> again to switch back.</p> <p><strong>On the relationship between <code class="language-plaintext highlighter-rouge">fit</code>, loss, and update:</strong> these three are <em>not</em> parallel — they are nested. The <code class="language-plaintext highlighter-rouge">fit</code> function does three things internally: forward pass to get scores; compute the loss (<code class="language-plaintext highlighter-rouge">criterion(classification_scores, query_labels.cuda())</code>); update parameters (<code class="language-plaintext highlighter-rouge">loss.backward()</code> + <code class="language-plaintext highlighter-rouge">optimizer.step()</code>). So this training loop simply calls <code class="language-plaintext highlighter-rouge">fit</code> repeatedly, and each <code class="language-plaintext highlighter-rouge">fit</code> completes one loss computation and one parameter update.</p> <p><strong>c. Plot the loss curve</strong></p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">window_width</span> <span class="o">=</span> <span class="mi">50</span>
<span class="n">cumsum_vec</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">cumsum</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">all_loss</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span>
<span class="n">ma_vec</span> <span class="o">=</span> <span class="p">(</span><span class="n">cumsum_vec</span><span class="p">[</span><span class="n">window_width</span><span class="p">:]</span> <span class="o">-</span> <span class="n">cumsum_vec</span><span class="p">[:</span><span class="o">-</span><span class="n">window_width</span><span class="p">])</span> <span class="o">/</span> <span class="n">window_width</span>
</code></pre></div></div> <p>These three lines compute a <em>moving average</em>. Because each task’s loss fluctuates a lot, plotting the raw loss curve would be very jagged and hard to read for a trend. So a window of 50 is used to smooth it: each point is the average of itself and the previous 49 points. This uses the cumulative-sum (<code class="language-plaintext highlighter-rouge">cumsum</code>) trick to compute the moving average quickly — the result is the same as averaging window by window, just faster.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plt</span><span class="p">.</span><span class="nf">title</span><span class="p">(</span><span class="sh">"</span><span class="s">Loss vs # Training Episodes</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">xlabel</span><span class="p">(</span><span class="sh">"</span><span class="s"># Training Episodes</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">ylabel</span><span class="p">(</span><span class="sh">"</span><span class="s">Loss</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">plot</span><span class="p">(</span><span class="n">ma_vec</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="sh">"</span><span class="s">green</span><span class="sh">"</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="nf">show</span><span class="p">()</span>
</code></pre></div></div> <p>Plot with matplotlib. The x-axis is the training task index, the y-axis is the loss, drawn as a green curve.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">evaluate</span><span class="p">(</span><span class="n">test_loader</span><span class="p">)</span>
</code></pre></div></div> <p><strong>Result:</strong></p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Model tested on 100 tasks. Accuracy: 89.92%, F1: 0.8966519882510688
0.8992
</code></pre></div></div>]]></content><author><name></name></author><category term="research-notes"/><category term="few-shot-learning"/><category term="machine-learning"/><category term="computer-vision"/><category term="food101"/><summary type="html"><![CDATA[A learning note on few-shot classification using the Food-101 dataset.]]></summary></entry><entry><title type="html">Another River</title><link href="https://ariajin20.github.io/blog/2026/another-river/" rel="alternate" type="text/html" title="Another River"/><published>2026-06-16T00:00:00+00:00</published><updated>2026-06-16T00:00:00+00:00</updated><id>https://ariajin20.github.io/blog/2026/another-river</id><content type="html" xml:base="https://ariajin20.github.io/blog/2026/another-river/"><![CDATA[<p>Academia is part of how I express myself.</p> <p>The road into academic life looks like it came about by chance. Before I was 22, I studied the social sciences. They taught me to connect my own point of view to the workings of society; pressures and feelings I couldn’t make sense of were finally given names, and stopped being a nameless dread. From 23 onward I began studying data science, statistics, and computer science—and this is where the whole thing started to feel questionable. I’m not critical enough to stand against all the social ills, not radical enough to tear down the frameworks, not clever enough to build a scientific vision of my own. But I’m stubborn enough; I love to express, I like to think; I’m slow enough not to fret about my age, lucky enough to have a family that supports me, and I happen to enjoy the game we call social competition.</p> <p>When Xiang Biao spoke about personal crisis, he said:</p> <blockquote> <p>It mostly comes down to not being able to get things written. You work on a project for years and you’re never satisfied with what you write, because you don’t have strong or deep ideas, you don’t have your own voice, and the writing wears you out. You keep at it, reaching for this framework and that one, this theory and that theory, and you sink into a painful state—you’ve already poured so much thought into it that you can’t give it up.</p> </blockquote> <p>This made me anxious, too. The theory of computer science is another river, and I want to follow his advice—to walk it for a while, to find my own voice, my own experience. It’s also why I built this blog: I hope to understand what my own line of thinking actually is—why I work on LLMs, why I work on AI. Perhaps my reflection and understanding are a gift the social sciences gave me, a gift that will carry me, flowing on, through this other river.</p> <p>As Hesse wrote:</p> <blockquote> <p>All the waters of the world are bound to meet again; the Arctic Ocean and the Nile mingle in the moisture of the clouds. This ancient, beautiful image makes this moment holy. Even in wandering, every road carries us home. … I am alone, and yet at ease. I ask for nothing but to be soaked through by the sun.</p> </blockquote> <hr/> <details> <summary>中文原文</summary> 学术是我个人表达的一部分。 走上学术的路看起来是随机的，我的 22 岁以前是学习社会科学的，社会科学帮助我将个人的视角与社会的运转联系起来，一些难以理解的压力与情绪得到了命名，不再是不可名状的恐惧。23 岁以后，我开始学习数据科学／统计／还有计算机，这个过程变得可疑，我不够批判去反对种种社会问题，不够激进去推翻框架，不够聪明去搭建起自己的科学理念，还好我足够执着，热爱表达，喜欢思考，足够迟钝去思考自己的年龄，足够幸运有支持自己的家人，并且喜欢名为社会竞争的游戏。 项飙在讨论个人危机的时候说到："主要就是东西写不出来。课题做了很多年，写得总是不满意，因为没有很强／很深厚的想法，没有自己的声音，写起来也很累。这样搞来搞去，想这个框架那个框架，这个理论那个理论，陷入一种很痛苦的状态，已经投入那么多精力去思考，又不能放弃。" 我因此也开始担忧，计算机的理论是另一条河流，我想按照他的建议走一走，找到自己的声音，自己的经验。这也是我建立个人博文系统的原因，希望我能明白我的思路是什么，我为什么做 LLM，为什么做 AI，或许我的反思和理解是社会科学带给我的礼物，而这份礼物也将带着我径流过另一条河流。 而就像黑塞所说："全世界的水都会重逢，北冰洋和尼罗河会在湿云中交融。这古老美丽的比喻让此刻变得神圣。即使漫游，每条路也都会带我们归家。……我独自一人，却很自在。我别无所求，只想被阳光晒透。" </details>]]></content><author><name></name></author><category term="essays"/><category term="reflection"/><category term="social-science"/><category term="data-science"/><summary type="html"><![CDATA[Why I left the social sciences for AI, and what I carried across.]]></summary></entry><entry><title type="html">Reading Notes on BlueCodeAgent</title><link href="https://ariajin20.github.io/blog/2026/bluecodeagent-notes/" rel="alternate" type="text/html" title="Reading Notes on BlueCodeAgent"/><published>2026-06-16T00:00:00+00:00</published><updated>2026-06-16T00:00:00+00:00</updated><id>https://ariajin20.github.io/blog/2026/bluecodeagent-notes</id><content type="html" xml:base="https://ariajin20.github.io/blog/2026/bluecodeagent-notes/"><![CDATA[<h1 id="bluecodeagent-reading-notes">BlueCodeAgent: Reading Notes</h1> <blockquote> <p><strong>Source:</strong> Chengquan Guo, Yuzhou Nie, Chulin Xie, Zinan Lin, Wenbo Guo, <strong>Bo Li</strong>. <em>BlueCodeAgent: A Blue Teaming Agent Enabled by Automated Red Teaming for CodeGen AI.</em> arXiv:2510.18131, 2025. <a href="https://arxiv.org/abs/2510.18131">[arXiv]</a> · <a href="https://openreview.net/forum?id=OPkWzU5Wz9">[OpenReview]</a> · <a href="https://www.microsoft.com/en-us/research/blog/bluecodeagent-a-blue-teaming-agent-enabled-by-automated-red-teaming-for-codegen-ai/">[Microsoft Research blog]</a></p> </blockquote> <h2 id="guiding-principles">Guiding Principles</h2> <ul> <li><strong>Read the structure for both signal and silence.</strong> Pay attention not only to what the paper explicitly states, but also to what it leaves unsaid.</li> <li><strong>Think outside the framework.</strong> A natural next step is to <em>escalate to a human expert</em>. If the future is one of human–AI coexistence, then the design itself must reserve a place for the human in the loop.</li> </ul> <h2 id="blue-team-and-red-team">Blue Team and Red Team</h2> <p>The work proceeds along <strong>two tracks</strong>: <em>benchmarks</em> and <em>methodology</em>.</p> <ul> <li><strong>Dataset:</strong> CWE (Common Weakness Enumeration)</li> </ul> <h3 id="problems-the-blue-team-faces">Problems the Blue Team faces</h3> <ol> <li>Difficulty in recognizing sophisticated harmful behavior, and in knowing how to actively resist it.</li> <li>Over-conservatism — classifying genuinely safe content as unsafe.</li> <li>Incomplete coverage in risk prediction.</li> </ol> <p><strong>Key insight:</strong> strong red-team knowledge can inform and strengthen the blue team.</p> <p><strong>Pain point:</strong> <em>manually defining large-scale, high-quality security principles is impractical.</em></p> <h3 id="the-four-contributions">The four contributions</h3> <ol> <li><strong>Diverse Red-Teaming Pipeline</strong> — integrates multiple strategies to synthesize red-teaming data for effective knowledge accumulation. <ul> <li>Open questions: How exactly is red-team data synthesized, and how is useful knowledge accumulated? How do we judge what counts as “useful”? How is accumulation performed, and can that process be optimized?</li> </ul> </li> <li><strong>Knowledge-Enhanced Blue Teaming</strong> — leverages the constitution derived from knowledge together with dynamic testing.</li> <li><strong>Principled-Level Defense and Nuanced-Level Analysis</strong> — demonstrates their complementary effects in vulnerable-code detection.</li> <li><strong>Generalization to Seen and Unseen Risks</strong> — the blue team can generalize across both seen and unseen risk categories.</li> </ol> <h3 id="target-risks-taxonomy">Target Risks Taxonomy</h3> <p>The risks BlueCodeAgent targets split into two levels — the <strong>input / textual level</strong> (risks in the instructions) and the <strong>output / code level</strong> (risks in the generated code). Each leaf category contrasts an <em>unsafe</em> case against its <em>safe</em> counterpart, which is what makes the detection task non-trivial: the defender must separate genuinely harmful inputs/outputs from superficially similar but benign ones.</p> <pre><code class="language-mermaid">flowchart TD
    TR[Target Risks]

    TR --&gt; IN[Input / Textual Level&lt;br/&gt;Risks in instructions]
    TR --&gt; OUT[Output / Code Level&lt;br/&gt;Risks in generated code]

    IN --&gt; BIAS[Bias Instructions]
    IN --&gt; MAL[Malicious Instructions]
    OUT --&gt; VC[Vulnerable Code]

    BIAS --&gt; BIAS_U[Unsafe&lt;br/&gt;Biased / unfair intent]
    BIAS --&gt; BIAS_S[Safe&lt;br/&gt;Normal coding requests]

    MAL --&gt; MAL_U[Unsafe&lt;br/&gt;Malware creation]
    MAL --&gt; MAL_S[Safe&lt;br/&gt;Normal coding requests]

    VC --&gt; VC_U[Unsafe&lt;br/&gt;CWE vulnerabilities]
    VC --&gt; VC_S[Safe&lt;br/&gt;CWE-repaired code]

    classDef top fill:#ede7f6,stroke:#7e57c2,color:#000;
    classDef input fill:#d7f0e4,stroke:#26a69a,color:#000;
    classDef output fill:#fbe3da,stroke:#e07a5f,color:#000;
    classDef unsafe fill:#fde4e4,stroke:#e53935,color:#000;
    classDef safe fill:#e6f4d7,stroke:#7cb342,color:#000;

    class TR top;
    class IN,BIAS,MAL input;
    class OUT,VC output;
    class BIAS_U,MAL_U,VC_U unsafe;
    class BIAS_S,MAL_S,VC_S safe;
</code></pre> <blockquote> <p><strong>Legend.</strong> Purple = top-level category · teal = input / textual-level risks · salmon = output / code-level risks · red = <em>unsafe</em> (harmful or vulnerable) · green = <em>safe</em> (normal or repaired).</p> </blockquote> <h2 id="how-the-selected-risks-are-evaluated">How the Selected Risks Are Evaluated</h2> <ol> <li><strong>Baselines</strong> — control groups for comparison.</li> <li><strong>Base LLMs</strong> — which foundation model the BlueAgent is built and tuned on.</li> <li><strong>Benchmarks</strong> — to test the BlueAgent’s capability. The evaluation uses in-house sets built from red-teaming (<code class="language-plaintext highlighter-rouge">BlueCodeEval</code>, plus <code class="language-plaintext highlighter-rouge">BlueCodeEval-PI</code> for prompt injection) alongside an external reference benchmark, <code class="language-plaintext highlighter-rouge">SecCodePLT</code>, which provides both insecure and secure code snippets.</li> <li><strong>Experiment Setup</strong> — the pipeline works as follows: <ul> <li>Feed in a harmful prompt.</li> <li>Using the embedded vector, retrieve the three most similar entries from <code class="language-plaintext highlighter-rouge">BlueCodeKnow</code> or <code class="language-plaintext highlighter-rouge">BlueCodeEval</code>.</li> <li>From these four items, use GPT-4o to summarize a new <em>constitution</em>.</li> <li>Use a Claude model as the <em>dynamic analyzer</em> to analyze the current input sample.</li> <li>Note: the dynamic analyzer does <strong>not</strong> internalize the constitution into the model parameters through training. Instead, at inference time the constitution is supplied to Claude as an in-context prompt, and Claude then analyzes the current sample according to this dynamically generated constitution.</li> </ul> </li> <li><strong>Metrics</strong> <ul> <li><strong>Precision</strong> — of the samples flagged as dangerous, how many are truly unsafe: <code class="language-plaintext highlighter-rouge">TP / (TP + FP)</code>.</li> <li><strong>Recall</strong> — of all the truly unsafe samples, how many does the model catch: <code class="language-plaintext highlighter-rouge">TP / (TP + FN)</code>.</li> <li><strong>F1</strong> — <code class="language-plaintext highlighter-rouge">2 · Precision · Recall / (Precision + Recall)</code>; a balanced measure that jointly accounts for precision and recall.</li> </ul> </li> </ol> <h2 id="results">Results</h2> <p>An easily overlooked detail: the distinction between <strong>seen</strong> and <strong>unseen</strong> risks.</p> <ul> <li><strong>Seen risks</strong> = risk categories that already appear in <code class="language-plaintext highlighter-rouge">BlueCodeKnow</code>.</li> <li><strong>Unseen risks</strong> = risk categories in the test set <code class="language-plaintext highlighter-rouge">BlueCodeEval</code> that do <strong>not</strong> overlap with the categories in <code class="language-plaintext highlighter-rouge">BlueCodeKnow</code>.</li> </ul> <p>Results are reported across <strong>four</strong> representative code-related tasks:</p> <ul> <li>Bias-instruction detection.</li> <li>Malicious-instruction detection.</li> <li>Vulnerable-code detection.</li> <li>Prompt-injection detection — evaluated on the <code class="language-plaintext highlighter-rouge">BlueCodeEval-PI</code> test set, whose prompt-injection cases are generated from red-teaming.</li> </ul> <p>BlueCodeAgent performs consistently well on both seen and unseen risks across these tasks.</p> <h2 id="ablation-study">Ablation Study</h2> <ul> <li> <p><strong>Sensitivity to Different Knowledge:</strong> compute the cosine similarity between the <em>category embeddings</em> of the seven test categories and the eight knowledge categories, and express the result via the Pearson coefficient:</p> <p><code class="language-plaintext highlighter-rouge">Pearson = corr(cosine similarity, F1 score)</code></p> <p>Interpretation: this measures how much each test category depends on the eight existing knowledge categories. The higher the coefficient, the stronger that dependence.</p> <ul> <li>The F1 of <strong>seen</strong> risks is higher than that of <strong>unseen</strong> risks.</li> <li>The <em>constitution</em> (which increases true positives (TP) and reduces false negatives (FN)) and <em>dynamic testing</em> (which reduces false positives (FP)) are <strong>complementary</strong>.</li> </ul> </li> </ul> <h2 id="future-directions-within-the-framework--and-beyond">Future Directions (within the framework — and beyond?)</h2> <ol> <li>Other categories of code-generation risks — explored via novel red-teaming strategies.</li> <li>Scaling BlueCodeAgent to the file and repository levels, which could further enhance its real-world utility.</li> <li>Mitigating risks in other modalities — text, image, video, and audio — as well as in multimodal applications.</li> </ol> <h2 id="a-direction-for-improvement-adding-a-risk-gate">A Direction for Improvement: Adding a Risk Gate</h2> <p>On the algorithmic side, I would add a <strong>risk gate</strong>. Static analysis — and especially LLM-assisted static analysis — inherently suffers from false negatives. This is precisely the motivation behind IRIS: traditional static analysis is constrained by hand-written specifications and limited contextual understanding, while an LLM on its own also struggles with whole-repository vulnerability reasoning, so the two are combined.</p> <p>The crucial point is that <strong>“no vulnerability found” should not be treated as a termination condition.</strong> A more reasonable approach is to introduce a risk gate:</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R ← RiskGate(T, S, C)

if S says safe and R = low and confidence(S) ≥ τ then
    return SAFE_LOW_RISK
else
    continue dynamic validation
</code></pre></div></div> <p>This design can borrow from the literature on SAST false-positive / false-negative reduction. For example, QASecClaw centers on the idea that a SAST tool first produces candidate vulnerabilities, and an LLM-based filter agent then judges true positives vs. false positives by reasoning over the code context. On the OWASP Benchmark v1.2, it raised Semgrep’s F1 from 78.39% to 90.93%, mainly by substantially cutting false positives.</p> <p>But QASecClaw also illustrates an important caveat: an LLM is well suited to <strong>triage / contextual filtering</strong>, not to being treated as an absolute oracle. Mapped onto this algorithm, “no vulnerability found” should therefore not end the process directly — it should instead be routed into continued dynamic validation.</p>]]></content><author><name></name></author><category term="research-notes"/><category term="ai-safety"/><category term="code-agent"/><category term="trustworthy-ai"/><summary type="html"><![CDATA[A reading note on BlueCodeAgent and possible optimization directions.]]></summary></entry></feed>