<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://sabalanya.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://sabalanya.github.io/" rel="alternate" type="text/html" /><updated>2026-10-18T10:13:40+00:00</updated><id>https://sabalanya.github.io/feed.xml</id><title type="html">Sergio Álvarez Balanyá</title><subtitle>Personal academic website of Sergio Álvarez Balanyá.</subtitle><author><name>Sergio Álvarez Balanyá</name></author><entry><title type="html">Learnings from Flow Matching, Part 1</title><link href="https://sabalanya.github.io/blog/flow-matching-p1/" rel="alternate" type="text/html" title="Learnings from Flow Matching, Part 1" /><published>2026-10-17T00:00:00+00:00</published><updated>2026-10-17T00:00:00+00:00</updated><id>https://sabalanya.github.io/blog/flow-matching-p1</id><content type="html" xml:base="https://sabalanya.github.io/blog/flow-matching-p1/"><![CDATA[<p>This is the first part of a series of posts on learnings from implementing toy flow matching experiments. The goal is to share practical insights as a way for me to consolidate my understanding, and hopefully be useful to anyone getting their hands dirty with flow matching for the first time.</p>

<h2 id="brief-overview-of-flow-matching">Brief Overview of Flow Matching</h2>

<p>Flow matching (Lipman et al., 2022) is a simulation-free generative modeling framework. The core idea: learn a velocity field $v_\theta(x_t, t)$ that transports samples from a simple source $p_0$ (a Gaussian) to the data distribution $p_1$.</p>

<p><strong>Training.</strong> Given a pair $(x_0, x_1)$ with $x_0 \sim p_0$ and $x_1 \sim p_1$, form the linear interpolant $x_t = (1-t)\,x_0 + t\,x_1$ and regress the target velocity $\dot{x}_t = x_1 - x_0$ with MSE.</p>

<p><strong>Sampling.</strong> Integrate $\dot{x} = v_\theta(x, t)$ from $t=0$ to $t=1$, starting from $x_0 \sim p_0$.</p>

<p>That’s essentially it. The elegance is that training never requires simulating the ODE — you just sample random $(x_0, x_1, t)$ triplets and fit the velocity. For a thorough treatment see the original paper, <em>Flow Matching for Generative Modeling</em> (Lipman et al., 2022).</p>

<p>The experiments below were done on two settings: a 2D two-moons dataset and MNIST. The code for all experiments is available in the <a href="https://github.com/sabalanya/flow-matching">flow-matching repository</a>.</p>

<hr />

<h2 id="1-base-noise-distribution-variance-matters">1. Base Noise Distribution: Variance Matters</h2>

<p>The source distribution $p_0 = \mathcal{N}(\mu, \sigma^2 I)$ is a design choice, and it has a real effect on sample quality.</p>

<p>Intuitively, smaller $\sigma$ means shorter, nearly straight training trajectories that are easy to fit. But it also means noise samples cluster tightly together, which limits the diversity of generated outputs. Larger $\sigma$ gives the model more room to explore the space but produces longer, noisier paths that are harder to regress accurately.</p>

<p>In the MNIST experiments I swept $\sigma$ across an order of magnitude and found a clear pattern: training loss decreases monotonically as $\sigma$ shrinks, but sample quality degrades at the extremes in both directions.</p>

<p><img src="/assets/images/2026-10-17-flow-matching-p1/sigma_train_loss.png" alt="Training Loss vs Variance" /></p>

<p>The sweet spot in my experiments was around $\sigma \approx 0.05$–$0.1$. At $\sigma = 0.01$ the model fits the training distribution nearly perfectly but generates samples with little variety, essentially memorizing a tight cluster. At $\sigma = 1.0$ the paths are so noisy that the model struggles to converge at all. The right value will depend on your dataset and model capacity, but defaulting to $\sigma = 1$ (standard Gaussian) is likely suboptimal for structured data like images.</p>

<hr />

<h2 id="2-ode-solver-the-midpoint-method-pays-off">2. ODE Solver: The Midpoint Method Pays Off</h2>

<p>Sampling requires integrating the learned ODE from $t=0$ to $t=1$. The simplest choice is the Euler method, which takes $n$ steps of size $1/n$:</p>

\[x_{t + \Delta t} = x_t + \Delta t \cdot v_\theta(x_t, t)\]

<p>A better alternative is the midpoint method, which evaluates the velocity at the midpoint of each step:</p>

