<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Louis Develle on Medium]]></title>
        <description><![CDATA[Stories by Louis Develle on Medium]]></description>
        <link>https://medium.com/@develle.louis?source=rss-94fda5ceaf85------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*2GeLjNh1yyzVrVXvVl93Nw.png</url>
            <title>Stories by Louis Develle on Medium</title>
            <link>https://medium.com/@develle.louis?source=rss-94fda5ceaf85------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 10:16:32 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@develle.louis/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Stop Guessing: A Systematic Methodology for Tuning Deep Learning Models]]></title>
            <link>https://medium.com/heuritech/stop-guessing-a-systematic-methodology-for-tuning-deep-learning-models-b3ea18e7e7c6?source=rss-94fda5ceaf85------2</link>
            <guid isPermaLink="false">https://medium.com/p/b3ea18e7e7c6</guid>
            <category><![CDATA[deep-learning]]></category>
            <category><![CDATA[dinov3]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[hyperparameter-tuning]]></category>
            <category><![CDATA[fine-tuning]]></category>
            <dc:creator><![CDATA[Louis Develle]]></dc:creator>
            <pubDate>Wed, 27 May 2026 16:13:22 GMT</pubDate>
            <atom:updated>2026-05-31T22:52:01.404Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Turning Hyperparameter tuning from art into science, and beating our baseline by an average of 4.6% across 13 production tasks.</em></p><p>You’ve run <strong>47 hyperparameter sweeps</strong>. Each one takes 6 hours on 4 GPUs. Your validation metric has improved by <strong>0.4% in two weeks, </strong>and it may have been a lucky run.</p><p>Your experiment tracker has a graveyard of rejected ideas:</p><ul><li>Cosine annealing: <strong>no improvement</strong>.</li><li>Weight decay: <strong>hurts performance</strong>.</li><li>Muon: <strong>unstable</strong>.</li></ul><p>But you tested cosine annealing with a learning rate tuned for a constant schedule. You tested weight decay on SGD but never tried it with Adam. You tested Muon with Adam’s learning rate range. None of those ideas were bad. The method was, and most of them probably would have worked given a fair shot.</p><p>This article shows you how to give them one. You’ll learn how to separate the parameters you’re studying from the ones you need to control, how to explore search spaces efficiently with quasi-random sampling, and how to sequence your experiments so each round builds on the last instead of invalidating it.</p><p>This methodology draws on my years of experience — directing research teams at 42 AI’s research lab, and teaching at Paris 1 Sorbonne and Amsterdam Tech University — as well as sources I’ll link throughout, particularly the Google Deep Learning Tuning Playbook [1] and Goodfellow et al.’s debugging strategies [1b].</p><p>One piece of context before we dive in: this work was part of migrating our production image classifiers from TensorFlow to PyTorch. Our TensorFlow baseline was a formidable target: it encodes a decade of accumulated knowledge from researchers and engineers, some of whom are now at Mistral and DeepMind. So “beating the baseline” meant out-tuning years of refinement with a PyTorch stack built from the ground up. Across our 13 production tasks, this methodology did exactly that, with an average gain of 4.6%.</p><h3>In This Article</h3><ol><li><strong>Heuritech</strong>:<br>The company behind this work</li><li><strong>The Problem With Ad-Hoc Tuning: <br></strong>Why sequential one-at-a-time tuning produces biased conclusions</li><li><strong>Scientific vs. Nuisance Parameters</strong>:<br>The key insight that makes fair comparisons possible</li><li><strong>Beyond Grid Search: Quasi-Random Sampling</strong>:<br>Sobol sequences, and why not Optuna (yet)</li><li><strong>Foundations Before Tuning</strong>: <br>Metric, batch size, and compute budget</li><li><strong>The Ordering Rationale</strong>:<br>Why architecture → optimizer → scheduler → regularization</li><li><strong>The Iterative Loop</strong>:<br>Four phases of compounding gains on 13 production tasks</li><li><strong>The 5-Phase Experiment Workflow:<br></strong>From research question to reproducible results</li><li><strong>Results: From Matching TensorFlow to Beating It</strong>:<br>+4.6% average, up to +14.5% on individual tasks</li><li><strong>Making It Your Own</strong>:<br>The distilled framework</li></ol><h3>Heuritech</h3><p>A word about the system this work lives in:</p><p><a href="https://heuritech.com/">Heuritech</a>’s computer vision pipeline processes over 1.5 billion images sourced from 16 markets (US, Europe, Japan, and beyond) to help fashion &amp; sportswear brands forecast demand more accurately and reduce overproduction.</p><p>The AI stack runs deep: object detection models identify garments in the wild, a dozen image classifiers label their attributes (shoe type, fabric, silhouette…), and from these visual signals we extract over 2,000 individual trends that get combined combinatorially (red × skirt × midi length) and mapped across geographies to produce more than 200,000 time series that we forecast into the future using advanced ensembling methods. We serve inference through Triton for production-grade throughput, use VLMs for specialized detections where traditional classifiers fall short, and run active learning pipelines to target our annotation budget (500K to 1M newly labeled images per month) where it matters most.</p><p>This article focuses specifically on one piece of that system: retraining our image classifiers, the dozen models that turn raw detections into the attribute labels everything downstream depends on.</p><p>For context on the rest of the pipeline, the team has written extensively across both sides of the stack:</p><p><strong>On the vision side</strong> — directly upstream of this article — <a href="https://www.linkedin.com/in/paulmorelurl/">Paul Morel</a>’s <a href="https://medium.com/heuritech/a-social-media-image-is-worth-a-thousand-fashion-labels-72423e472587">A social media image is worth a thousand fashion labels…</a> walks through how we extract fashion attributes from social media images using deep learning, using metal-studs jackets as a worked example.</p><p><strong>On the forecasting side</strong> — what happens downstream once these labels feed the time series — <a href="https://www.linkedin.com/in/etienne-david-896777171/">Etienne David</a>’s three-part series:</p><ol><li><a href="https://medium.com/heuritech/is-fashion-predictable-9894d0861f1e">Is fashion predictable?</a> — the foundation: translating fashion dynamics into measurable time series.</li><li><a href="https://medium.com/heuritech/is-fashion-predictable-part-2-e66fd321d363">Is Fashion Predictable? From Basic to Accurate Forecasting Models</a> — applying statistical and deep learning forecasting models, with code.</li><li><a href="https://medium.com/heuritech/is-fashion-predictable-the-rise-of-foundation-models-44c7d12cd0ba">Is Fashion Predictable? The Rise of Foundation Models</a> — evaluating time series foundation models from Amazon, Microsoft, and IBM on fashion forecasting.</li></ol><h3>The Problem With Ad-Hoc Tuning</h3><p>Most hyperparameter tuning in practice looks like this:</p><p>1. Pick a parameter (usually learning rate)<br>2. Try a few values<br>3. Pick the best one<br>4. Move to the next parameter<br>5. Repeat until deadline</p><p>This approach has four fatal flaws:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ln8ccFphVC0MrHNeFPZ9OA.png" /></figure><p><strong>Confounded comparisons:</strong> Hyperparameters interact.<br>When you compare lr=1e-3 vs lr=1e-4 with weight decay (wd) fixed at wd=1e-5, you’re not comparing learning rates, you’re comparing:</p><ul><li>“lr=1e-3 with appropriate weight decay”</li><li>“lr=1e-4 with the wrong weight decay.”</li></ul><p>Each learning rate has its own optimal weight decay. A fair comparison requires tuning the parameters separately for each.</p><p><strong>Incomplete exploration.</strong> Finding a good run isn’t enough, you need to find where performance <em>degrades</em>. <br>If your best result sits at the edge of your search range, you haven’t found the optimum; you’ve found the boundary of where you looked. Keep expanding until the best value is surrounded by worse ones on all sides.</p><p><strong>Biased conclusions</strong>. If augmentation A gets 30 experiments and augmentation B gets 10, A may win simply because it was searched more thoroughly. Worse: if A’s sub-parameters are well-tuned but B’s aren’t, you’ll conclude A is better, when B might win with equal tuning budget. Fair comparison demands equal search effort across the options you’re evaluating.</p><p><strong>Wasted compute</strong>. Grid search tests every combination, including those in unproductive regions. Random search is better but still inefficient.</p><h3>Scientific vs. Nuisance Parameters: The Key Insight</h3><p>The single most important concept in systematic tuning is the distinction between <strong>scientific</strong> and <strong>nuisance</strong> parameters.</p><p><strong>Scientific parameters</strong> are what you’re trying to measure. They answer your research question directly. <br>Example: “What optimizer works best?” → The scientific parameter is optimizer and could take values sgd, adam, adamw and muon.</p><p><strong>Nuisance parameters</strong> must be tuned <em>per scientific parameter value</em> to ensure a fair comparison. When comparing optimizer types, the learning rate is one of the nuisance parameters: different optimizers need different optimal learning rates. If you hold it constant, you might not be fairly evaluating the optimizer types.</p><p>Critically, nuisance parameters <strong>don’t have to be the same</strong> across scientific parameter values. When comparing SGD vs Adam, SGD’s nuisance parameter is learning rate (range 1e-3 to 1e-1), while Adam’s nuisance parameters are learning_rate (range 1e-5 to 1e-3) <strong><em>and</em></strong> beta1 (range 0.8 to 0.99). Each scientific value gets its own nuisance search space, tailored to what makes sense for that option. This is what makes the comparison fair.</p><p>The same applies to scheduler comparison: a constant learning rate has no nuisance parameters beyond LR itself, while cosine annealing has warmup epochs, restart multiplier, and decay length — all nuisance parameters that must be tuned to give cosine annealing a fair shot.</p><p><strong>Fixed parameters</strong> are everything else, held constant to reduce the search space.</p><h4>The Common Mistake</h4><p>An easy mistake is to confuse nuisance parameters with scientific ones. <br>Here’s a real example. Suppose your research question is: <strong>“Which optimizer achieves the best validation metric: Muon or Adam?”</strong></p><p>That question already tells you everything about the parameter classification, but in practice an easy mistake is to reach for the parameter suspected to have the most impact and place it as scientific:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VtzXjFazytyey_-WnHF6Kw.png" /></figure><p>In this setup, you’re treating a sub-parameter as the thing under study, and burying the actual decision — which optimizer? — as something to hold constant. The right setup flips this: the optimizer <em>type</em> is what you’re measuring, and each option gets its own nuisance search space.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n7NZbYxfwwjEWIuCfJosdw.png" /></figure><p>Notice that the nuisance spaces are not just different values : they’re <strong>different parameters entirely</strong>. Muon doesn’t have a beta1, Adam doesn&#39;t have ns_steps.</p><p>Each optimizer gets tuned on its own terms:</p><ul><li><strong>Muon:</strong> <br>learning_rate ∈ <strong>[1e-3, 1e-1]</strong> ← search space is not shared<br>ns_steps ∈ [3, 10]<br>momentum ∈ [0.8, 0.95]</li><li><strong>Adam:<br></strong> learning_rate ∈ <strong>[1e-5, 1e-3]</strong> ← search space is not shared<br>beta1 ∈ [0.8, 0.99]<br>epsilon ∈ [1e-9, 1e-7]</li></ul><p><em>Notice that even for nuisance parameters sharing the same name, their search spaces do not have to be shared across scientific parameters.</em></p><p>Now, the instinct to study learning rate isn’t <em>wrong: </em>it’s <em>premature</em>. Learning rate is one of the highest-impact continuous hyperparameters, and wanting to understand it deeply is exactly the right reflex. But the sequential logic matters: first, settle the optimizer choice (where LR is a nuisance parameter that must be tuned per optimizer to ensure a fair comparison). Then, once the optimizer is locked, that same instinct to study learning rate becomes the <strong>next scientific question</strong> : you’re now comparing LR schedules, warmup strategies, and decay shapes. The parameter is promoted from nuisance to scientific, in the right order.</p><p>This distinction seems pedantic until you realize it prevents the most common source of bias in ML experiments: <em>unfairly comparing a well-tuned option against a poorly-tuned one.</em></p><h3>Beyond Grid Search: Quasi-Random Sampling</h3><p>Once you’ve defined your search space, how do you explore it efficiently?</p><p>To build intuition, consider a simple two-dimensional example: tuning two hyperparameters with a budget of just 16 experiments.</p><ul><li>One parameter has a high impact on our metric of interest (<a href="https://en.wikipedia.org/wiki/F-score">F1-score</a> in this example): a narrow optimal region that’s easy to miss.</li><li>The other has low impact : it matters, but not nearly as much.</li></ul><p><strong>Grid search</strong> places experiments on a rigid lattice. With 16 trials, you get a 4×4 grid, <strong>only 4 unique values per dimension</strong>. If the optimal region falls between two grid lines, every single experiment misses it. The heatmap tells the story: the grid’s best configuration scores 0.13, completely blind to the peak.</p><figure><img alt="Grid sampling: 16 points on a rigid 4×4 lattice, best = 0.13 — the grid straddles the optimum without ever hitting it" src="https://cdn-images-1.medium.com/max/1024/1*aV2_p0_q1nF4Uluc-zPEKQ.png" /></figure><p><strong>Random search</strong> breaks free from the lattice. <em>Bergstra and Bengio (2012)</em> showed that random search is more efficient than grid search precisely because it samples each dimension independently [2]. With 16 random trials, you get <strong>16 unique values per dimension</strong> instead of 4. But randomness comes with a cost: clusters and gaps. Some regions get over-sampled while others are ignored entirely. Here, a lucky draw lands near the peak — best = 0.48, but coverage is uneven, and another random seed might miss it entirely.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cS18p1cwzbvjFYXw2CNYwQ.png" /></figure><p><strong>Quasi-random sampling</strong> (Halton/Sobol sequences) combines the best of both worlds: the structural coverage of a grid with the dimensional independence of random search [3a]. Sobol sequences are designed to fill the space as uniformly as possible, <strong>no clusters, no gaps</strong>. Every new point is placed to maximally reduce the largest remaining hole in the coverage. The result: best = 0.51, with points distributed evenly across the entire space.</p><figure><img alt="Quasi-random (Sobol) sampling: 16 points with uniform coverage, best = 0.51 — consistent, unbiased exploration" src="https://cdn-images-1.medium.com/max/1024/1*D6dHaZ8r40QrTlGwDG-L3Q.png" /></figure><p>The marginal F1 plots on each figure make the mechanism visible. Look at the dots projected onto the high-impact parameter curve (top):</p><ul><li>Grid search gives you only 4 probes, none near the peak.</li><li>Random search gives you 16, but they cluster on the left.</li><li>Sobol gives you 16 evenly spaced probes, guaranteeing that at least one lands close to any narrow optimum.</li></ul><h4>Where It Really Matters: Higher Dimensions</h4><p>The two-dimensional case is illustrative. The real payoff comes in higher dimensions.</p><p>In a 5-dimensional search with a grid of 5 values per dimension, you need <strong>3,125 trials</strong>, but each dimension still only sees <strong>5 unique values</strong>. That’s 3,125 experiments to test the same 5 learning rates. <em>80% of your compute is spent re-confirming what the low-impact dimensions do.</em></p><p>Random search with 30 trials gives you 30 unique values per dimension : <strong>6× better coverage</strong> than the 3,125-point grid, <strong>at less than 1% of the cost</strong>.</p><p>Halton and Sobol sequences with 30 trials give you 30 unique values <em>with guaranteed uniformity</em>, <strong>no clusters, no gaps</strong>, reducing lucky-seed dependence. As dimensionality grows, the advantage of quasi-random over both grid and random search compounds dramatically.</p><p>Both SciPy’s Sobol and Halton samplers work the same way: generate uniform points in [0, 1], then scale to your parameter ranges. Here’s a minimal Sobol example with log-scaled learning rate and a categorical optimizer:</p><pre>from scipy.stats.qmc import Sobol<br>import numpy as np<br><br>sampler = Sobol(d=2, scramble=True)<br>points = sampler.random(n=32)  # Sobol works best with powers of 2<br><br>lr = 10 ** (np.log10(1e-5) + points[:, 0] * (np.log10(1e-2) - np.log10(1e-5)))<br>optimizer = np.array([&quot;sgd&quot;, &quot;adam&quot;, &quot;adamw&quot;])[<br>    (points[:, 1] * 3).astype(int).clip(max=2)<br>]<br># lr.shape = (32,)       → [2.1e-5, 7.4e-4, 3.1e-3, ...]<br># optimizer.shape = (32,) → [&quot;adam&quot;, &quot;sgd&quot;, &quot;adamw&quot;, ...]</pre><p>And the Halton equivalent, same interface, but accepts <strong>any sample size</strong> and is naturally extensible (add trials by continuing the index):</p><pre>from scipy.stats.qmc import Halton<br><br>sampler = Halton(d=2, scramble=True)<br>points = sampler.random(n=30)  # Halton: any n, not just powers of 2<br><br>lr = 10 ** (np.log10(1e-5) + points[:, 0] * (np.log10(1e-2) - np.log10(1e-5)))<br>optimizer = np.array([&quot;sgd&quot;, &quot;adam&quot;, &quot;adamw&quot;])[<br>    (points[:, 1] * 3).astype(int).clip(max=2)<br>]<br># lr.shape = (30,)       → [1.2e-3, 4.8e-5, 6.0e-4, ...]<br># optimizer.shape = (30,) → [&quot;sgd&quot;, &quot;adam&quot;, &quot;adamw&quot;, ...]</pre><p><strong>Sobol vs. Halton </strong>:</p><p>Sobol sequences (used above) have slightly better theoretical uniformity in high dimensions (measured by discrepancy [3d], how evenly points fill a space), but require power-of-2 sample sizes for optimal coverage.</p><p><a href="https://en.wikipedia.org/wiki/Sobol_sequence#/media/File:Sobol_sequence_2D.svg">Visual example of Sobol sampling, from Wikipedia</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*T1JyEfXaSkkMxl1E.png" /></figure><p>Halton sequences accept any sample size and are naturally extensible (you can add trials by continuing the index sequence, and new points automatically fill gaps in the existing coverage; however, regions added by widening the search space afterwards remain less explored).</p><p><a href="https://en.wikipedia.org/wiki/Halton_sequence">Visual example of Halton sampling, from Wikipedia</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4S4N5fVa46wTjrtM.png" /></figure><p>Both are vastly superior to grid or random search. If you anticipate extending a running experiment (widening a search range, adding more trials), Halton is the more practical choice.</p><p><a href="https://en.wikipedia.org/wiki/Sobol_sequence#/media/File:Pseudorandom_sequence_2D.svg">Visual example of Random sampling, from Wikipedia</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7yvkDOLpY1gAFidI.png" /></figure><p><strong>How many trials?</strong> <br>As a rule of thumb: <strong>5–30 trials per scientific parameter value</strong>, depending on the number of nuisance dimensions and the type of hyperparameter.</p><ul><li>With 1–2 nuisance parameters, 5–10 trials suffice.</li><li>With 4–5, you need 20–30.</li><li>Avoid tuning <strong>more than 5 hyperparameters simultaneously</strong>, above that, the space is too large for a reasonable trial budget to cover meaningfully (unless you are GPU rich!).</li></ul><p><strong>The type of parameter greatly impacts the number of trials needed</strong>. Continuous numeric parameters require the fewest trials (e.g. learning rate), then discrete numeric parameters (e.g. batch size, number of layers), and finally categorical parameters are the most costly — they will exceed the recommendations above as soon as they can take more than a couple of values.</p><p>In practice, <strong>we keep most experiments to 2–4 nuisance dimensions</strong>, and we try to exclude categorical parameters (or to treat them as scientific ones in later rounds of optimization).</p><h4>Why Not Optuna or Hyperopt?</h4><p>A natural question: if we’re trying to find the best configuration efficiently, why not use Bayesian optimization tools like Optuna[3b] or Hyperopt[3c]? They’re designed for exactly this : they model the objective function (our error metric) and focus trials on the most promising regions of each of our hyperparameters.</p><p>That’s precisely the problem. <strong>We’re not just trying to find the best configuration (yet) we’re trying to understand the system to direct our compute resources.</strong></p><p>Bayesian optimizers tend to bias exploration toward the first good results they encounter. After a few trials, they build a surrogate model and start exploiting : concentrating new trials near the current best. This is efficient for finding a single optimum, but it leaves large regions of the search space unexplored. You can get a great configuration, but you learn almost nothing about <em>why</em> it’s great, or what else might work.</p><p>Quasi-random search doesn’t bias. Every region gets equal coverage, which means:</p><p><strong>You can run all trials in parallel.</strong> Sobol generates every configuration upfront, so you can launch them all simultaneously across available GPUs. Bayesian optimization needs completed results to update its surrogate before sampling the next trial; you’re either waiting between runs or batching blindly, losing the adaptive advantage that justified using it in the first place.</p><p><strong>You can distinguish high-impact from low-impact parameters.</strong> When all configurations near lr=1e-3 perform well regardless of weight decay, you know weight decay doesn&#39;t matter, and stop wasting time on it. Biased sampling makes this kind of sensitivity analysis unreliable.</p><p><strong>You can extend a search range without starting over.</strong> If your best learning rate sits at the boundary, you need to explore further. With Sobol, you extend the range and add trials; all previous results remain valid data points. A Bayesian surrogate model built for the old range doesn’t transfer cleanly.</p><p><strong>You can promote nuisance parameters to scientific ones.</strong> Sometimes you discover mid-experiment that a nuisance parameter actually matters. Because quasi-random search explored it uniformly, your existing data is already an unbiased sample of its effect. Bayesian optimization concentrated its exploration wherever the objective was high; you can’t draw fair conclusions about independent effects from biased coverage.</p><p><strong>You build transferable knowledge.</strong> “Adam’s optimal LR for DINOv3[4a] backbones is broadly 1e-4 to 1e-3” is reusable across tasks and projects. “Optuna found lr=3.7e-4” is not.</p><figure><img alt="Quasi-random vs Bayesian optimization: understanding the system vs finding a single optimum" src="https://cdn-images-1.medium.com/max/1024/1*HRyWpa2RFjeNVGOcKyxvGQ.png" /></figure><p>Bayesian optimization has its place: once you understand your system, it’s powerful for squeezing out the last few points. But starting with it means optimizing a system you don’t understand, and that’s how you end up with fragile configurations that break when anything changes.</p><p>One notable exception, is when using a quasi-random sampler (like Sobol or Halton) specifically — for example, Optuna’s QMCSampler [4b]</p><h4>Search Space Boundaries: A Critical Diagnostic</h4><p>One diagnostic that’s easily missed in practice: always make your search space large enough to include clearly bad configurations at the edges. You want to see performance degrading at both boundaries: that’s how you know the optimum is enclosed, not just clipped by where you stopped looking.</p><blockquote><strong>If your best configuration is at the boundary of your search space, you haven’t found the best configuration — you’ve found the edge of where you looked.</strong></blockquote><p>This sounds obvious, but it’s an easy mistake to make : setting a learning rate range of [1e-4, 1e-3] and finding the best result at 1e-3. Is 1e-3 optimal, or would 2e-3 be better? You can’t know without expanding the range. Design search spaces to include the bad runs on purpose: they’re not wasted compute — they’re confirmation that you’ve explored far enough.</p><p>Sources: <br>[2] Bergstra &amp; Bengio, 2012 (JMLR) : <a href="https://www.jmlr.org/papers/v13/bergstra12a.html">https://www.jmlr.org/papers/v13/bergstra12a.html</a><br>[3a] SciPy QMC Documentation : <a href="https://docs.scipy.org/doc/scipy/reference/stats.qmc.html">https://docs.scipy.org/doc/scipy/reference/stats.qmc.html</a><br>[3b] Optuna : <a href="https://github.com/optuna/optuna">https://github.com/optuna/optuna</a><br>[3c] HyperOpt : <a href="https://github.com/hyperopt/hyperopt">https://github.com/hyperopt/hyperopt</a><br>[3d] Discrepancy : <a href="https://en.wikipedia.org/wiki/Low-discrepancy_sequence">https://en.wikipedia.org/wiki/Low-discrepancy_sequence</a><br>[4a] DINOv3 : <a href="https://arxiv.org/abs/2508.10104">https://arxiv.org/abs/2508.10104</a><br>[4b] Optuna QMCS <a href="https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.QMCSampler.html">https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.QMCSampler.html</a></p><h3>Foundations Before Tuning</h3><p>Before running a single experiment, three decisions shape everything that follows.</p><h4>Know Your Compute Budget</h4><p>Your compute budget shapes your experimental design. With 4 GPUs and 6-hour runs, you get ~20 experiments per week, enough for 2–3 nuisance parameters per round, not 5. This constraint is precisely why sequential tuning works: you can’t search a 10-parameter space on a realistic budget, but you <em>can</em> search three 3-parameter spaces in sequence.</p><h4>Choose a Single-Number Metric</h4><p>Whenever it’s possible to optimize for a single metric, do it. <br>A single target simplifies every decision in the tuning process: you always know whether a configuration is better or worse.</p><p>When you have multiple optimization targets, experiments become much harder to exploit. You’ll frequently find that improving one metric degrades another, making comparisons blurry and conclusions uncertain. Many experiments end up with ambiguous results that are hard to act on. A single number cuts through that ambiguity.</p><p>Your metric will depend on your task, but the principle is the same: <strong>collapse your evaluation to one number</strong> whenever the problem allows it. It’s not always possible, but when it is, it transforms the tuning process.</p><h4>Set Batch Size First</h4><p>Set batch size before anything else. Batch size is primarily a hardware efficiency decision, not a model quality decision.</p><p><strong>Choose the largest batch size that maximizes GPU utilization.</strong> <br>The effect of batch size on optimization dynamics (larger batches → smoother gradients → potentially different optima) can almost always be compensated by adjusting the learning rate downstream. This means batch size doesn’t need to participate in the hyperparameter search — <strong>it’s a fixed precondition</strong>.</p><p>The notable exception: tasks where batch size has semantic meaning, such as contrastive learning, where the batch defines the ratio of negative samples. In those cases, batch size is a scientific parameter. For standard classification, it’s not.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I0mGJ9IqF73Wp2_bmo2nmw.png" /></figure><h4><strong>The Ordering Rationale</strong></h4><p>The sequence in which you tune dimensions is not arbitrary : it’s chosen to minimize cascading retuning. The principle: <strong>tune the most globally impactful decisions first, and the most dependent decisions last.</strong></p><p>Why does this work? <em>Hutter et al. (ICML 2014)</em> showed through functional <a href="https://en.wikipedia.org/wiki/Analysis_of_variance">ANOVA</a> decomposition that even in high-dimensional HP spaces, most performance variance is explained by just a few parameters [4]. <em>Bergstra and Bengio (2012)</em> confirmed this with their effective dimensionality finding: for most tasks, only a small subset of hyperparameters truly matter, but <em>which</em> ones matter varies by task [2]. <em>Wang et al. (2025)</em> measured this directly on CNN image classification: architectural depth alone explained <strong>38.5%</strong> of performance variance, learning rate 22.8%, dropout 13.1%, batch size just 1.5% [5]. Their Grouped Sequential Optimization Strategy, tuning groups in decreasing importance order, achieved a <strong>32% reduction in total optimization time</strong> versus simultaneous search, with equivalent final accuracy.</p><p>The practical consequence is what we call <strong>cascading invalidation</strong>: changing a high-impact parameter shifts the optimal values of everything downstream, but not vice versa. Tuning in the right order means later decisions rarely invalidate earlier ones.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a80TywfjUbDskiHpALcfGg.png" /></figure><p><strong>Architecture first.</strong> Architecture determines the entire function class, computational budget, and memory footprint. Changing the backbone invalidates optimizer settings, scheduler parameters, and regularization, everything downstream must be re-tuned. This is why <em>Wang et al.</em> found it explains the most variance: it’s the most <em>global</em> decision. Our approach: try the largest model that fits on your GPU for each architecture family you’re considering, then scale down if needed. This gives each family its best shot.</p><p><strong>Optimizer second.</strong> The optimal learning rate for SGD (typically 1e-2 to 1e-1) differs from Adam’s (typically 1e-4 to 1e-3) by <strong>several orders of magnitude</strong> [1]. As <em>the Google Tuning Playbook states</em>: switching optimizers after tuning the learning rate wastes all prior LR work, the learning rate is effectively a different parameter for each optimizer. Fix the optimizer early, and everything you build on top stays valid.</p><p><strong>Scheduler third.</strong> Scheduler parameters (warmup length, decay rate, restart multiplier) are tightly coupled to the optimizer and learning rate; they’re essentially a more granular tuning of the learning dynamics. <em>Karpathy’s advice</em> is emphatic: disable learning rate decay entirely until you’ve established the base learning rate, because the best schedule is sensitive to nearly every other optimization choice [6]. <em>The Google Playbook</em> recommends starting with a constant LR, then adding cosine or linear decay only once the base rate is locked [1].</p><p><strong>Regularization last.</strong> Three reasons:</p><ul><li>First, regularization addresses the gap between training and validation metrics — you can only meaningfully measure that gap once architecture, optimizer, and scheduler are stable. Tuning regularization earlier means optimizing against a moving target.</li><li>Second, large learning rates themselves act as implicit regularizers through optimization noise <em>(Smith, 2018)</em> [7]. The optimal dropout and weight decay values therefore <em>depend</em> on your learning rate: tune regularization before LR is set, and your results are misleading.</li><li>Third, <em>Hernandez-Garcia &amp; König (2018)</em> showed that data augmentation is robust to other HP changes, while weight decay and dropout are brittle, they can dramatically hurt performance if not carefully tuned in the context of the final configuration [8]. Regularization is the most dependent dimension, which is exactly why it comes last.</li></ul><p>One interaction deserves special mention: <strong>learning rate × weight decay</strong>. <em>Loshchilov &amp; Hutter (2019)</em> proved that L2 regularization and weight decay are <em>not equivalent</em> for Adam, which motivated AdamW [9]. More subtly, with BatchNorm layers, weight decay’s primary effect is modulating the <strong>effective learning rate</strong> rather than providing traditional regularization [10]. This means weight decay tuned before the learning rate is set will produce doubly misleading results, it’s partially an LR modifier masquerading as a regularizer.</p><p>Sources: <br>[1] <a href="https://github.com/google-research/tuning_playbook">Google Research Deep Learning Tuning Playbook</a> · <br>[2] <a href="https://www.jmlr.org/papers/v13/bergstra12a.html">Bergstra &amp; Bengio, 2012 (JMLR)</a> · <br>[4] <a href="http://proceedings.mlr.press/v32/hutter14.pdf">Hutter, Hoos &amp; Leyton-Brown, 2014 (ICML)</a> · <br>[5] <a href="https://arxiv.org/abs/2503.05106">Wang et al., 2025, Grouped Sequential Optimization Strategy (arXiv:2503.05106)</a> · <br>[6] <a href="http://karpathy.github.io/2019/04/25/recipe/">Karpathy, A Recipe for Training Neural Networks</a> · <br>[7] <a href="https://arxiv.org/abs/1803.09820">Smith, 2018, A Disciplined Approach to Neural Network Hyper-Parameters (arXiv:1803.09820)</a> · <br>[8] <a href="https://arxiv.org/abs/1806.03852">Hernandez-Garcia &amp; König, 2018 (arXiv:1806.03852)</a> · <br>[9] <a href="https://openreview.net/forum?id=Bkg6RiCqY7">Loshchilov &amp; Hutter, 2019, Decoupled Weight Decay Regularization (ICLR)</a> · <br>[10] <a href="https://arxiv.org/abs/1706.05350">van Laarhoven, 2017, L2 Regularization versus Batch and Weight Normalization (arXiv:1706.05350)</a></p><h3>The Iterative Loop: Building Systematically</h3><p>You can’t tune everything at once, the search space is too large. A 5-dimensional search with continuous ranges requires orders of magnitude more trials than a 2-dimensional one. The solution is sequential: <strong>fix one dimension, tune the next, build on previous results</strong>.</p><p>The rest of this article applies that principle to a concrete case: Heuritech’s image classification pipeline. Our computer vision stack detects garments in social media images, then classifies their attributes through a set of hierarchical classifiers : one model identifies the type of garment (jacket, dress, top…), others label its texture, silhouette, pattern, color, and finer attributes like neckline shape or sleeve length. These predictions combine to define the thousands of fashion trends we track and forecast.</p><p>In total, we retrained <strong>13 production classifiers</strong> spanning diverse visual tasks: outerwear type, dress type, shoe type, bag model, gender, brand logo, fabric, and more. Each task has different class distributions, visual complexity, and sensitivity to hyperparameters, which is precisely what makes them a good stress test for a general-purpose methodology.</p><p>One crucial design choice: We ran this entire progression on a <strong>single dataset, </strong>outerwear classification. The goal was never to find the best configuration for outerwear specifically. It was to find a <strong>general-purpose configuration</strong> that would work out of the box across all 13 production tasks, without any per-task tuning (our compute budget couldn’t afford tuning all 13 datasets directly). This is a much harder constraint, and it influenced decisions throughout. The other 12 datasets were held out entirely: the methodology was blind to them until final evaluation.</p><p>All results below are reported in <strong>p@r60</strong> (precision at 60% recall): the precision our classifier achieves when recall is held at 60%. We need a recall floor for coverage, and we optimize precision above that floor : a single number, following the principle described earlier in this article.</p><p>As a reference point: DINOv3-ViTS16 with default PyTorch training parameters scored <strong>0.650 p@r60</strong> on our outerwear test set. Every phase below builds from there.</p><p>We followed a 4-phase progression (after the batch size and metric foundations described above), each phase building on the previous one’s best configuration:</p><h4>Phase 1: Backbone Selection → p@r60 = 0.676</h4><p><strong>Scientific parameter</strong>: backbone architecture. <br><strong>Nuisance</strong>: learning rate. <br><strong>Fixed</strong>: constant scheduler, SGD optimizer, no regularization.</p><p><strong>Result</strong>: DINOv3-ViTS16 selected as the foundation. Starting metric on our outerwear classification task: 0.676 p@r60, by far the best option among all the tested architectures!</p><h4>Phase 2: Optimizer Choice → p@r60 = 0.692 (+1.6%)</h4><p><strong>Scientific parameter</strong>: optimizer (SGD, Adam, AdamW, Muon). <br><strong>Nuisance</strong>: learning rate, betas, momentum, with different ranges per optimizer. <br><strong>Fixed</strong>: DINOv3, constant scheduler, no regularization.</p><p><strong>Result</strong>: Four optimizers, four different stories:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WFNa0HfEhWjsYiFY0R0WoA.png" /></figure><p>A quick note on experiment counts: the number of trials per optimizer is proportional to its nuisance dimensions, following the guidelines from the quasi-random sampling section. SGD has one nuisance parameter (learning rate), so it got fewer trials. Adam has three (learning rate, beta1, epsilon), so it required more trials to cover its larger search space. This isn’t unequal effort — it’s <em>appropriately scaled</em> effort. Each optimizer gets the budget its search space demands.</p><p><strong>For SGD</strong>, we ran 4 experiments because we only had one nuisance parameter. SGD achieved the highest single-configuration score, but it had a very narrow optimal region, the peak was sharp and easy to miss:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qqr5VUvGZtdslfhmD_6k9A.png" /></figure><p><strong>Adam</strong> had more nuisance parameters to tune, so 15 experiments were needed. It scored lower but had a broad, flat optimal region of good learning rates:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2uemjQ3bA1_ChcEO8geWow.png" /></figure><p>Since we needed a single configuration that works across 13 classification tasks (shoe type, bag model, dress type, gender, etc.), <strong>Adam’s robustness mattered more than SGD’s peak performance</strong>. A configuration that’s 2% worse but works across all tasks beats one that’s 2% better on one task but fails on others.</p><p>Adam sensitivity analysis:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1n7G2ZDLCnLBPBJMyVgmsQ.png" /></figure><p><strong>Recommended</strong>: Adam with lr=5e-4, beta1=0.86, epsilon=1e-3, beta2=0.999.</p><h4>Phase 3: Scheduler Choice → p@r60 = 0.722 (+3.0%)</h4><p><strong>Scientific parameter</strong>: scheduler type (constant, linear decay, cosine annealing). <br><strong>Nuisance</strong>: scheduler-specific parameters — constant LR has just the learning rate, while cosine annealing has warmup epochs, restart multiplier, decay length, and more. <br>With so many nuisance parameters, we increased the number of experiments to 30.<br><strong>Fixed</strong>: DINOv3, Adam.</p><p><strong>Result</strong>: Cosine annealing was the clear winner at 0.722 vs. constant LR at 0.699. The gain was not free, it required more epochs (training went from optimal at epoch 2–3 to optimal at epoch 10–20). But 3% relative improvement justified the additional compute (internally we mostly care about inference-time compute).</p><p>The first round of cosine experiments revealed an important diagnostic: the best configurations clustered at the boundary of our search space (especially for learning_rate), meaning we hadn’t explored far enough:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lEV7wBdte35EEPuYV60iBw.png" /></figure><p>After extending the search space and doing our 30 experiments, we had a clearer picture, though the full scatter is dense. Filtering to the top 8 runs (&gt;25% of our 30 experiments) makes the sweet-spot regions visible :</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Khn4E_oFfE2oMxERs3TjZw.png" /></figure><p>Recommended scheduler: <strong>Cosine Annealing</strong> with:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/844/1*VGQBXho7iU-DA459A8H2cQ.png" /></figure><h4>Phase 4: Regularization → p@r60 = 0.727 (+0.5%)</h4><p><strong>Scientific parameter</strong>: regularization strategy. <br><strong>Nuisance</strong>: dropout, weight decay, image size. <br><strong>Fixed</strong>: DINOv3, Adam, cosine annealing.</p><p><strong>Result</strong>: Data augmentation did not improve results for our tasks — the sweet spot was simply dropout=0.10, image_size=224, weight_decay=0. The key takeaway: <strong>not every addition of complexity helps.</strong> Regularization should be validated, not assumed.</p><p>Regularization comes last for a reason: it addresses the gap between training and validation metrics. You can only meaningfully measure and reduce that gap once everything else — architecture, optimizer, scheduler — is stable. Tuning regularization earlier would mean optimizing against a moving target.</p><h4>The Compound Effect</h4><p>Each phase’s improvement was modest individually: +1.5%, +3%, +0.5%. But <strong>they compounded</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1oPWTVUH7CGHa7NeTPN74w.png" /></figure><p>And that was just outerwear. On other tasks, the gains were larger: up to +14.5% on dress type classification.</p><h3>The 5-Phase Experiment Workflow</h3><p>The methodology above tells you <em>what</em> to tune. But in practice, even well-designed experiments fail: wrong YAML configs, unlogged parameters, GPU crashes wasting hours of compute. We developed a 5-phase workflow to make experiments reliable:</p><figure><img alt="The 5-phase experiment workflow: design, generate, verify, document, launch" src="https://cdn-images-1.medium.com/max/1024/1*C2E0GoBMkib4xdcM7zkt_g.png" /></figure><h4>Phase 1: Collaborative Design</h4><p>Start with a research question: “Does label smoothing improve DINOv3 accuracy?” Define the scientific parameter (label_smoothing, range 0.0–0.2), nuisance parameters (dropout, data augmentation), and fixed parameters (everything else). Research the literature for informed range choices.</p><h4>Phase 2: Configuration Generation</h4><p>A generation script creates all experiment configurations using quasi-random sampling. Critically, it also generates a <strong>TEST configuration</strong>: a single config with max_epochs=3, limit_train_batches=10, designed to validate the setup in minutes rather than hours.</p><h4>Phase 3: Pre-Launch Verification</h4><p>Before burning any real compute:</p><p>1. <strong>Verify hyperparameter logging:</strong> Run a verification script against the TEST config to confirm all scientific and nuisance parameters will appear in your experiment tracker. We use <a href="https://www.tensorflow.org/tensorboard">TensorBoard</a>’s hparams tab: it logs every hyperparameter alongside training metrics, making post-experiment analysis possible. If a parameter isn’t logged, you can’t analyze it.</p><p>2. <strong>Verify code state: </strong>git status and git log to ensure you’re running committed code. Nothing is worse than discovering a great result came from uncommitted changes you can’t reproduce.</p><p>3. <strong>Run the TEST config</strong>: Launch the TEST yaml on a single GPU. This catches YAML syntax errors, missing dataset paths, shape mismatches, and configuration bugs. Catching these before launching 30 full experiments saves days.</p><figure><img alt="Pre-launch verification gates: hparam logging, code state, and TEST run" src="https://cdn-images-1.medium.com/max/1024/1*OTx-CfWFSNNfsXVet8yHDg.png" /></figure><h4>Phase 4: Documentation</h4><p>Write the experiment hypothesis <strong>before</strong> seeing results. This prevents post-hoc rationalization, the tendency to explain whatever result you got as if you expected it.</p><p>Our experiment documentation template includes:</p><ul><li>Objective (the research question)</li><li>Hypothesis (what we expect and why)</li><li>Configuration (dataset, model, metric, trial count)</li><li>Parameter tables (scientific, nuisance, fixed — with ranges and rationale)</li></ul><h4>Phase 5: Launch and Monitor</h4><p>After TEST validation passes, launch the full experiment. Distribute across available GPUs, monitor training curves for early divergence, and document results while they’re fresh.</p><p>The discipline of this workflow is what makes systematic tuning practical. Without it, the friction of running rigorous experiments pushes teams back toward ad-hoc tuning.</p><h3>The Results: From Matching TensorFlow to Beating It</h3><p>After applying this methodology across our full production workload, here are the results. Each task is a different image classification problem evaluated on precision-at-recall-60.</p><p>The TF Baseline column is <em>not</em> default TensorFlow, it’s <strong>the best TensorFlow configuration our team had refined over years of production use</strong>, based on an <a href="https://arxiv.org/pdf/1610.02357">Xception backbone</a>. This is the bar we needed to clear:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S821FrarHJGzOUjpTmMmzA.png" /></figure><p>A few things to note:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oO8CDahuHRWR1JNtqMeK1A.png" /></figure><p><strong>Not every task improved :</strong> Brand logo and shoe type showed regressions of -2.9% and -1.8%. This is expected: the entire methodology was tuned on a single dataset and applied blindly to 12 others. <br>The fact that 11 out of 13 tasks improved (with an average gain of +4.6%) validates the general-purpose approach. But the two regressions point to the natural next step: now that we have well-characterized search spaces and a solid baseline configuration, we can run <strong>task-specific optimization</strong> on the underperforming tasks. <br>Data augmentation — which didn’t help on outerwear — may matter for brand logos, which have fundamentally different visual patterns. And this is precisely where Bayesian optimization earns its place: the exploration phase is done, the search spaces are mapped, and tools like Optuna can now squeeze out per-task gains within those spaces without risk of optimizing blindly.</p><p><strong>Remember the starting point</strong>. On gender classification, our TensorFlow baseline (Xception backbone, refined over years) scored 82.5%. A naive PyTorch port with the same Xception architecture and default parameters scored 70.5% — a 12pp gap that made the migration look like a regression.</p><p>The final result — 91.0% — came from two compounding improvements: switching to a DINOv3 backbone (the architecture phase), and systematically tuning the training configuration (the methodology phases). Neither alone would have gotten there. The architecture gave us a better starting point; the methodology ensured we actually found its optimal operating point.</p><p>This is the broader lesson: an architecture upgrade without proper tuning leaves performance on the table, and tuning without the right architecture has a lower ceiling. The methodology’s value is making sure you reach whatever ceiling your architecture allows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BDa1AAyURZY8EKHdI9vrfg.png" /></figure><p>A telling sign of how unexpected these results were: when first presented, the team suspected a measurement error. The improvements, particularly the 10pp jump on gender classification, seemed too large to be real. This is exactly why the phased verification methodology matters: every result was traceable to a documented experiment with logged hyperparameters, committed code, and reproducible configurations. <em>The skepticism was healthy; the evidence was airtight.</em></p><h3>Making It Your Own</h3><p>This methodology is framework-agnostic. The same principles apply whether you’re tuning PyTorch, TensorFlow, JAX, or anything else, and whether you’re recovering from a migration or starting from scratch.</p><p>Here’s the distilled framework:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uW1TcqB-qpX7uV95LkWbaA.png" /></figure><p>The tools we built (Halton sequence generators, YAML configuration scripts, hyperparameter verification scripts, experiment documentation templates) are not complex, and the heavy lifting still runs on standard PyTorch. The value isn’t in the tools, it’s in the discipline of using them consistently.</p><p>Sources: <br>[1] Google Research Deep Learning Tuning Playbook :https://github.com/google-research/tuning_playbook<br>[1b] Goodfellow, Bengio &amp; Courville, Deep Learning, Ch. 11.5: Debugging Strategies : https://www.deeplearningbook.org/</p><h3>The Bottom Line</h3><p>An experienced engineer’s intuition about learning rate might be right 80% of the time. But across 6 hyperparameters, 4 architectural choices, and 13 classification tasks, that 80% accuracy compounds to less than a coin flip. Systematic methodology beats intuition not because intuition is wrong, but because <strong>the investment in systematic methodology delivers compounding interest after each new experiment</strong>.</p><p>This approach gave us an average +4.6% over configurations our team had refined for years, not from a single insight, but from the compound effect of many small, rigorous, well-validated improvements. The two tasks that regressed (−2.9% on brand logos, −1.8% on shoe type) point to the natural next step: now that search spaces are mapped, Bayesian optimization can squeeze out per-task gains where the general-purpose config falls short.</p><p>Now it’s your turn to share: what’s your tuning methodology?</p><p>I’m particularly curious whether you’ve found techniques that transfer reliably across diverse tasks, the interaction between optimizer choice and task characteristics is something we’re still investigating.</p><p><em>Louis Develle is an ML Research Engineer at Heuritech, where he led the TensorFlow-to-PyTorch migration and designed the systematic tuning methodology described in this series.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b3ea18e7e7c6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heuritech/stop-guessing-a-systematic-methodology-for-tuning-deep-learning-models-b3ea18e7e7c6">Stop Guessing: A Systematic Methodology for Tuning Deep Learning Models</a> was originally published in <a href="https://medium.com/heuritech">Heuritech, a Luxurynsight Company</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>