\[x_{t + \Delta t} = x_t + \Delta t \cdot v_\theta\!\left(x_t + \tfrac{\Delta t}{2} v_\theta(x_t, t),\; t + \tfrac{\Delta t}{2}\right)\]

<p>This costs 2 network evaluations per step instead of 1, but the accuracy improvement is dramatic. In the 2D experiments, achieving similar endpoint error required roughly 40 Euler steps versus only 5 midpoint steps, an 8× reduction in the number of steps for the same effective cost per step.</p>

<p><img src="/assets/images/2026-10-17-flow-matching-p1/euler_v_mid.png" alt="Midpoint vs Euler convergence" /></p>

<p>In code, the midpoint step slots in cleanly:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">step</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x_t</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">,</span> <span class="n">t_start</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">,</span> <span class="n">t_end</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tensor</span><span class="p">:</span>
    <span class="n">t_start</span> <span class="o">=</span> <span class="n">t_start</span><span class="p">.</span><span class="n">view</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">).</span><span class="n">expand</span><span class="p">(</span><span class="n">x_t</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">1</span><span class="p">)</span>
    <span class="n">dt</span> <span class="o">=</span> <span class="n">t_end</span> <span class="o">-</span> <span class="n">t_start</span>
    <span class="k">return</span> <span class="n">x_t</span> <span class="o">+</span> <span class="n">dt</span> <span class="o">*</span> <span class="bp">self</span><span class="p">(</span>
        <span class="n">t</span><span class="o">=</span><span class="n">t_start</span> <span class="o">+</span> <span class="n">dt</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span>
        <span class="n">x_t</span><span class="o">=</span><span class="n">x_t</span> <span class="o">+</span> <span class="bp">self</span><span class="p">(</span><span class="n">x_t</span><span class="o">=</span><span class="n">x_t</span><span class="p">,</span> <span class="n">t</span><span class="o">=</span><span class="n">t_start</span><span class="p">)</span> <span class="o">*</span> <span class="n">dt</span> <span class="o">/</span> <span class="mi">2</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>If you’re doing any FM inference and haven’t switched from Euler, this is a free win.</p>

<hr />

<h2 id="3-optimal-transport-coupling">3. Optimal Transport Coupling</h2>

<p>By default, training pairs each noise sample $x_0$ with a random data sample $x_1$, as you would get by simply shuffling a batch. The resulting interpolant trajectories $x_t$ can cross each other at arbitrary angles, and the velocity field has to be consistent with all of them simultaneously. This creates a harder learning problem.</p>

<p>Optimal transport (OT) coupling fixes this by solving a matching problem: given a batch of noise samples and a batch of data samples, find the assignment that minimizes total squared distance. In practice, this is just a linear sum assignment solved with scipy:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">scipy.optimize</span> <span class="kn">import</span> <span class="n">linear_sum_assignment</span>
<span class="kn">from</span> <span class="nn">scipy.spatial</span> <span class="kn">import</span> <span class="n">distance</span>

<span class="n">cost</span> <span class="o">=</span> <span class="n">distance</span><span class="p">.</span><span class="n">cdist</span><span class="p">(</span><span class="n">x_0</span><span class="p">.</span><span class="n">numpy</span><span class="p">(),</span> <span class="n">x_1</span><span class="p">.</span><span class="n">numpy</span><span class="p">())</span>
<span class="n">row_ind</span><span class="p">,</span> <span class="n">col_ind</span> <span class="o">=</span> <span class="n">linear_sum_assignment</span><span class="p">(</span><span class="n">cost</span><span class="p">)</span>
<span class="n">x_0</span><span class="p">,</span> <span class="n">x_1</span> <span class="o">=</span> <span class="n">x_0</span><span class="p">[</span><span class="n">row_ind</span><span class="p">],</span> <span class="n">x_1</span><span class="p">[</span><span class="n">col_ind</span><span class="p">]</span>
</code></pre></div></div>

<p>The result is that each $x_0$ gets paired with the nearest (in batch) $x_1$, producing shorter, more parallel trajectories.</p>

<p><img src="/assets/images/2026-10-17-flow-matching-p1/OT_example.png" alt="OT coupling vs random coupling in 2D" /></p>

<p>On the left, random coupling produces crossing trajectories. On the right, OT coupling produces much more parallel paths that are easier to regress. In the 2D experiments, OT training also converged to a lower final loss and did so faster. The downside is the added per-step cost of solving the assignment problem, which scales as $O(n^3)$. This is fine for moderate batch sizes, but something to watch at scale.</p>

<hr />

<h2 id="4-class-conditional-flow-matching">4. Class Conditional Flow Matching</h2>

<p>On MNIST, conditioning on the class label lets us generate digits from a specific class at sampling time. It also improves sample quality in general, since the model doesn’t have to explain all ten classes with a single velocity field.</p>

<h4 id="samples-generated-with-unconditioned-fm">Samples generated with unconditioned FM:</h4>
<p><img src="/assets/images/2026-10-17-flow-matching-p1/uncond_gen_samples.png" alt="Samples generated with unconditioned FM" /></p>

<h4 id="samples-generated-with-class-conditioned-fm">Samples generated with class-conditioned FM:</h4>
<p><img src="/assets/images/2026-10-17-flow-matching-p1/cond_gen_samples.png" alt="Samples generated with conditioned FM" /></p>

<p>I experimented with three different ways to inject class information.</p>

<h3 id="approach-1-concatenating-a-compact-class-embedding">Approach 1: Concatenating a compact class embedding</h3>

<p>The most straightforward approach: learn a small $d$-dimensional embedding per class and concatenate it with $x_t$ and $t$ as input to the network.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CondFlow</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="bp">self</span><span class="p">,</span> <span class="n">dim</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">784</span><span class="p">,</span> <span class="n">h</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">64</span><span class="p">,</span> <span class="n">c_dim</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">2</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">class_emb</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">c_dim</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">encoder</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">dim</span> <span class="o">+</span> <span class="n">c_dim</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">h</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">h</span><span class="p">),</span>
        <span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">decoder</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">h</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">dim</span><span class="p">),</span>
        <span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">t</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">,</span> <span class="n">c</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">,</span> <span class="n">x_t</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tensor</span><span class="p">:</span>
        <span class="n">c_emb</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">class_emb</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
        <span class="n">inp</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">cat</span><span class="p">((</span><span class="n">x_t</span><span class="p">,</span> <span class="n">c_emb</span><span class="p">,</span> <span class="n">t</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span>
        <span class="n">z</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">encoder</span><span class="p">(</span><span class="n">inp</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">decoder</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>
</code></pre></div></div>

<p>This works well. The class embedding is compact (just <code class="language-plaintext highlighter-rouge">c_dim</code> extra input dimensions), and the network has full flexibility to use it however it wants. At sampling time, fix the class label and integrate as usual.</p>

<h3 id="approach-2-additive-image-domain-embedding">Approach 2: Additive image-domain embedding</h3>

<p>A variation I tried: instead of a compact latent embedding, give each class a full 784-dimensional prototype $e_c$ that gets <em>added</em> to $x_t$ in pixel space before encoding.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CondFlow</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="bp">self</span><span class="p">,</span> <span class="n">dim</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">784</span><span class="p">,</span> <span class="n">h</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">64</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">class_emb</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">dim</span><span class="p">)</span>  <span class="c1"># 784-dim per class
</span>        <span class="p">...</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">t</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">,</span> <span class="n">c</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">,</span> <span class="n">x_t</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tensor</span><span class="p">:</span>
        <span class="n">c_emb</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">class_emb</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
        <span class="n">inp</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">cat</span><span class="p">((</span><span class="n">x_t</span> <span class="o">+</span> <span class="n">c_emb</span><span class="p">,</span> <span class="n">t</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span>
        <span class="n">z</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">encoder</span><span class="p">(</span><span class="n">inp</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">decoder</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>
</code></pre></div></div>

<p>The original motivation was interpretability: since each $e_c$ lives in the same 784-dimensional pixel space as the images, you could visualize it as a 28×28 image and inspect what the model learned as a class prototype.</p>

<h4 id="samples-generated-from-conditioned-fm">Samples generated from conditioned FM:</h4>

<p><img src="/assets/images/2026-10-17-flow-matching-p1/img_cond_samples.png" alt="Examples of image generated via image embeddings" width="600" /></p>

<p>In practice, however, the learned embeddings don’t look like digits at all.</p>

<p><img src="/assets/images/2026-10-17-flow-matching-p1/img_embeddings.png" alt="Learned image embeddings" width="600" /></p>

<p>They’re not noise either; they have some structure, but there’s no visual correspondence to the class they’re supposed to represent. This makes sense in hindsight: the embeddings are optimized to minimize the flow matching loss, not to be interpretable to a human. The gradient signal pushes each $e_c$ toward whatever shift of $x_t$ makes the velocity regression easiest, which has no reason to align with what a digit looks like.</p>

<p>The takeaway is that placing an embedding in a semantically meaningful space (pixel space) doesn’t make it semantically meaningful. If interpretability is the goal, you need to add an explicit objective for it.</p>

<h3 id="approach-3-conditioning-on-the-base-distribution">Approach 3: Conditioning on the base distribution</h3>

<p>Both approaches above inject class information into the velocity field. There’s a different angle: condition on the <em>source</em> distribution instead of the velocity field. Concretely, learn a class-specific mean $\mu_c$ for $p_0$ and sample $x_0 \sim \mathcal{N}(\mu_c, \sigma^2 I)$ at training and inference time, while keeping the velocity field itself unconditional.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CondBaseFlow</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="bp">self</span><span class="p">,</span> <span class="n">dim</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">784</span><span class="p">,</span> <span class="n">h</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">64</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">mu_emb</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">dim</span><span class="p">)</span>  <span class="c1"># one source mean per class
</span>        <span class="bp">self</span><span class="p">.</span><span class="n">encoder</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">dim</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">h</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">h</span><span class="p">),</span>
        <span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">decoder</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">h</span><span class="p">),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">dim</span><span class="p">),</span>
        <span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">t</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">,</span> <span class="n">x_t</span><span class="p">:</span> <span class="n">Tensor</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tensor</span><span class="p">:</span>
        <span class="n">inp</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">cat</span><span class="p">((</span><span class="n">x_t</span><span class="p">,</span> <span class="n">t</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span>
        <span class="n">z</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">encoder</span><span class="p">(</span><span class="n">inp</span><span class="p">)</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">decoder</span><span class="p">(</span><span class="n">z</span><span class="p">)</span>
</code></pre></div></div>

<p>Note the <code class="language-plaintext highlighter-rouge">forward</code> method takes no class label. The class comes in only when constructing the training path and the sampling starting point:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mu</span> <span class="o">=</span> <span class="n">cond_bflow</span><span class="p">.</span><span class="n">mu_emb</span><span class="p">(</span><span class="n">l</span><span class="p">)</span>                     <span class="c1"># class-specific source mean
</span><span class="n">x_0</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">randn_like</span><span class="p">(</span><span class="n">x_1</span><span class="p">)</span> <span class="o">*</span> <span class="n">SIGMA_NOISE</span> <span class="o">+</span> <span class="n">mu</span>
<span class="n">dx_t</span> <span class="o">=</span> <span class="n">x_1</span> <span class="o">-</span> <span class="n">x_0</span>
<span class="n">x_t</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">t</span><span class="p">)</span> <span class="o">*</span> <span class="n">x_0</span> <span class="o">+</span> <span class="n">t</span> <span class="o">*</span> <span class="n">x_1</span>
<span class="n">flow_pred</span> <span class="o">=</span> <span class="n">cond_bflow</span><span class="p">(</span><span class="n">x_t</span><span class="o">=</span><span class="n">x_t</span><span class="p">,</span> <span class="n">t</span><span class="o">=</span><span class="n">t</span><span class="p">)</span>
</code></pre></div></div>

<p>The idea is that if the source means $\mu_c$ are spread out (learned jointly with the flow), each region of the source space will be “claimed” by a different class via OT-like effects. The velocity field then learns to transport each region toward the right digits without needing an explicit label.</p>

<p>This actually works. The model generates coherent class-conditional samples from a completely unconditional velocity field.</p>

<h4 id="samples-generated-from-conditioned-base-distribution">Samples generated from conditioned base distribution:</h4>

<p><img src="/assets/images/2026-10-17-flow-matching-p1/base_dist_cond_samples.png" alt="Examples of image generated via base distribution conditioning" width="600" /></p>

<p>The tradeoff is that the class information is baked into the sampling procedure rather than the model, which makes it less flexible: you can’t easily interpolate between classes or use classifier-free guidance. But it’s a clean and surprisingly effective trick.</p>

<hr />

<h2 id="whats-next">What’s Next</h2>

<p>Part 2 will go further: flow inversion (running the ODE backwards to encode real images into noise), mode identification with multimodal source distributions, and time embeddings as an alternative to passing $t$ as a raw scalar. More on that when the experiments are done.</p>]]></content><author><name>Sergio Álvarez Balanyá</name></author><summary type="html"><![CDATA[First part of a series on learnings from implementing flow matching experiments.]]></summary></entry></feed>