<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>PyImageSearch</title>
	<atom:link href="https://pyimagesearch.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://pyimagesearch.com/</link>
	<description>You can master Computer Vision, Deep Learning, and OpenCV - PyImageSearch</description>
	<lastBuildDate>Mon, 14 Sep 2026 05:19:26 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.8</generator>
	<item>
		<title>DVC Pipelines for MLOps: Build a Reproducible ML Pipeline</title>
		<link>https://pyimagesearch.com/2026/09/14/dvc-pipelines-for-mlops-build-a-reproducible-ml-pipeline/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Data Version Control]]></category>
		<category><![CDATA[Machine Learning]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[data version control]]></category>
		<category><![CDATA[dataset versioning]]></category>
		<category><![CDATA[dvc]]></category>
		<category><![CDATA[dvc pipelines]]></category>
		<category><![CDATA[dvc repro]]></category>
		<category><![CDATA[dvc yaml]]></category>
		<category><![CDATA[machine learning pipeline]]></category>
		<category><![CDATA[mlops]]></category>
		<category><![CDATA[model versioning]]></category>
		<category><![CDATA[reproducible machine learning]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=55343</guid>

					<description><![CDATA[<p>Table of Contents DVC Pipelines for MLOps: Build a Reproducible ML Pipeline Introduction Project Setup Understanding the Pipeline Architecture Stage 1: Dataset Preprocessing Stage 2: Model Training and Versioning with DVC Stage 3: Model Evaluation and Metrics Tracking with DVC&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/09/14/dvc-pipelines-for-mlops-build-a-reproducible-ml-pipeline/">DVC Pipelines for MLOps: Build a Reproducible ML Pipeline</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-DVC-Pipelines-for-MLOps-Build-a-Reproducible-ML-Pipeline"><a rel="noopener" target="_blank" href="#h1-DVC-Pipelines-for-MLOps-Build-a-Reproducible-ML-Pipeline">DVC Pipelines for MLOps: Build a Reproducible ML Pipeline</a></li>
    <li id="TOC-h2-Introduction"><a rel="noopener" target="_blank" href="#h2-Introduction">Introduction</a></li>
    <li id="TOC-h2-Project-Setup"><a rel="noopener" target="_blank" href="#h2-Project-Setup">Project Setup</a></li>
    <li id="TOC-h2-Understanding-the-Pipeline-Architecture"><a rel="noopener" target="_blank" href="#h2-Understanding-the-Pipeline-Architecture">Understanding the Pipeline Architecture</a></li>
    <li id="TOC-h2-Stage-1-Dataset-Preprocessing"><a rel="noopener" target="_blank" href="#h2-Stage-1-Dataset-Preprocessing">Stage 1: Dataset Preprocessing</a></li>
    <li id="TOC-h2-Stage-2-Model-Training-and-Versioning-with-DVC"><a rel="noopener" target="_blank" href="#h2-Stage-2-Model-Training-and-Versioning-with-DVC">Stage 2: Model Training and Versioning with DVC</a></li>
    <li id="TOC-h2-Stage-3-Model-Evaluation-and-Metrics-Tracking-with-DVC"><a rel="noopener" target="_blank" href="#h2-Stage-3-Model-Evaluation-and-Metrics-Tracking-with-DVC">Stage 3: Model Evaluation and Metrics Tracking with DVC</a></li>
    <li id="TOC-h2-Running-and-Reproducing-the-Pipeline"><a rel="noopener" target="_blank" href="#h2-Running-and-Reproducing-the-Pipeline">Running and Reproducing the Pipeline</a></li>
    <li id="TOC-h2-Visualizing-and-Inspecting-Pipelines"><a rel="noopener" target="_blank" href="#h2-Visualizing-and-Inspecting-Pipelines">Visualizing and Inspecting Pipelines</a></li>
    <li id="TOC-h2-Extending-the-Pipeline-Optional-Enhancements"><a rel="noopener" target="_blank" href="#h2-Extending-the-Pipeline-Optional-Enhancements">Extending the Pipeline (Optional Enhancements)</a></li>
    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-DVC-Pipelines-for-MLOps-Build-a-Reproducible-ML-Pipeline"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-DVC-Pipelines-for-MLOps-Build-a-Reproducible-ML-Pipeline">DVC Pipelines for MLOps: Build a Reproducible ML Pipeline</a></h2>



<p>In this lesson you will learn how to build a fully reproducible machine learning pipeline using DVC, starting from raw data and moving through preprocessing, model training, and evaluation, all connected inside a clean and automated workflow that you can run with a single command.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured.png" target="_blank" rel=" noreferrer noopener"><img fetchpriority="high" decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured.png?lossy=2&strip=1&webp=1" alt="dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured.png" class="wp-image-55363"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-pipelines-mlops-build-reproducible-ml-pipeline-featured.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p>This lesson is the last in a 2-part series on <strong>Data and Model Versioning with DVC</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/bu1ya" target="_blank" rel="noreferrer noopener">DVC for MLOps: Versioning Your Data and Models the Right Way</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/v78x4" target="_blank" rel="noreferrer noopener">DVC Pipelines for MLOps: Build a Reproducible ML Pipeline</a></strong></em><strong> (this tutorial)</strong></li>
</ol>



<p><strong>To learn how to design, run, and reproduce a multi-stage pipeline using DVC’s powerful workflow engine,</strong><em><strong> just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Introduction"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Introduction">Introduction</a></h2>



<h3 class="wp-block-heading">What We Did in Lesson 1</h3>



<p>In the previous lesson, we learned how to use DVC to <strong>version datasets and model artifacts</strong> without polluting Git with large files.</p>



<p>You tracked a raw IMDb dataset, generated a dummy checkpoint, and pushed everything to a remote using DVC metadata files instead of storing actual data in Git.</p>



<p>We also covered how DVC caches files, how <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> metadata files point to real data in the cache, and why this workflow makes ML projects easier to share, update, and reproduce.</p>



<p>By the end of Lesson 1, you had a clean, Git-friendly approach to managing raw data and model outputs across a team.</p>



<h3 class="wp-block-heading">Why Pipelines Matter in MLOps</h3>



<p>Versioning data is useful, but <strong>real </strong><strong>machine learning (ML)</strong><strong> systems need reproducibility across entire workflows</strong>, not just individual files.</p>



<p>Once you have preprocessing scripts, training code, and evaluation logic, you need a way to connect them so they run in the right order and re-run only when necessary.</p>



<p>This is where <strong>DVC pipelines</strong> come in.</p>



<p>Pipelines let you define each step of your ML workflow as a stage with its own inputs, outputs, and logic, so DVC can trace dependencies and rebuild only the stages affected by changes.</p>



<p>In production teams, pipelines enforce <strong>repeatability</strong>, <strong>traceability</strong>, and <strong>automation</strong>. This structure allows any teammate or continuous integration (CI) job to rebuild your results exactly, even months later.</p>



<p>Put simply, pipelines transform ad hoc scripts into <strong>deterministic ML systems</strong>.</p>



<h3 class="wp-block-heading">What We Will Build in This Lesson (3-Stage Iris Pipeline)</h3>



<p>In this lesson, you will build a <strong>fully reproducible, 3-stage ML pipeline</strong> for the classic Iris dataset.</p>



<p>Each stage will be defined in <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code> and orchestrated with <code data-enlighter-language="python" class="EnlighterJSRAW">dvc repro</code>, making it easy to rebuild the workflow from scratch.</p>



<p>Here is the exact pipeline we will construct:</p>



<ul class="wp-block-list">
<li><strong>Preprocess:</strong> load <code data-enlighter-language="python" class="EnlighterJSRAW">iris.csv</code>, split into train/test, and save processed data</li>



<li><strong>Train:</strong> train a logistic regression classifier using the processed training data</li>



<li><strong>Evaluate:</strong> compute accuracy, generate a classification report, and save metrics and a human-readable report</li>
</ul>



<p>By the end of this lesson, you will have a production-style pipeline where any team member (or CI server) can:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="1">git clone &lt;repo>
pip install -r requirements.txt
dvc pull
dvc repro</pre>



<p>You can then reproduce <strong>the</strong><strong> outputs, metrics, and model</strong> without running each stage manually.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Project-Setup"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Project-Setup">Project Setup</a></h2>



<h3 class="wp-block-heading">Reviewing the Lesson 2 Repository Structure</h3>



<p>Before writing any pipeline logic, let us quickly walk through the repository so you know where every component lives.</p>



<p>This project uses a simple, production-friendly layout that separates <strong>raw data</strong>, <strong>scripts</strong>, <strong>outputs</strong>, and <strong>reports</strong>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="2">dvc-lesson2/
├── data/
│   └── raw/
│       └── iris.csv                  # Raw dataset
├── src/
│   ├── preprocess.py                 # Stage 1: Preprocessing
│   ├── train.py                      # Stage 2: Training
│   └── evaluate.py                   # Stage 3: Evaluation
├── outputs/
│   ├── processed/                    # Stage 1 output
│   ├── model/                        # Stage 2 output
│   └── metrics.json                  # Stage 3 output
├── reports/
│   └── accuracy.txt                  # Human-readable evaluation report
├── dvc.yaml                          # Pipeline definition
├── requirements.txt                  # Python dependencies
├── .gitignore                        # Files ignored by Git
└── .dvcignore                        # Files ignored by DVC</pre>



<p>The structure mirrors real ML projects. <strong>E</strong><strong>very stage writes outputs that become inputs for the next stage</strong>, and DVC tracks these relationships automatically through <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>.</p>



<h3 class="wp-block-heading">Installing Dependencies</h3>



<p>This lesson uses only 3 core packages: DVC, pandas, and scikit-learn.</p>



<p>Everything is lightweight so you can focus on learning pipelines, not juggling toolchains.</p>



<p>Install dependencies:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="3">pip install -r requirements.txt</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">requirements.txt</code> contains:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="4">dvc==3.48.4
pandas==2.1.4
scikit-learn==1.3.2</pre>



<p>Once installed, you have everything needed to preprocess data, train a model, evaluate it, and let DVC orchestrate the entire workflow.</p>



<h3 class="wp-block-heading">Initializing Git and DVC</h3>



<p>Just like Lesson 1, the pipeline project begins with Git and DVC initialization.</p>



<p>If this repository is fresh on your machine, run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="5">git init</pre>



<p>Now initialize DVC:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="6">dvc init</pre>



<p>This will create:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/</code>: internal DVC configuration</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.dvcignore</code>: paths ignored by DVC</li>



<li>Git hooks for DVC tracking</li>
</ul>



<p>Commit the setup:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="7">git add .dvc .dvcignore
git commit -m "Initialize DVC for pipeline project"</pre>



<p>At this point, the repository is ready for <strong>multi-stage pipelines</strong>, and DVC is prepared to track every dependency, output, and command we define in <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Understanding-the-Pipeline-Architecture"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Understanding-the-Pipeline-Architecture">Understanding the Pipeline Architecture</a></h2>



<p>A DVC pipeline is simply a <strong>series of stages that depend on each other</strong>. Each stage has a script, inputs, outputs, and a command. DVC connects those stages into a reproducible workflow that can be re-run automatically whenever something changes.</p>



<p>Our project implements a classic 3-stage ML workflow: preprocess → train → evaluate.</p>



<p>Let us break down how this works.</p>



<h3 class="wp-block-heading">The 3-Stage Iris Pipeline</h3>



<p>The Iris workflow in this project consists of <strong>3 connected stages</strong>, each defined in <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>.</p>



<h4 class="wp-block-heading">Stage 1: preprocess</h4>



<p>Script: <code data-enlighter-language="python" class="EnlighterJSRAW">src/preprocess.py</code></p>



<ul class="wp-block-list">
<li>Loads the raw <code data-enlighter-language="python" class="EnlighterJSRAW">iris.csv</code></li>



<li>Splits it into train/test sets</li>



<li>Saves cleaned data to <code data-enlighter-language="python" class="EnlighterJSRAW">outputs/processed/</code></li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="8">data/raw/iris.csv → outputs/processed/</pre>



<h4 class="wp-block-heading">Stage 2: train</h4>



<p>Script: <code data-enlighter-language="python" class="EnlighterJSRAW">src/train.py</code></p>



<ul class="wp-block-list">
<li>Reads processed training data</li>



<li>Fits a logistic regression model</li>



<li>Saves the model and label encoder to <code data-enlighter-language="python" class="EnlighterJSRAW">outputs/model/</code></li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="9">outputs/processed/ → outputs/model/</pre>



<h4 class="wp-block-heading">Stage 3: evaluate</h4>



<p>Script: <code data-enlighter-language="python" class="EnlighterJSRAW">src/evaluate.py</code></p>



<ul class="wp-block-list">
<li>Loads the trained model and test data</li>



<li>Computes metrics (accuracy and classification report)</li>



<li>Saves results to <code data-enlighter-language="python" class="EnlighterJSRAW">outputs/metrics.json</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">reports/accuracy.txt</code></li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="10">outputs/model/ → outputs/metrics.json, reports/accuracy.txt</pre>



<p>These 3 stages form a clean, linear ML pipeline. Data flows from the raw dataset to the processed dataset, model, and evaluation metrics.</p>



<h3 class="wp-block-heading">How DVC Uses dvc.yaml</h3>



<p>DVC connects all 3 stages through a single file: <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>.</p>



<p>This file tells DVC:</p>



<ul class="wp-block-list">
<li><strong>What command</strong> to run (<code data-enlighter-language="python" class="EnlighterJSRAW">cmd</code>)</li>



<li><strong>Which files</strong> a stage depends on (<code data-enlighter-language="python" class="EnlighterJSRAW">deps</code>)</li>



<li><strong>What outputs</strong> it produces (<code data-enlighter-language="python" class="EnlighterJSRAW">outs</code>)</li>
</ul>



<p>Your project’s <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code> looks like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="11">stages:
  preprocess:
    cmd: python src/preprocess.py
    deps:
      - data/raw/iris.csv
      - src/preprocess.py
    outs:
      - outputs/processed/

  train:
    cmd: python src/train.py
    deps:
      - outputs/processed/
      - src/train.py
    outs:
      - outputs/model/

  evaluate:
    cmd: python src/evaluate.py
    deps:
      - outputs/model/
      - src/evaluate.py
    outs:
      - outputs/metrics.json
      - reports/accuracy.txt</pre>



<p>In one place, DVC now knows:</p>



<ul class="wp-block-list">
<li>Changing <code data-enlighter-language="python" class="EnlighterJSRAW">iris.csv</code> forces the <strong>entire pipeline</strong> to re-run</li>



<li>Changing <code data-enlighter-language="python" class="EnlighterJSRAW">preprocess.py</code> forces <strong>preprocess → train → evaluate</strong> to re-run</li>



<li>Changing <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code> forces only <strong>train → evaluate</strong> to re-run</li>



<li>Changing <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate.py</code> forces only <strong>evaluate</strong> to re-run</li>
</ul>



<p>This is what makes ML pipelines reproducible and efficient.</p>



<h3 class="wp-block-heading">Reading the Directed Acyclic Graph</h3>



<p>DVC can visualize the workflow as a <strong>Directed Acyclic Graph (DAG)</strong>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="12">dvc dag</pre>



<p>You will see an ASCII diagram like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="13">          +-------------+
          | data/raw/... |
          +-------------+
                 *
                 *
                 *
          +-------------+
          | preprocess  |
          +-------------+
                 *
                 *
                 *
             +--------+
             | train  |
             +--------+
                 *
                 *
                 *
           +-------------+
           |  evaluate   |
           +-------------+</pre>



<p>This DAG tells you:</p>



<ul class="wp-block-list">
<li>The pipeline begins with <code data-enlighter-language="python" class="EnlighterJSRAW">iris.csv</code></li>



<li>Every stage depends on the previous one</li>



<li>Training will not run until preprocessing finishes</li>



<li>Evaluation will not run until a model exists</li>
</ul>



<p>It is a perfect, linear end-to-end representation of your ML system.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Stage-1-Dataset-Preprocessing"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Stage-1-Dataset-Preprocessing">Stage 1: Dataset Preprocessing</a></h2>



<p>The first stage in our pipeline handles dataset preparation. This includes loading the raw Iris comma-separated values (CSV) file, splitting it into training and test sets, and writing clean structured files into a dedicated output directory. DVC uses this stage as the foundation for all downstream work. If preprocessing changes, every downstream stage automatically reruns.</p>



<h3 class="wp-block-heading">Reviewing preprocess.py</h3>



<p>Your preprocessing script is located at:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="14">src/preprocess.py</pre>



<p>Here is what it does, step by step:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="15">df = pd.read_csv("data/raw/iris.csv")</pre>



<p>It loads the raw Iris dataset from <code data-enlighter-language="python" class="EnlighterJSRAW">data/raw/iris.csv</code>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="16">X = df.drop("species", axis=1)
y = df["species"]</pre>



<p>It separates features and target labels.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="17">X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)</pre>



<p>It creates an 80/20 train-test split while preserving class proportions (<code data-enlighter-language="python" class="EnlighterJSRAW">stratify=y</code>).</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="18">output_dir = "outputs/processed"
os.makedirs(output_dir, exist_ok=True)</pre>



<p>It prepares the output directory where DVC will store processed datasets.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="19">train_df.to_csv(os.path.join(output_dir, "train.csv"), index=False)
test_df.to_csv(os.path.join(output_dir, "test.csv"), index=False)</pre>



<p>Finally, the preprocessed data is saved as:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">outputs/processed/train.csv</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">outputs/processed/test.csv</code></li>
</ul>



<p>These are the cleaned, structured files the rest of the ML pipeline depends on.</p>



<h3 class="wp-block-heading">Inputs and Outputs</h3>



<h4 class="wp-block-heading">Inputs (DVC Dependencies)</h4>



<p>Listed under <code data-enlighter-language="python" class="EnlighterJSRAW">deps</code> in <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="20">deps:
  - data/raw/iris.csv
  - src/preprocess.py</pre>



<p>Meaning:</p>



<ul class="wp-block-list">
<li>Changing the raw dataset reruns Stage 1</li>



<li>Changing the script itself reruns Stage 1</li>
</ul>



<h4 class="wp-block-heading">Outputs (DVC Tracked Artifacts)</h4>



<p>Listed under <code data-enlighter-language="python" class="EnlighterJSRAW">outs</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="21">outs:
  - outputs/processed/</pre>



<p>This directory contains 2 files:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">train.csv</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">test.csv</code></li>
</ul>



<p>DVC tracks the entire directory as a single output artifact.</p>



<h3 class="wp-block-heading">How DVC Tracks Directory Outputs (outputs/processed/)</h3>



<p>DVC treats <code data-enlighter-language="python" class="EnlighterJSRAW">outputs/processed/</code> as one logical output, not just a folder.</p>



<p>When you run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="22">dvc repro</pre>



<p>DVC:</p>



<ul class="wp-block-list">
<li>Computes hashes of the directory contents</li>



<li>Stores processed files in <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code></li>



<li>Creates a reference entry in <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code></li>



<li>Recreates (or updates) the directory when needed</li>
</ul>



<p>DVC <strong>does not</strong> version each individual CSV separately. Instead, it versions the <strong>directory state</strong>, making preprocessing reproducible and lightweight.</p>



<p>If you open <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code>, you will see a section such as the following:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="23">outs:
  - path: outputs/processed
    md5: &lt;hash>
    size: &lt;bytes></pre>



<p>This hash represents the exact combination of <code data-enlighter-language="python" class="EnlighterJSRAW">train.csv</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">test.csv</code>.</p>



<h3 class="wp-block-heading">What Changes Trigger Re-Runs</h3>



<p>DVC re-runs the preprocessing stage only when necessary.</p>



<p>The following changes <strong>trigger Stage 1 to run again</strong>:</p>



<p><strong>Modify the raw dataset</strong></p>



<p>Example: editing <code data-enlighter-language="python" class="EnlighterJSRAW">data/raw/iris.csv</code></p>



<p><strong>Modify the preprocessing script</strong></p>



<p>Example: changing the test split from 0.2 to 0.3:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="24">test_size=0.3</pre>



<p>This triggers:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="25">preprocess → train → evaluate</pre>



<p>because downstream stages depend on processed data.</p>



<p><strong>Running dvc repro again without changes</strong></p>



<p>DVC outputs:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="26">Stage 'preprocess' didn't change, skipping</pre>



<p>This shows why DVC pipelines are efficient: no unnecessary computation.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Stage-2-Model-Training-and-Versioning-with-DVC"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Stage-2-Model-Training-and-Versioning-with-DVC">Stage 2: Model Training and Versioning with DVC</a></h2>



<p>The training stage consumes the preprocessed dataset from Stage 1, fits a model, and writes all model artifacts into a DVC-tracked output directory. This stage represents the “learning” portion of the pipeline, and any upstream change automatically forces retraining.</p>



<h3 class="wp-block-heading">Reviewing train.py</h3>



<p>Your training script lives at:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="27">src/train.py</pre>



<p>Here is what it does:</p>



<h4 class="wp-block-heading">Load processed training data</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="28">train_path = "outputs/processed/train.csv"
train_df = pd.read_csv(train_path)</pre>



<p>It reads the output from Stage 1, meaning preprocessing <strong>must</strong> complete successfully before training can run.</p>



<h4 class="wp-block-heading">Split Features and Labels</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="29">X_train = train_df.drop("species", axis=1)
y_train = train_df["species"]</pre>



<h4 class="wp-block-heading">Encode Target Labels</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="30">label_encoder = LabelEncoder()
y_train_encoded = label_encoder.fit_transform(y_train)</pre>



<p>The Iris dataset uses string labels (<code data-enlighter-language="python" class="EnlighterJSRAW">setosa</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">versicolor</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">virginica</code>).</p>



<p>The model expects numeric classes, so they are encoded.</p>



<h4 class="wp-block-heading">Train a Logistic Regression Model</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="31">model = LogisticRegression(
    max_iter=200,
    random_state=42,
    solver='lbfgs',
    multi_class='multinomial'
)
model.fit(X_train, y_train_encoded)</pre>



<p>This is a simple, lightweight model suitable for demonstrating pipelines, dependencies, and reproducibility.</p>



<h4 class="wp-block-heading">Report Training Accuracy</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="32">train_accuracy = model.score(X_train, y_train_encoded)
print(f"📈 Training accuracy: {train_accuracy:.4f}")</pre>



<p>This provides quick feedback but is <em>not</em> saved as a metric because evaluation happens later.</p>



<h3 class="wp-block-heading">How It Loads Stage 1 Output</h3>



<p>The key detail in this stage is the direct dependency on Stage 1’s output directory:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="33">train_path = "outputs/processed/train.csv"</pre>



<p>That file is generated by the preprocessing step.</p>



<p>In <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="34">deps:
  - outputs/processed/</pre>



<p>This means:</p>



<ul class="wp-block-list">
<li>If any file inside <code data-enlighter-language="python" class="EnlighterJSRAW">outputs/processed/</code> changes</li>



<li>If the preprocessing script changes</li>



<li>If the raw data changes</li>
</ul>



<p>Stage 2 then automatically reruns when you run the following command:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="35">dvc repro</pre>



<p>This ensures reproducibility and tightly coupled data lineage.</p>



<h3 class="wp-block-heading">Saving the Model and Label Encoder</h3>



<p>After training, the script saves 2 artifacts:</p>



<ul class="wp-block-list">
<li><strong>The trained model</strong></li>



<li><strong>The label encoder</strong></li>
</ul>



<p>This is important because predictions on new data would not map to the correct class names without the encoder.</p>



<p><strong>Create output directory</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="36">output_dir = "outputs/model"
os.makedirs(output_dir, exist_ok=True)</pre>



<p><strong>Write the model </strong><strong>and</strong><strong> encoder</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="37">model_path = os.path.join(output_dir, "model.pkl")
encoder_path = os.path.join(output_dir, "label_encoder.pkl")

with open(model_path, "wb") as f:
    pickle.dump(model, f)

with open(encoder_path, "wb") as f:
    pickle.dump(label_encoder, f)</pre>



<p>These artifacts drive the evaluation stage and are vital for any downstream inference.</p>



<h3 class="wp-block-heading">Why Model Outputs Are Tracked as DVC Artifacts</h3>



<p>In <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>, Stage 2 defines:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="38">outs:
  - outputs/model/</pre>



<p>This ensures DVC:</p>



<ul class="wp-block-list">
<li>Versions your model files deterministically</li>



<li>Treats the entire <code data-enlighter-language="python" class="EnlighterJSRAW">outputs/model/</code> directory as an artifact</li>



<li>Stores the model in <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code> using content hashing</li>



<li>Adds reproducibility guarantees (via <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code>)</li>



<li>Re-runs Stage 3 only if the model changes</li>
</ul>



<p>DVC tracks only metadata in Git rather than the <code data-enlighter-language="python" class="EnlighterJSRAW">.pkl</code> files. This approach keeps your repository lightweight while preserving reproducibility.</p>



<p>This setup also mirrors real production workflows:</p>



<ul class="wp-block-list">
<li>Lightning or PyTorch generates checkpoints</li>



<li>DVC versions them</li>



<li>Teams share them without pushing binaries to Git</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Stage-3-Model-Evaluation-and-Metrics-Tracking-with-DVC"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Stage-3-Model-Evaluation-and-Metrics-Tracking-with-DVC">Stage 3: Model Evaluation and Metrics Tracking with DVC</a></h2>



<p>The evaluation stage is the final step in the pipeline. It loads the trained model and label encoder from Stage 2, evaluates performance on the test dataset from Stage 1, generates structured metrics, produces a human-readable report, and writes both outputs to DVC-tracked locations.</p>



<p>This stage demonstrates how DVC can treat <strong>metrics as first-class citizens</strong> in a reproducible ML workflow.</p>



<h3 class="wp-block-heading">Reviewing evaluate.py</h3>



<p>This script lives at:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="39">src/evaluate.py</pre>



<p>Here is a walkthrough of what it does.</p>



<h4 class="wp-block-heading">Load Test Data</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="40">test_path = "outputs/processed/test.csv"
test_df = pd.read_csv(test_path)</pre>



<p>Just as training depends on processed training data, evaluation depends on processed test data from Stage 1.</p>



<h4 class="wp-block-heading">Split Features and Labels</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="41">X_test = test_df.drop("species", axis=1)
y_test = test_df["species"]</pre>



<h4 class="wp-block-heading">Load the Trained Model and Label Encoder</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="42">model_path = "outputs/model/model.pkl"
encoder_path = "outputs/model/label_encoder.pkl"

with open(model_path, "rb") as f:
    model = pickle.load(f)

with open(encoder_path, "rb") as f:
    label_encoder = pickle.load(f)</pre>



<p>This is where pipeline dependency chaining becomes visible:</p>



<ul class="wp-block-list">
<li><strong>Stage 1:</strong> generates processed data</li>



<li><strong>Stage 2:</strong> generates the model and encoder</li>



<li><strong>Stage 3:</strong> loads output from both</li>
</ul>



<h4 class="wp-block-heading">Encode Labels and Run Predictions</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="43">y_test_encoded = label_encoder.transform(y_test)
y_pred = model.predict(X_test)</pre>



<h4 class="wp-block-heading">Compute Primary Metric (Accuracy)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="44">accuracy = accuracy_score(y_test_encoded, y_pred)
print(f"📈 Test Accuracy: {accuracy:.4f}")</pre>



<p>The script also prints a classification report and confusion matrix to help users understand model behavior.</p>



<h4 class="wp-block-heading">Create Output Directories</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="45">os.makedirs("reports", exist_ok=True)</pre>



<p>This sets the stage for saving the structured and human-readable results.</p>



<h3 class="wp-block-heading">Generating Metrics (metrics.json)</h3>



<p>The evaluation script produces a machine-readable metrics file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="46">metrics = {
    "accuracy": float(accuracy),
    "test_samples": len(X_test),
    "classification_report": report
}

metrics_path = "outputs/metrics.json"
with open(metrics_path, "w") as f:
    json.dump(metrics, f, indent=2)</pre>



<p><strong>Why this matters</strong></p>



<ul class="wp-block-list">
<li>DVC can <strong>compare </strong><strong>metrics across runs</strong> (e.g., before and after hyperparameter tuning)</li>



<li>Continuous integration and continuous delivery (CI/CD) pipelines can read this JavaScript Object Notation (JSON) file automatically</li>



<li>You can integrate these metrics into dashboards or experiment tracking tools</li>
</ul>



<p>In <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>, this file appears under:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="47">outs:
  - outputs/metrics.json</pre>



<p><em>(You may optionally move this to the metrics: section for metric-specific behavior.)</em></p>



<h3 class="wp-block-heading">Generating Human-Readable Reports (accuracy.txt)</h3>



<p>While <code data-enlighter-language="python" class="EnlighterJSRAW">metrics.json</code> is for machines, humans often want plain text.</p>



<p>The script generates:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="48">accuracy_report_path = "reports/accuracy.txt"
with open(accuracy_report_path, "w") as f:
    f.write("Model Evaluation Report\n")
    f.write(f"{'='*50}\n\n")
    f.write(f"Test Accuracy: {accuracy:.4f}\n")
    f.write(f"Test Samples: {len(X_test)}\n\n")
    f.write(f"Classification Report:\n")
    f.write(f"{'-'*50}\n")
    f.write(classification_report(...))
    f.write("\nConfusion Matrix:\n")
    f.write(f"{'-'*50}\n")
    f.write(f"{cm}\n")</pre>



<p>This produces:</p>



<ul class="wp-block-list">
<li>Header</li>



<li>Accuracy summary</li>



<li>Classification report</li>



<li>Confusion matrix</li>
</ul>



<p>It stores the report in the following location:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="49">reports/accuracy.txt</pre>



<p>Because this is listed under <code data-enlighter-language="python" class="EnlighterJSRAW">outs</code> in <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>, DVC automatically tracks it.</p>



<h3 class="wp-block-heading">Marking Metrics and Plots in DVC (optional but recommended)</h3>



<p>DVC has special sections for:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">metrics</code>: numeric evaluation values</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">plots</code>: visualizations such as confusion matrices</li>
</ul>



<p>You <em>can</em> enhance the <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate</code> stage with the following configuration:</p>



<p><strong>Example configuration</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="50">stages:
  evaluate:
    cmd: python src/evaluate.py
    deps:
      - outputs/model/
      - src/evaluate.py
    metrics:
      - outputs/metrics.json:
          cache: false
    outs:
      - reports/accuracy.txt</pre>



<p><strong>Benefits</strong></p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">dvc metrics show</code>: displays the current metrics</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">dvc metrics diff</code>: compares accuracy between commits</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">cache: false</code>: <code data-enlighter-language="python" class="EnlighterJSRAW">metrics.json</code> is not stored in the DVC cache because metrics files are lightweight and can be tracked directly by Git</li>
</ul>



<h4 class="wp-block-heading">Optional: Track Plots</h4>



<p>If you ever generate <code data-enlighter-language="python" class="EnlighterJSRAW">confusion_matrix.png</code> or similar:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="51">plots:
  - outputs/confusion_matrix.png</pre>



<p>Then run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="52">dvc plots show</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-and-Reproducing-the-Pipeline"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-and-Reproducing-the-Pipeline">Running and Reproducing the Pipeline</a></h2>



<p>Once your pipeline is defined in <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>, everything else becomes beautifully simple.</p>



<p>Instead of manually running <code data-enlighter-language="python" class="EnlighterJSRAW">preprocess.py</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate.py</code>, you can let DVC orchestrate the entire workflow. DVC reruns only the stages that need to run.</p>



<p>This is where DVC starts feeling <em>like magic</em>.</p>



<h3 class="wp-block-heading">Running the Full Pipeline (dvc repro)</h3>



<p>Your pipeline has 3 stages:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="53">preprocess → train → evaluate</pre>



<p>To run all of them in the correct order, execute:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="54">dvc repro</pre>



<p>The <strong>first run</strong> always executes all stages because no cached outputs exist yet.</p>



<p><strong>Expected output</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="55">Running stage 'preprocess':
> python src/preprocess.py
📊 Starting data preprocessing...
...
💾 Saved train data to: outputs/processed/train.csv
💾 Saved test data to: outputs/processed/test.csv
✅ Preprocessing complete!

Running stage 'train':
> python src/train.py
🚀 Starting model training...
...
💾 Saved model to: outputs/model/model.pkl
💾 Saved label encoder to: outputs/model/label_encoder.pkl
✅ Training complete!

Running stage 'evaluate':
> python src/evaluate.py
📊 Starting model evaluation...
...
📈 Test Accuracy: 0.9333
📄 Saved report to: reports/accuracy.txt
✅ Evaluation complete!</pre>



<p>Behind the scenes, DVC is:</p>



<ul class="wp-block-list">
<li>hashing each dependency</li>



<li>hashing each output</li>



<li>saving everything in <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code></li>



<li>writing a reproducible lock file</li>
</ul>



<p>That brings us to the next part.</p>



<h3 class="wp-block-heading">Understanding dvc.lock</h3>



<p>After your first <code data-enlighter-language="python" class="EnlighterJSRAW">dvc repro</code> run, DVC generates:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="56">dvc.lock</pre>



<p>This file records the exact state of the pipeline when it ran, similar to <code data-enlighter-language="python" class="EnlighterJSRAW">package-lock.json</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">poetry.lock</code>.</p>



<p>Open it, and you will see entries such as the following:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="57">stages:
  preprocess:
    cmd: python src/preprocess.py
    deps:
      - path: data/raw/iris.csv
        md5: &lt;hash>
      - path: src/preprocess.py
        md5: &lt;hash>
    outs:
      - path: outputs/processed
        md5: &lt;hash>
        size: &lt;int>

  train:
    cmd: python src/train.py
    deps:
      - path: outputs/processed
        md5: &lt;hash>
      - path: src/train.py
        md5: &lt;hash>
    outs:
      - path: outputs/model
        md5: &lt;hash>

  evaluate:
    cmd: python src/evaluate.py
    deps:
      - path: outputs/model
        md5: &lt;hash>
      - path: src/evaluate.py
        md5: &lt;hash>
    outs:
      - path: outputs/metrics.json
      - path: reports/accuracy.txt</pre>



<h3 class="wp-block-heading">Why This File Matters</h3>



<ul class="wp-block-list">
<li>It guarantees <strong>reproducibility across machines</strong>.</li>



<li>It tells DVC which outputs match which inputs.</li>



<li>It allows DVC to detect when <em>nothing changed</em>.</li>



<li>It must be committed to Git.</li>
</ul>



<p>Anyone pulling your repository can run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="58">dvc repro</pre>



<p>They can then reproduce the pipeline.</p>



<h3 class="wp-block-heading">Incremental Re-Runs (Partial Pipeline Rebuild)</h3>



<p>DVC’s biggest advantage is <em>incrementality</em>.</p>



<p>It runs <strong>only the stages affected by changes</strong>.</p>



<p>Let us walk through real examples.</p>



<h4 class="wp-block-heading">Example 1: You Change src/preprocess.py</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="59"># Change test split from 0.2 → 0.3
dvc repro</pre>



<p>DVC will detect the following changes:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">preprocess</code> changed: must run</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">outputs/processed/</code> changed: <code data-enlighter-language="python" class="EnlighterJSRAW">train</code> must run</li>



<li>model changed: <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate</code> must run</li>
</ul>



<p><strong>Console output:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="60">Running stage 'preprocess' (changed deps):
Running stage 'train' (changed deps):
Running stage 'evaluate' (changed deps):</pre>



<h4 class="wp-block-heading">Example 2: You Change Only src/train.py</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="61">dvc repro</pre>



<p>DVC will detect the following:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">preprocess</code> unchanged: skip</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">train</code> changed: run</li>



<li>model changed: <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate</code> must run</li>
</ul>



<p><strong>Console output:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="62">Stage 'preprocess' didn't change, skipping
Running stage 'train'
Running stage 'evaluate'</pre>



<h4 class="wp-block-heading">Example 3: Nothing Changed</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="63">dvc repro</pre>



<p>Output:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="64">Stage 'preprocess' didn't change, skipping
Stage 'train' didn't change, skipping
Stage 'evaluate' didn't change, skipping
Data and pipelines are up to date.</pre>



<p>This means DVC checked <em>all dependencies, hashes, and outputs</em>. Everything matches the lock file.</p>



<p>Zero wasted compute. Zero unnecessary runs.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Visualizing-and-Inspecting-Pipelines"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Visualizing-and-Inspecting-Pipelines">Visualizing and Inspecting Pipelines</a></h2>



<p>Once your pipeline is running, you will want ways to inspect, debug, understand, and compare different runs.</p>



<p>DVC includes a suite of visualization and inspection tools that help you answer questions such as the following:</p>



<ul class="wp-block-list">
<li><em>What stages are in my pipeline?</em></li>



<li><em>Which files changed since the last run?</em></li>



<li><em>Do I need to re-run anything?</em></li>



<li><em>What metrics improved or regressed?</em></li>
</ul>



<p>This section walks you through the most useful tools for pipeline introspection.</p>



<h3 class="wp-block-heading">Visualizing the Pipeline DAG (dvc dag)</h3>



<p>The fastest way to understand your ML workflow is to visualize it as a <strong>DAG (Directed Acyclic Graph)</strong>.</p>



<p>DVC generates a clean ASCII graph of your pipeline:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="65">dvc dag</pre>



<p><strong>Expected output for your exact pipeline:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="66">        +----------------------+
        |   data/raw/iris.csv |
        +----------------------+
                    *
                    *
             +------------+
             | preprocess |
             +------------+
                    *
                    *
                +-------+
                | train |
                +-------+
                    *
                    *
              +----------+
              | evaluate |
              +----------+</pre>



<p>This confirms the 3-stage flow:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="67">raw data → preprocess → train → evaluate</pre>



<p><strong>Use cases:</strong></p>



<ul class="wp-block-list">
<li>Quickly verify pipeline structure</li>



<li>Check whether a stage is connected properly</li>



<li>Ensure there are no missing dependencies</li>
</ul>



<h4 class="wp-block-heading">Bonus: Mermaid DAG Output</h4>



<p>For documentation, slides, or PyImageSearch blog posts:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="68">dvc dag --mermaid</pre>



<p>Produces a Mermaid flowchart:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="69">flowchart TD
    preprocess --> train
    train --> evaluate</pre>



<p>Paste this output into a Markdown renderer that supports Mermaid to produce a visual diagram.</p>



<h3 class="wp-block-heading">Checking the Pipeline Status (dvc status)</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">dvc status</code> tells you whether <strong>any dependency or output </strong><strong>has </strong><strong>changed</strong> since the last <code data-enlighter-language="python" class="EnlighterJSRAW">dvc repro</code> run.</p>



<p>Run it anytime:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="70">dvc status</pre>



<p><strong>If nothing changed:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="71">Data and pipelines are up to date.</pre>



<p><strong>If something changed</strong> (e.g., you modified <code data-enlighter-language="python" class="EnlighterJSRAW">src/preprocess.py</code>):</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="72">preprocess:
    changed deps:
        modified: src/preprocess.py</pre>



<p>This means the following:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">preprocess</code> must rerun</li>



<li>which triggers reruns for downstream stages (<code data-enlighter-language="python" class="EnlighterJSRAW">train</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate</code>)</li>
</ul>



<p><strong>If the dataset changed:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="73">preprocess:
    changed deps:
        modified: data/raw/iris.csv</pre>



<p>Use this before running large pipelines; it tells you what will execute.</p>



<h3 class="wp-block-heading">Comparing Pipeline Versions (dvc diff)</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">dvc diff</code> helps you inspect <em>changes </em><em>between 2 Git commits or between </em><em>the current workspace and a Git commit</em><em>.</em></p>



<p>It is especially useful when you are debugging why metrics changed.</p>



<p>To compare your current workspace to the last committed run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="74">dvc diff</pre>



<h4 class="wp-block-heading">Example 1: Preprocessing Logic Changed</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="75">Modified outs:
  outputs/processed:
    size: 4.2KB -> 4.5KB</pre>



<h4 class="wp-block-heading">Example 2: Model Weights Changed (e.g., parameter update)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="76">Modified outs:
  outputs/model:
    size: 12KB -> 15KB</pre>



<h4 class="wp-block-heading">Example 3: Metrics Improved or Regressed</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="77">Modified outs:
  outputs/metrics.json:
    diff:
      accuracy: 0.9333 -> 0.9666</pre>



<p>This is the easiest way to track performance shifts after code or data changes.</p>



<h3 class="wp-block-heading">Inspecting Metrics (dvc metrics show, metrics diff)</h3>



<p>In your pipeline:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">outputs/metrics.json</code>: contains structured evaluation metrics</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">reports/accuracy.txt</code>: contains the human-readable report</li>
</ul>



<p>DVC can read JSON metrics directly.</p>



<h4 class="wp-block-heading">Show metrics from the latest run</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="78">dvc metrics show</pre>



<p>Expected output:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="79">Path: outputs/metrics.json
accuracy: 0.9333
test_samples: 30</pre>



<h4 class="wp-block-heading">Compare Metrics Between Commits</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="80">dvc metrics diff</pre>



<p>Example output:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="81">Path: outputs/metrics.json
metric: accuracy
    old: 0.9333
    new: 0.9666
    diff: +0.0333</pre>



<p>This is extremely powerful during model development:</p>



<ul class="wp-block-list">
<li>See whether your model improved</li>



<li>Detect regressions immediately</li>



<li>Track hyperparameter sensitivity</li>



<li>Log performance across experiments</li>
</ul>



<p><em><strong>Note: </strong></em><em>Your pipeline currently lists </em><code data-enlighter-language="python" class="EnlighterJSRAW">outputs/metrics.json</code><em> as a regular output in </em><code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code><em>. You can move it to the </em><code data-enlighter-language="python" class="EnlighterJSRAW">metrics</code><em> section to mark it explicitly as a metrics file.</em></p>



<p>If you <em>want</em> to tag them explicitly:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="82">stages:
  evaluate:
    metrics:
      - outputs/metrics.json:
          cache: false</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Extending-the-Pipeline-Optional-Enhancements"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Extending-the-Pipeline-Optional-Enhancements">Extending the Pipeline (Optional Enhancements)</a></h2>



<p>Your current DVC pipeline already handles a clean 3-stage workflow: <code data-enlighter-language="python" class="EnlighterJSRAW">preprocess</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">train</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate</code>.</p>



<p>However, real-world ML pipelines rarely stay this small. As projects grow, you will need hyperparameter tuning, metrics tracking, and new stages like feature engineering or postprocessing.</p>



<p>This section shows how to evolve your pipeline <em>without breaking its reproducibility</em>, using DVC’s extensibility features.</p>



<h3 class="wp-block-heading">Using params.yaml for Hyperparameters</h3>



<p>Right now, your training script (<code data-enlighter-language="python" class="EnlighterJSRAW">src/train.py</code>) has hyperparameters hardcoded:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="83">model = LogisticRegression(
    max_iter=200,
    random_state=42,
    solver='lbfgs',
    multi_class='multinomial'
)</pre>



<p>To make these configurable through DVC, create a <code data-enlighter-language="python" class="EnlighterJSRAW">params.yaml</code> file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="84">train:
  max_iter: 200
  solver: lbfgs
  multi_class: multinomial
  random_state: 42

split:
  test_size: 0.2
  random_state: 42</pre>



<h4 class="wp-block-heading">Step 1: Modify train.py to read params</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="85">import yaml

params = yaml.safe_load(open("params.yaml"))

cfg = params["train"]

model = LogisticRegression(
    max_iter=cfg["max_iter"],
    random_state=cfg["random_state"],
    solver=cfg["solver"],
    multi_class=cfg["multi_class"]
)</pre>



<h4 class="wp-block-heading">Step 2: Update dvc.yaml to declare params</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="86">stages:
  train:
    cmd: python src/train.py
    deps:
      - outputs/processed/
      - src/train.py
    params:
      - train.max_iter
      - train.solver
      - train.multi_class
      - train.random_state
    outs:
      - outputs/model/</pre>



<p><strong>Result:</strong></p>



<ul class="wp-block-list">
<li>Changing <em>any value</em> in <code data-enlighter-language="python" class="EnlighterJSRAW">params.yaml</code> triggers only the <code data-enlighter-language="python" class="EnlighterJSRAW">train</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate</code> stages.</li>



<li>You get clean, reproducible hyperparameter experiments.</li>
</ul>



<h3 class="wp-block-heading">Adding Metrics and Plots Tracking</h3>



<p>Your pipeline already produces:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">outputs/metrics.json</code>: machine-readable metrics</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">reports/accuracy.txt</code>: human-readable report</li>
</ul>



<p>But DVC can <em>track these outputs as first-class metrics</em>.</p>



<h4 class="wp-block-heading">Mark metrics in dvc.yaml</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="87">stages:
  evaluate:
    cmd: python src/evaluate.py
    deps:
      - outputs/model/
      - src/evaluate.py
    metrics:
      - outputs/metrics.json:
          cache: false
    outs:
      - reports/accuracy.txt</pre>



<p>Now try:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="88">dvc metrics show
dvc metrics diff</pre>



<p>DVC displays accuracy differences across commits, which makes the command useful for comparing experiments.</p>



<h4 class="wp-block-heading">Optional: Track Plots</h4>



<p>If you add visualization code to <code data-enlighter-language="python" class="EnlighterJSRAW">evaluate.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="89">import matplotlib.pyplot as plt
from sklearn.metrics import ConfusionMatrixDisplay

disp = ConfusionMatrixDisplay(confusion_matrix=cm)
disp.plot()
plt.savefig("outputs/confusion_matrix.png")</pre>



<p>Update <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="90">plots:
  - outputs/confusion_matrix.png</pre>



<p>Now you can compare model performance visually:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="91">dvc plots show
dvc plots diff</pre>



<h3 class="wp-block-heading">Adding More Pipeline Stages</h3>



<p>Your pipeline is intentionally simple, but DVC makes it easy to scale.</p>



<p>Here are 3 realistic extension patterns:</p>



<h4 class="wp-block-heading">Option A: Feature Engineering Stage</h4>



<p>Create <code data-enlighter-language="python" class="EnlighterJSRAW">src/feature_engineering.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="92">def engineer():
    df = pd.read_csv("outputs/processed/train.csv")
    df["sepal_ratio"] = df["sepal_length"] / df["sepal_width"]
    df.to_csv("outputs/features/train_fe.csv", index=False)</pre>



<p>Add to <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="93">stages:
  feature_engineering:
    cmd: python src/feature_engineering.py
    deps:
      - outputs/processed/train.csv
      - src/feature_engineering.py
    outs:
      - outputs/features/</pre>



<p>Update train dependencies:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="94">train:
  deps:
    - outputs/features/</pre>



<p>DVC automatically inserts the new stage in the DAG.</p>



<h4 class="wp-block-heading">Option B: Hyperparameter Search Stage</h4>



<p>You can add a Python script that runs multiple training configurations.</p>



<p>For example, <code data-enlighter-language="python" class="EnlighterJSRAW">src/hpt.py</code> can invoke the equivalent of the following Shell commands:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="95">python src/train.py --max_iter=100
python src/train.py --max_iter=200
python src/train.py --max_iter=500</pre>



<p>Add a stage:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="96">stages:
  hpt:
    cmd: python src/hpt.py
    deps:
      - src/hpt.py
      - src/train.py
      - params.yaml
    outs:
      - outputs/hpt_results/</pre>



<h4 class="wp-block-heading">Option C: Postprocessing Stage</h4>



<p>Example: Generate a final report that combines the metrics and confusion matrix.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="97">stages:
  report:
    cmd: python src/final_report.py
    deps:
      - outputs/metrics.json
      - reports/accuracy.txt
      - src/final_report.py
    outs:
      - reports/final_report.md</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you learned how to turn a simple training workflow into a fully reproducible, multi-stage ML pipeline using DVC. Instead of running scripts manually and hoping results remain consistent, you now have a system that captures every dependency, data transformation, and output in a transparent, version-controlled workflow.</p>



<p>We began by breaking the Iris example into 3 stages: preprocessing, training, and evaluation. We then saw how DVC connects them using <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code>, and a DAG. You ran the pipeline end-to-end with <code data-enlighter-language="python" class="EnlighterJSRAW">dvc repro</code>, inspected which stages changed, and watched DVC rebuild only the parts affected by modifications. This is the cornerstone of reproducible, efficient ML engineering.</p>



<p>You also learned how to inspect pipelines visually, track metrics across runs, and optionally extend the workflow with hyperparameters, plots, and new stages. These enhancements prepare your pipelines for real-world growth, where experiments evolve and projects become more complex.</p>



<p>By the end, you should have a clear understanding of how DVC pipelines bring structure, repeatability, and discipline to ML projects. They turn ad hoc experimentation into a maintainable engineering process that scales with your team and codebase.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V</strong><strong>. </strong>“DVC Pipelines for MLOps: Build a Reproducible ML Pipeline,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/v78x4" target="_blank" rel="noreferrer noopener">https://pyimg.co/v78x4</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="DVC Pipelines for MLOps: Build a Reproducible ML Pipeline" data-enlighter-group="98">@incollection{Singh_2026_dvc-pipelines-mlops-build-reproducible-ml-pipeline,
  author = {Vikram Singh},
  title = {{DVC Pipelines for MLOps: Build a Reproducible ML Pipeline}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/v78x4},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/09/14/dvc-pipelines-for-mlops-build-a-reproducible-ml-pipeline/">DVC Pipelines for MLOps: Build a Reproducible ML Pipeline</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>DVC for MLOps: Versioning Your Data and Models the Right Way</title>
		<link>https://pyimagesearch.com/2026/09/07/dvc-for-mlops-versioning-your-data-and-models-the-right-way/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 07 Sep 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Data Version Control]]></category>
		<category><![CDATA[Machine Learning]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[data version control]]></category>
		<category><![CDATA[dataset versioning]]></category>
		<category><![CDATA[dvc]]></category>
		<category><![CDATA[git for data]]></category>
		<category><![CDATA[ml pipeline]]></category>
		<category><![CDATA[mlops]]></category>
		<category><![CDATA[mlops tools]]></category>
		<category><![CDATA[mlops workflow]]></category>
		<category><![CDATA[model versioning]]></category>
		<category><![CDATA[pytorch lightning]]></category>
		<category><![CDATA[reproducible machine learning]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=55214</guid>

					<description><![CDATA[<p>Table of Contents DVC for MLOps: Versioning Your Data and Models the Right Way DVC for MLOps: Data and Model Versioning Explained Project Setup Dataset Versioning with DVC Model Versioning with DVC Configuring DVC Remotes Using DVC Push and Pull&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/09/07/dvc-for-mlops-versioning-your-data-and-models-the-right-way/">DVC for MLOps: Versioning Your Data and Models the Right Way</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-DVC-for-MLOps-Versioning-Your-Data-and-Models-the-Right-Way"><a rel="noopener" target="_blank" href="#h1-DVC-for-MLOps-Versioning-Your-Data-and-Models-the-Right-Way">DVC for MLOps: Versioning Your Data and Models the Right Way</a></li>
    <li id="TOC-h2-DVC-for-MLOps-Data-and-Model-Versioning-Explained"><a rel="noopener" target="_blank" href="#h2-DVC-for-MLOps-Data-and-Model-Versioning-Explained">DVC for MLOps: Data and Model Versioning Explained</a></li>
    <li id="TOC-h2-Project-Setup"><a rel="noopener" target="_blank" href="#h2-Project-Setup">Project Setup</a></li>
    <li id="TOC-h2-Dataset-Versioning-with-DVC"><a rel="noopener" target="_blank" href="#h2-Dataset-Versioning-with-DVC">Dataset Versioning with DVC</a></li>
    <li id="TOC-h2-Model-Versioning-with-DVC"><a rel="noopener" target="_blank" href="#h2-Model-Versioning-with-DVC">Model Versioning with DVC</a></li>
    <li id="TOC-h2-Configuring-DVC-Remotes"><a rel="noopener" target="_blank" href="#h2-Configuring-DVC-Remotes">Configuring DVC Remotes</a></li>
    <li id="TOC-h2-Using-DVC-Push-and-Pull-to-Sync-Data-and-Model-Artifacts"><a rel="noopener" target="_blank" href="#h2-Using-DVC-Push-and-Pull-to-Sync-Data-and-Model-Artifacts">Using DVC Push and Pull to Sync Data and Model Artifacts</a></li>
    <li id="TOC-h2-Building-a-Reproducible-Machine-Learning-Pipeline-with-DVC"><a rel="noopener" target="_blank" href="#h2-Building-a-Reproducible-Machine-Learning-Pipeline-with-DVC">Building a Reproducible Machine Learning Pipeline with DVC</a></li>
    <li id="TOC-h2-Full-Real-World-Workflow-with-Lightning-DVC-and-GitHub"><a rel="noopener" target="_blank" href="#h2-Full-Real-World-Workflow-with-Lightning-DVC-and-GitHub">Full Real-World Workflow with Lightning, DVC, and GitHub</a></li>
    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-DVC-for-MLOps-Versioning-Your-Data-and-Models-the-Right-Way"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-DVC-for-MLOps-Versioning-Your-Data-and-Models-the-Right-Way">DVC for MLOps: Versioning Your Data and Models the Right Way</a></h2>



<p>In this lesson, you will learn how to use DVC (Data Version Control) to version datasets, track model checkpoints, and create reproducible ML (machine learning) pipelines without bloating your Git repository. You will see how DVC integrates seamlessly with modern MLOps (machine learning operations) workflows and how it solves the versioning challenges that Git alone cannot handle.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured.png?lossy=2&strip=1&webp=1" alt="dvc-for-mlops-versioning-data-models-right-way-featured.png" class="wp-image-55294"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/dvc-for-mlops-versioning-data-models-right-way-featured.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p>This lesson is the 1st in a 2-part series on <strong>Data and Model Versioning with DVC</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/bu1ya" target="_blank" rel="noreferrer noopener">DVC for MLOps: Versioning Your Data and Models the Right Way</a></strong></em><strong> (this tutorial)</strong></li>



<li><em>Lesson 2</em></li>
</ol>



<p><strong>To learn how to </strong><strong>version</strong> <strong>your data, manage checkpoints, configure remotes, and reproduce pipelines with confidence, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-DVC-for-MLOps-Data-and-Model-Versioning-Explained"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-DVC-for-MLOps-Data-and-Model-Versioning-Explained">DVC for MLOps: Data and Model Versioning Explained</a></h2>



<p>Modern ML systems rely on <strong>large datasets</strong>, <strong>evolving model checkpoints</strong>, and <strong>pipeline stages</strong> that must be rerun reliably. Git alone was never built for multi-GB datasets or binary artifacts, and that is exactly where <strong>DVC (Data Version Control)</strong> becomes essential. In this introduction, we will explore why ML projects need a tool like DVC, how it elevates MLOps workflows, and what you will build by the end of this lesson.</p>



<h3 class="wp-block-heading">What DVC Is and Why Git Alone Is Not Enough</h3>



<p>Git excels at tracking text files, but it breaks down when you try to version datasets or model checkpoints:</p>



<ul class="wp-block-list">
<li><strong>Large files slow down Git: </strong>A single dataset or checkpoint can be hundreds of megabytes (MB) or several gigabytes (GB), making Git repositories heavy and slow.</li>



<li><strong>Git cannot store multiple versions efficiently: </strong>Binary diffs do not compress well, so Git history becomes huge over time.</li>



<li><strong>Teams end up sharing data manually: </strong>Emailing zip files, keeping <code data-enlighter-language="python" class="EnlighterJSRAW">final_v3_really_final.csv</code>, or relying on someone’s local drive.</li>
</ul>



<p><strong>DVC solves all these problems</strong>:</p>



<ul class="wp-block-list">
<li>Stores data <strong>outside Git</strong>, while Git tracks only lightweight metadata (<code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> files).</li>



<li>Guarantees <strong>reproducibility</strong> using hashes and dependency tracking.</li>



<li>Makes datasets and checkpoints <strong>shareable</strong> across the team via remotes (local, Amazon S3 (Amazon Simple Storage Service), GCS (Google Cloud Storage), Azure, SSH (Secure Shell)).</li>



<li>Integrates naturally with existing Git workflows without changing how you commit or create branches.</li>
</ul>



<p>Think of it as <strong>“Git for data and models”</strong>, tailor-made for machine learning.</p>



<h3 class="wp-block-heading">How DVC Fits Into Modern MLOps</h3>



<p>MLOps has 3 pillars:</p>



<ul class="wp-block-list">
<li><strong>Reproducible code</strong></li>



<li><strong>Reproducible environments</strong></li>



<li><strong>Reproducible data and model artifacts</strong></li>
</ul>



<p>Docker or virtual environments solve the first two.</p>



<p><strong>DVC completes the third.</strong></p>



<p>With DVC, teams can:</p>



<ul class="wp-block-list">
<li>Version datasets the same way they version code</li>



<li>Recreate any training run using DVC’s hashes and pipeline engine</li>



<li>Store multi-GB artifacts in efficient, cache-backed, cloud-friendly storage</li>



<li>Collaborate without duplicating files or manually syncing assets</li>



<li>Build DAG (directed acyclic graph)-style ML pipelines (<code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>) for preprocessing, training, and evaluation</li>
</ul>



<p>Most importantly, DVC integrates smoothly with:</p>



<ul class="wp-block-list">
<li><strong>PyTorch Lightning</strong> (for checkpoints)</li>



<li><strong>FastAPI </strong><strong>and</strong><strong> Flask apps</strong> (for deployed models)</li>



<li><strong>CI/CD</strong><strong> (continuous integration and continuous delivery)</strong><strong> systems</strong> (GitHub Actions, GitLab, Jenkins)</li>



<li><strong>Cloud storage providers</strong> (S3, GCS, Azure, etc.)</li>
</ul>



<p>In short: DVC brings <strong>software engineering discipline</strong> to ML artifact management.</p>



<h3 class="wp-block-heading">What We Will Build in This Lesson</h3>



<p>In this lesson, you will build a complete DVC-enabled workflow around the provided repository:</p>



<ul class="wp-block-list">
<li><strong>Dataset Versioning: </strong>You will track the sample IMDb dataset (<code data-enlighter-language="python" class="EnlighterJSRAW">data/raw/imdb_sample.csv</code>) using <code data-enlighter-language="python" class="EnlighterJSRAW">dvc add</code>, inspect its <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> metadata, and understand how DVC moves the real file into its cache.</li>



<li><strong>Model Checkpoint Versioning: </strong>You will generate a dummy model checkpoint by running <code data-enlighter-language="python" class="EnlighterJSRAW">src/noop_train.py</code>, then version it with DVC in the same way that you would track real PyTorch Lightning checkpoints.</li>



<li><strong>Local Remote Storage: </strong>You will configure a <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc_remote/</code> folder to simulate real cloud remotes, learning how DVC pushes and pulls artifacts.</li>



<li><strong>A Mini Training Pipeline: </strong>You will run the auto-generated DVC pipeline (dvc repro) that recreates model artifacts and updates dvc.lock.</li>
</ul>



<p>By the end, you will have a fully working <strong>data </strong><strong>and</strong><strong> model versioning workflow</strong>, complete with remotes, metadata, pipeline execution, and team-ready reproducibility.</p>



<p>It is simple enough to understand but realistic enough to scale into a real MLOps project.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Project-Setup"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Project-Setup">Project Setup</a></h2>



<p>Before we begin versioning datasets and model artifacts, let us walk through the repository structure, install dependencies, and initialize Git and DVC. This gives us a consistent, reproducible foundation for everything we will build later in the lesson.</p>



<h3 class="wp-block-heading">Overview of the Lesson 1 Repository Structure</h3>



<p>Your <code data-enlighter-language="python" class="EnlighterJSRAW">dvc-lesson1/</code> project is intentionally small, mirroring a real ML workflow but without unnecessary complexity. Here is the structure you will be working with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="1">dvc-lesson1/
├── data/
│   └── raw/
│       └── imdb_sample.csv
├── models/
├── src/
│   └── noop_train.py
├── .dvc/
├── .dvcignore
├── .gitignore
├── dvc.yaml
├── requirements.txt
└── README.md
</pre>



<p>What each part does:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">data/raw/</code>: Contains your raw dataset (IMDb sample).</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">models/</code>: Stores model checkpoints generated by the training script.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">src/noop_train.py</code>: A dummy training script that outputs <code data-enlighter-language="python" class="EnlighterJSRAW">model.ckpt</code>.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>: Defines a single DVC pipeline stage (<code data-enlighter-language="python" class="EnlighterJSRAW">generate_model</code>).</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">requirements.txt</code>: Dependencies: DVC + pandas.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.dvcignore</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">.gitignore</code>: Ensure Git and DVC track the right files.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/</code>: Will hold DVC metadata and cache configuration once initialized.</li>
</ul>



<p>This structure is simple but realistic: data, models, and code are cleanly separated, making it easy to scale toward multi-stage pipelines later.</p>



<h3 class="wp-block-heading">Installing Dependencies (requirements.txt)</h3>



<p>Your project uses only 2 core dependencies: <strong>DVC</strong> and <strong>pandas</strong>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="2"># DVC for data and model versioning
dvc==3.48.4

# Python basics
pandas==2.1.4
</pre>



<p>Install them using:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="3">pip install -r requirements.txt
</pre>



<p>This installs:</p>



<ul class="wp-block-list">
<li><strong>DVC core</strong> (local filesystem support)</li>



<li>Optional cloud backends (S3, GCS, Azure, and SSH) can be added later using extras such as the following:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="4">pip install 'dvc[s3]'
</pre>



<p>After installation, verify that DVC is available:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="5">dvc --version
</pre>



<p>You should see output similar to the following:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="6">3.48.4
</pre>



<p>If DVC installs successfully, you are ready for initialization.</p>



<h3 class="wp-block-heading">Initializing Git and DVC (git init, dvc init)</h3>



<p>DVC is designed to work <strong>on top of Git</strong>, not instead of it.</p>



<p>Let us initialize both:</p>



<h4 class="wp-block-heading">Step 1: Initialize Git</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="7">git init
</pre>



<p>This creates the <code data-enlighter-language="python" class="EnlighterJSRAW">.git/</code> folder and prepares the repository for tracking metadata.</p>



<h4 class="wp-block-heading">Step 2: Initialize DVC</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="8">dvc init
</pre>



<p>DVC sets up:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/</code>: internal DVC configuration and cache references</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.dvcignore</code>: patterns for files DVC should ignore</li>



<li>Git hooks for pipeline reproducibility</li>
</ul>



<p>Commit them:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="9">git add .dvc .dvcignore
git commit -m "Initialize DVC"
</pre>



<p>After this step, your project is now:</p>



<ul class="wp-block-list">
<li><strong>Git-tracked</strong><strong>:</strong> for code and metadata</li>



<li><strong>DVC-enabled:</strong> for datasets and model artifacts</li>



<li><strong>Pipeline-ready</strong><strong>:</strong> via the included <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code></li>
</ul>



<p>You now have everything required to start tracking data and model files the right way.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Dataset-Versioning-with-DVC"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Dataset-Versioning-with-DVC">Dataset Versioning with DVC</a></h2>



<p>Your dataset is the first artifact you will version with DVC. In this section, you will inspect the raw IMDb dataset, add it to DVC tracking, understand the resulting <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> metadata file, and see how DVC uses its cache behind the scenes. By the end, you will understand <em>precisely</em> why Git stores the metadata while DVC manages the actual data files.</p>



<h3 class="wp-block-heading">Inspecting the Raw IMDb Dataset</h3>



<p>Your dataset lives here:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="10">data/raw/imdb_sample.csv
</pre>



<p>It contains 10 short IMDb movie reviews labeled <strong>positive</strong> or <strong>negative</strong>.</p>



<p>Here are the first few rows:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="11">text,label
"This movie was absolutely fantastic! I loved every minute of it.",positive
"Terrible waste of time. Do not watch this movie.",negative
</pre>



<p>Even though this dataset is tiny, the workflow you are learning works identically for:</p>



<ul class="wp-block-list">
<li>gigabyte-scale comma-separated values (CSV) files</li>



<li>multi-GB image folders</li>



<li>terabyte-scale corpora</li>
</ul>



<p>DVC applies the same versioning workflow to all these data types.</p>



<h3 class="wp-block-heading">Tracking Data with DVC</h3>



<p>To version this dataset, run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="12">dvc add data/raw/imdb_sample.csv
</pre>



<p>DVC automatically performs several steps:</p>



<ul class="wp-block-list">
<li>Calculates an <strong>MD</strong><strong>5 hash</strong> of the file</li>



<li>Stores the CSV file content in the DVC cache (<code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code>)</li>



<li>Makes <code data-enlighter-language="python" class="EnlighterJSRAW">models/model.ckpt</code> available in the workspace as a link or copy, depending on the configuration</li>



<li>Generates a <strong>metadata file</strong>:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="13">data/raw/imdb_sample.csv.dvc
</pre>



<ul class="wp-block-list">
<li>Updates (or creates) a <code data-enlighter-language="python" class="EnlighterJSRAW">.gitignore</code> inside <code data-enlighter-language="python" class="EnlighterJSRAW">data/raw/</code> so Git does not track the actual CSV.</li>
</ul>



<p>Now add the generated metadata to Git:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="14">git add data/raw/imdb_sample.csv.dvc data/raw/.gitignore
git commit -m "Track IMDB dataset with DVC"
</pre>



<p>Git now tracks the <strong>dataset version</strong>, not the dataset itself.</p>



<h3 class="wp-block-heading">Understanding DVC Metadata Files</h3>



<p>Let us inspect the metadata:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="15">cat data/raw/imdb_sample.csv.dvc
</pre>



<p>You will see configuration similar to the following:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="16">outs:
- md5: a1b2c3d4e5f6...
  size: 512
  path: imdb_sample.csv
</pre>



<p>What each field means:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">md5</code>: fingerprint of the exact dataset content</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">size</code>: file size</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">path</code>: original location relative to its <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> file</li>
</ul>



<p>DVC uses the MD5 checksum to:</p>



<ul class="wp-block-list">
<li>detect changes</li>



<li>ensure reproducibility</li>



<li>retrieve the correct version from the cache or remote</li>
</ul>



<p>This <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> file is a small text file that represents a specific version of your dataset.</p>



<h3 class="wp-block-heading">How DVC Uses Its Cache</h3>



<p>When you ran <code data-enlighter-language="python" class="EnlighterJSRAW">dvc add</code>, the physical CSV file was moved into:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="17">.dvc/cache/
</pre>



<p>Inside the cache, every file is stored using its MD5 hash:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="18">.dvc/cache/a1/b2c3d4e5f6...
</pre>



<p>Why?</p>



<ul class="wp-block-list">
<li>Hash-based storage allows <strong>deduplication</strong></li>



<li>Identical files can reference the same cached object</li>



<li>Cached or remotely stored versions can be restored when needed</li>



<li>Remote storage (S3, GCS, local) becomes trivial</li>
</ul>



<p>Your working directory (<code data-enlighter-language="python" class="EnlighterJSRAW">data/raw/</code>) contains either:</p>



<ul class="wp-block-list">
<li>a <strong>hardlink</strong></li>



<li>a <strong>symlink</strong></li>



<li>a <strong>simple copy</strong></li>
</ul>



<p>depending on the operating system (OS) and configuration.</p>



<p>If the dataset changes, DVC computes a <strong>new</strong> hash and creates <strong>a new cached version</strong>, without overwriting the old one.</p>



<p>This is what makes dataset versioning possible.</p>



<h3 class="wp-block-heading">Why Git Tracks Metadata, Not Data</h3>



<p>Git is excellent at managing:</p>



<ul class="wp-block-list">
<li>small files</li>



<li>code</li>



<li>configuration</li>
</ul>



<p>However, Git is not designed to efficiently manage:</p>



<ul class="wp-block-list">
<li>large files</li>



<li>binary blobs</li>



<li>constantly evolving datasets</li>



<li>large model checkpoints</li>
</ul>



<p>Git repositories can become slow and difficult to manage when they contain large or frequently changing binary files.</p>



<p>DVC solves this by splitting responsibilities:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/image-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="186" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1024x186.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55303"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1024x186.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1024x186.png?size=126x23&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1024x186.png?size=252x46&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1024x186.png?size=378x69&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1024x186.png?size=504x92&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1024x186.png?size=630x114&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 1:</strong> How DVC Stores Data and Metadata</figcaption></figure></div>


<p>When you:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="19">git checkout &lt;commit>
</pre>



<p>Git restores the correct <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> files.</p>



<p>Then:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="20">dvc pull
</pre>



<p>DVC restores the <strong>exact dataset version</strong> associated with that commit.</p>



<p><em>This workflow allows Git and DVC to coordinate source-code and dataset versions.</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Model-Versioning-with-DVC"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Model-Versioning-with-DVC">Model Versioning with DVC</a></h2>



<p>Datasets evolve over time. Model artifacts do too. Checkpoints, weights, embeddings, tokenizers, and trained models must be versioned with the same rigor as your data. In this section, you will use DVC to track a dummy checkpoint file produced by <code data-enlighter-language="python" class="EnlighterJSRAW">noop_train.py</code>, and you will see how this workflow directly maps to real PyTorch Lightning training workflows.</p>



<h3 class="wp-block-heading">Reviewing the Dummy Training Script</h3>



<p>Your repository includes a tiny training script designed to <em>simulate</em> a real ML training job:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="21"># src/noop_train.py
def generate_model():
    print("🚀 Starting model generation...")
    time.sleep(1)
    os.makedirs("models", exist_ok=True)

    model_path = "models/model.ckpt"
    with open(model_path, "wb") as f:
        dummy_data = b"PYTORCH_LIGHTNING_CHECKPOINT_v1.0\n" * 30
        f.write(dummy_data)

    print(f"✅ Model checkpoint generated at: {model_path}")
    print(f"📦 File size: {os.path.getsize(model_path)} bytes")
</pre>



<p>When executed, it:</p>



<ul class="wp-block-list">
<li>Creates a <code data-enlighter-language="python" class="EnlighterJSRAW">models/</code> directory (if it does not exist)</li>



<li>Writes a <strong>1KB dummy checkpoint</strong> called <code data-enlighter-language="python" class="EnlighterJSRAW">model.ckpt</code></li>



<li>Produces stable output so you can track it with DVC</li>
</ul>



<p>This script plays the role of your “training job” in an actual ML workflow.</p>



<h3 class="wp-block-heading">Generating the Model Checkpoint</h3>



<p>Run the dummy training script:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="22">python src/noop_train.py
</pre>



<p>You should see:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="23">🚀 Starting model generation...
✅ Model checkpoint generated at: models/model.ckpt
📦 File size: 960 bytes
</pre>



<p>At this point, your project structure now includes:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="24">models/model.ckpt
</pre>



<p>This file is exactly what you would typically receive from:</p>



<ul class="wp-block-list">
<li>PyTorch Lightning</li>



<li>Hugging Face Trainer</li>



<li>Keras and TensorFlow training loops</li>



<li>Custom PyTorch training scripts</li>
</ul>



<p>In every real-world ML project, these checkpoints <strong>must be versioned</strong>.</p>



<h3 class="wp-block-heading">Adding Model Artifacts to DVC</h3>



<p>To version the checkpoint, run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="25">dvc add models/model.ckpt
</pre>



<p>Behind the scenes, DVC:</p>



<ul class="wp-block-list">
<li>Computes an MD5 checksum of the checkpoint</li>



<li>Moves the physical <code data-enlighter-language="python" class="EnlighterJSRAW">.ckpt</code> file into DVC’s cache (<code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/*</code>)</li>



<li>Recreates a link or copy at <code data-enlighter-language="python" class="EnlighterJSRAW">models/model.ckpt</code></li>



<li>Generates:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="26">models/model.ckpt.dvc
</pre>



<ul class="wp-block-list">
<li>Updates <code data-enlighter-language="python" class="EnlighterJSRAW">models/.gitignore</code> so Git no longer tracks <code data-enlighter-language="python" class="EnlighterJSRAW">model.ckpt</code></li>
</ul>



<p>Next, commit the metadata:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="27">git add models/model.ckpt.dvc models/.gitignore
git commit -m "Version model checkpoint with DVC"
</pre>



<p>Your Git history now records:</p>



<ul class="wp-block-list">
<li><strong>when the checkpoint changed</strong></li>



<li><strong>why it changed</strong> (commit message)</li>



<li><strong>how to reproduce it</strong> (via DVC pipeline or scripts)</li>
</ul>



<p>Git remains lightweight; DVC handles the binary file safely.</p>



<h4 class="wp-block-heading">What the DVC Metadata Looks Like</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="28">outs:
- md5: e3b0c44298fc...
  size: 960
  path: model.ckpt
</pre>



<p>This metadata supports:</p>



<ul class="wp-block-list">
<li><strong>Reproducibility</strong><strong>:</strong> DVC knows the exact checksum of the checkpoint.</li>



<li><strong>Integrity</strong><strong>:</strong> If the file changes, the hash changes too.</li>



<li><strong>Recoverability</strong><strong>:</strong> DVC can restore the checkpoint from cache or remote.</li>
</ul>



<p>This makes your ML experiments auditable and reversible.</p>



<h3 class="wp-block-heading">Integrating with Real PyTorch Lightning Workflows (Conceptual)</h3>



<p>In real-world ML pipelines, PyTorch Lightning generates checkpoints automatically:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="29">lightning_logs/
    version_0/
        checkpoints/
            epoch=3-step=500.ckpt
            best.ckpt
</pre>



<p>To version these with DVC, you would:</p>



<h4 class="wp-block-heading">1. Train Your Lightning Model</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="30">python train.py
</pre>



<h4 class="wp-block-heading">2. Identify the Checkpoint</h4>



<p>Lightning saves checkpoints to:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="31">lightning_logs/version_X/checkpoints/
</pre>



<p>Pick a file (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">best.ckpt</code>)</p>



<h4 class="wp-block-heading">3. Add It to DVC</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="32">dvc add lightning_logs/version_0/checkpoints/best.ckpt
git add lightning_logs/version_0/checkpoints/best.ckpt.dvc
git commit -m "Track best Lightning checkpoint"
</pre>



<h4 class="wp-block-heading">4. Push It to Your Remote</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="33">dvc push
</pre>



<h4 class="wp-block-heading">5. Teammates or Deployment Servers Restore It</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="34">git pull
dvc pull
</pre>



<p>This gives every engineer:</p>



<ul class="wp-block-list">
<li><strong>the exact model version</strong> used for inference</li>



<li><strong>the exact dataset</strong> used for training</li>



<li><strong>a reproducible pipeline state</strong></li>
</ul>



<p>In production settings (e.g., serving through FastAPI, TorchServe, vLLM, BentoML, or Lambda), DVC helps ensure that the intended model version is available.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuring-DVC-Remotes"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuring-DVC-Remotes">Configuring DVC Remotes</a></h2>



<p>Up to this point, you have tracked datasets and model artifacts with DVC. However, everything still lives <strong>locally</strong> inside your <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code> directory. For real collaboration across team members, machines, or deployment environments, you need a <strong>remote storage backend</strong>.</p>



<p>A <strong>DVC remote</strong> is simply a storage location (e.g., a local directory, S3 bucket, GCS bucket, Azure Blob container, or SSH server) where DVC stores cached versions of your datasets and model files. Git tracks the metadata, while <strong>the remote stores the actual bytes</strong>.</p>



<p>Let us break it down.</p>



<h3 class="wp-block-heading">What Are DVC Remotes and Why They Matter?</h3>



<p>DVC remotes solve the biggest challenge in ML collaboration:</p>



<p><strong>How do we store and share datasets and model checkpoints without bloating Git?</strong></p>



<p>A DVC remote gives you:</p>



<p><strong>Centralized artifact storage</strong></p>



<p>All team members sync to the same dataset and model versions.</p>



<p><strong>Lightweight Git history</strong></p>



<p>Git stores only <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> files, not large binaries.</p>



<p><strong>Reproducible experiments</strong></p>



<p>Anyone can restore the exact dataset and model checkpoint for a given commit.</p>



<p><strong>Separation of concerns</strong></p>



<ul class="wp-block-list">
<li><strong>Git:</strong> Versions metadata</li>



<li><strong>DVC </strong><strong>r</strong><strong>emote:</strong> versioning large files</li>
</ul>



<p>This is essential for training consistency, debugging, deployments, and CI/CD.</p>



<h3 class="wp-block-heading">Setting Up a Local DVC Remote</h3>



<p>Your lesson uses a <strong>local folder remote</strong>, which is perfect for beginners or offline environments.</p>



<h4 class="wp-block-heading">Step 1: Create the Remote Directory</h4>



<p>Run this at the project root:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="35">mkdir -p .dvc_remote
</pre>



<h4 class="wp-block-heading">Step 2: Add It as a DVC Remote</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="36">dvc remote add -d local_remote .dvc_remote
</pre>



<p>Here is what this command does:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/image-1.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="684" height="367" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55304" style="width:700px"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1.png?size=126x68&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1-300x161.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1.png?size=378x203&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1.png?size=504x270&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1.png?size=630x338&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-1.png?lossy=2&strip=1&webp=1 684w" sizes="(max-width: 684px) 100vw, 684px" /></a><figcaption class="wp-element-caption"><strong>Table 2:</strong> Components of the <code>dvc remote add</code> Command</figcaption></figure></div>


<h4 class="wp-block-heading">Step 3: Commit the New DVC Configuration</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="37">git add .dvc/config
git commit -m "Configure local DVC remote for dataset and model storage"
</pre>



<p>Now your project has a fully functional remote for storing versioned artifacts.</p>



<h3 class="wp-block-heading">Understanding .dvc/config</h3>



<p>After configuring the remote, open the file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="38">cat .dvc/config
</pre>



<p>You will see configuration similar to the following:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="39">['core']
    remote = 'local_remote'

['remote "local_remote"']
    url = '.dvc_remote'
</pre>



<p>Let us decode this:</p>



<h4 class="wp-block-heading">1. [core]</h4>



<p>Specifies the <em>default remote</em> used whenever you run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="40">dvc push
dvc pull
</pre>



<h4 class="wp-block-heading">2. remote &#8220;local_remote&#8221;</h4>



<p>Defines the remote’s properties:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/image-2.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="862" height="150" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-2.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55305" style="width:700px"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-2.png?size=126x22&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-2-300x52.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-2.png?size=378x66&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-2.png?size=504x88&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-2.png?size=630x110&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-2-768x134.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-2.png?lossy=2&strip=1&webp=1 862w" sizes="(max-width: 862px) 100vw, 862px" /></a><figcaption class="wp-element-caption"><strong>Table 3:</strong> DVC Remote Configuration Fields</figcaption></figure></div>


<p>If you were using S3, GCS, or Azure, this section would include the applicable paths and protocol details.</p>



<h4 class="wp-block-heading">Where Does the Actual Data Go?</h4>



<p>Inside <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc_remote/</code>, DVC stores files under a hash-based directory structure:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="41">.dvc_remote/
└── aa/
    └── bbcd1234...
</pre>



<p>Each stored artifact is:</p>



<ul class="wp-block-list">
<li>Broken into chunks (optional)</li>



<li>Named by checksum (similar to <code data-enlighter-language="python" class="EnlighterJSRAW">.git/objects/</code>)</li>
</ul>



<p>This ensures deduplication and integrity.</p>



<h3 class="wp-block-heading">Optional: Cloud Remotes (S3, GCS, Azure, SSH)</h3>



<p>Even though this lesson uses a local remote, most real MLOps pipelines use <strong>cloud remotes</strong> so training jobs, teammates, and CI systems can all access the same data.</p>



<p>The following examples show commands for each storage option.</p>



<h4 class="wp-block-heading">AWS S3 Remote</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="42">dvc remote add -d myremote s3://mybucket/dvc-storage
dvc remote modify myremote access_key_id &lt;AWS_ACCESS_KEY>
dvc remote modify myremote secret_access_key &lt;AWS_SECRET_KEY>
</pre>



<p>DVC will store:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="43">s3://mybucket/dvc-storage/aa/bbcd123...
</pre>



<h4 class="wp-block-heading">Google Cloud Storage Remote</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="44">dvc remote add -d myremote gs://mybucket/dvc-storage
</pre>



<p>Authenticate with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="45">gcloud auth application-default login
</pre>



<h4 class="wp-block-heading">Azure Blob Storage Remote</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="46">dvc remote add -d myremote azure://mycontainer/dvc-storage
dvc remote modify myremote connection_string "&lt;AZURE_CONNECTION_STRING>"
</pre>



<h4 class="wp-block-heading">SSH and SFTP Remote (Self-Hosted)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="47">dvc remote add -d myremote ssh://user@server:/path/to/storage
dvc remote modify myremote keyfile ~/.ssh/id_rsa
</pre>



<p>Great for on-premise teams.</p>



<h3 class="wp-block-heading">When Should You Use Cloud Remotes?</h3>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/image-3-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="360" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-3-1024x360.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55307"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-3-1024x360.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-3-1024x360.png?size=126x44&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-3-1024x360.png?size=252x89&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-3-1024x360.png?size=378x133&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-3-1024x360.png?size=504x177&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-3-1024x360.png?size=630x221&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 4:</strong> Choosing a DVC Remote Storage Option</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Using-DVC-Push-and-Pull-to-Sync-Data-and-Model-Artifacts"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Using-DVC-Push-and-Pull-to-Sync-Data-and-Model-Artifacts">Using DVC Push and Pull to Sync Data and Model Artifacts</a></h2>



<p>So far, you have tracked:</p>



<ul class="wp-block-list">
<li>A dataset (<code data-enlighter-language="python" class="EnlighterJSRAW">data/raw/imdb_sample.csv</code>)</li>



<li>A model checkpoint (<code data-enlighter-language="python" class="EnlighterJSRAW">models/model.ckpt</code>)</li>



<li>A pipeline stage (<code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code> → <code data-enlighter-language="python" class="EnlighterJSRAW">generate_model</code>)</li>
</ul>



<p>However, all of these artifacts still live <strong>locally</strong> in your <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code> directory.</p>



<p>To actually <em>share</em> them, <em>back them up</em>, and <em>restore them anywhere</em>, you need to sync them with your configured DVC remote.</p>



<p>This is where <code data-enlighter-language="python" class="EnlighterJSRAW">dvc push</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">dvc pull</code> come in.</p>



<h3 class="wp-block-heading">Uploading Artifacts to a DVC Remote</h3>



<p>Once a remote is configured (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc_remote/</code>), DVC can upload any tracked artifacts to it.</p>



<p><strong>Command</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="48">dvc push
</pre>



<p><strong>What happens?</strong></p>



<p>DVC performs several steps under the hood:</p>



<ul class="wp-block-list">
<li><strong>Scans all </strong><code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code><strong> files and </strong><code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code> to determine which files are tracked.<br>Example tracked files:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="49">data/raw/imdb_sample.csv
models/model.ckpt
</pre>



<ul class="wp-block-list">
<li><strong>Locates these files in the local </strong><code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code><strong>: </strong>Remember: DVC stores the <em>real data</em> in the cache using checksum names.</li>



<li><strong>Copies the cache objects to the remote: </strong>For example, a file might be uploaded to:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="50">.dvc_remote/aa/bbcd1234...
</pre>



<ul class="wp-block-list">
<li><strong>Skips uploading duplicates: </strong>If the checksum already exists in the remote, DVC does not upload it again.</li>
</ul>



<p><strong>Output Example</strong></p>



<p>When pushing your dataset and model:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="51">$ dvc push
100%|████████████████████████|1/1 [00:00&lt;00:00, 101.23file/s]
</pre>



<p>This confirms your artifacts are safely stored in the remote.</p>



<h3 class="wp-block-heading">Restoring Artifacts (dvc pull)</h3>



<p>Imagine a teammate clones your Git repository:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="52">git clone &lt;your-repo>
cd dvc-lesson1
</pre>



<p>They now have:</p>



<ul class="wp-block-list">
<li>Your <strong>code</strong></li>



<li>Your <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code><strong> metadata files</strong></li>



<li>Your <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code> pipeline definition</li>



<li>Your <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/config</code> pointing to the remote</li>
</ul>



<p>But they <strong>do not have the data or model artifacts</strong>.</p>



<p><strong>To restore them, they run:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="53">dvc pull
</pre>



<p>With this single command, DVC:</p>



<ul class="wp-block-list">
<li>Reads your <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> files to know <em>which artifacts should exist</em></li>



<li>Downloads their cache objects from the remote into <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code></li>



<li>Recreates the original directory structure:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="54">data/raw/imdb_sample.csv
models/model.ckpt
</pre>



<p><strong>Real Output Example</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="55">$ dvc pull
100%|█████████████████████|1/1 [00:00&lt;00:00, 250.50file/s]
</pre>



<p>Now your teammate has:</p>



<ul class="wp-block-list">
<li><strong>The exact same dataset</strong> you used.</li>



<li><strong>The same model checkpoint</strong>.</li>



<li>A fully reproducible environment.</li>
</ul>



<h3 class="wp-block-heading">How Git and DVC Work Together</h3>



<p>The magic of DVC comes from how it pairs with Git while keeping responsibilities clean.</p>



<h4 class="wp-block-heading">Git Stores:</h4>



<ul class="wp-block-list">
<li>Code (<code data-enlighter-language="python" class="EnlighterJSRAW">src/</code>)</li>



<li>Pipeline definitions (<code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code>)</li>



<li>Metadata (<code data-enlighter-language="python" class="EnlighterJSRAW">*.dvc</code>)</li>



<li>Remote configuration (<code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/config</code>)</li>



<li>Docs and scripts</li>
</ul>



<p>Git <strong>never</strong> stores large binaries.</p>



<h4 class="wp-block-heading">DVC Remote Stores:</h4>



<ul class="wp-block-list">
<li>Actual dataset files</li>



<li>Actual model checkpoints</li>



<li>Other large ML artifacts (e.g., features, embeddings, plots, and metrics)</li>
</ul>



<p>All files are stored by <strong>checksum</strong>, ensuring:</p>



<ul class="wp-block-list">
<li>Deduplication</li>



<li>Integrity</li>



<li>Fast restoration</li>
</ul>



<h4 class="wp-block-heading">Workflow Example for a Team of 3</h4>



<p>Let us imagine you run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="56">python src/noop_train.py
dvc add models/model.ckpt
git add models/model.ckpt.dvc
git commit -m "Add v1 checkpoint"
dvc push
git push
</pre>



<p>Now the remote contains the checkpoint, and Git contains its metadata.</p>



<h4 class="wp-block-heading">Teammate Workflow</h4>



<p>Your teammate runs:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="57">git pull
dvc pull
</pre>



<p>They instantly get:</p>



<ul class="wp-block-list">
<li>The same model checkpoint</li>



<li>The same dataset</li>



<li>The same pipeline configuration</li>
</ul>



<p>They can:</p>



<ul class="wp-block-list">
<li>Reproduce training</li>



<li>Evaluate the model</li>



<li>Extend experiments</li>



<li>Debug issues using the same data</li>
</ul>



<h4 class="wp-block-heading">Why This Is a Game-Changer for ML Teams</h4>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/image-4-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="304" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-4-1024x304.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55309"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-4-1024x304.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-4-1024x304.png?size=126x37&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-4-1024x304.png?size=252x75&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-4-1024x304.png?size=378x112&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-4-1024x304.png?size=504x150&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-4-1024x304.png?size=630x187&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 5:</strong> How Git and DVC Solve Common Machine Learning Collaboration Challenges</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Building-a-Reproducible-Machine-Learning-Pipeline-with-DVC"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Building-a-Reproducible-Machine-Learning-Pipeline-with-DVC">Building a Reproducible Machine Learning Pipeline with DVC</a></h2>



<p>One of DVC’s superpowers is that it can turn arbitrary commands (e.g., training scripts, preprocessing steps, feature generation, evaluation) into <strong>reproducible pipelines</strong>.</p>



<p>Even though this lesson uses a simple “noop” model generator, the ideas scale directly to real PyTorch Lightning workflows.</p>



<p>In this section, you will learn how DVC automatically:</p>



<ul class="wp-block-list">
<li>Creates a pipeline stage (<code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>)</li>



<li>Tracks script dependencies</li>



<li>Tracks model artifacts</li>



<li>Rebuilds outputs only when something changes</li>



<li>Guarantees reproducibility using <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code></li>
</ul>



<h3 class="wp-block-heading">Understanding the Auto-Generated DVC Pipeline Configuration</h3>



<p>When you ran:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="58">dvc add models/model.ckpt
</pre>



<p>DVC tracked the artifact, <em>but did not create a pipeline</em>.</p>



<p>To turn model generation into a reproducible pipeline, you used:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="59">dvc run -n generate_model -d src/noop_train.py -o models/model.ckpt python src/noop_train.py
</pre>



<p>Alternatively, a prewritten <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code> file can define the stage.</p>



<p>Your repository now contains the following file:</p>



<p><strong>dvc.yaml</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="60">stages:
  generate_model:
    cmd: python src/noop_train.py
    deps:
      - src/noop_train.py
    outs:
      - models/model.ckpt
</pre>



<p>Let us break this down:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/image-5-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="374" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-5-1024x374.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55313"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-5-1024x374.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-5-1024x374.png?size=126x46&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-5-1024x374.png?size=252x92&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-5-1024x374.png?size=378x138&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-5-1024x374.png?size=504x184&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-5-1024x374.png?size=630x230&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 6:</strong> Fields in a <code>dvc.yaml</code> Pipeline Definition</figcaption></figure></div>


<p><strong>Why This Matters</strong></p>



<p>If you modify <code data-enlighter-language="python" class="EnlighterJSRAW">src/noop_train.py</code>, DVC knows the dependency changed and will rerun the pipeline.</p>



<p>If nothing changed, DVC skips execution.</p>



<p>It is like <code data-enlighter-language="python" class="EnlighterJSRAW">make</code> but designed for ML.</p>



<h3 class="wp-block-heading">Running the DVC Pipeline Stage</h3>



<p>To execute the pipeline stage, run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="61">dvc repro
</pre>



<p>Example output:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="62">$ dvc repro

Running stage 'generate_model':
> python src/noop_train.py
🚀 Starting model generation...
✅ Model checkpoint generated at: models/model.ckpt
📦 File size: 870 bytes

Updating lock file 'dvc.lock'
</pre>



<p>What happened?</p>



<ul class="wp-block-list">
<li>DVC checked whether <code data-enlighter-language="python" class="EnlighterJSRAW">src/noop_train.py</code> changed.</li>



<li>Since it is the first run, DVC executed the command.</li>



<li>The checkpoint was generated.</li>



<li>DVC updated <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code> with:
<ul class="wp-block-list">
<li>Hash of the script</li>



<li>Hash of the output</li>



<li>Command used</li>
</ul>
</li>
</ul>



<p>Now the pipeline is fully reproducible.</p>



<h4 class="wp-block-heading">Rerunning Without Changes</h4>



<p>Run again:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="63">dvc repro
</pre>



<p>Output:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="64">Stage 'generate_model' didn't change, skipping
</pre>



<p>DVC intelligently avoids unnecessary work.</p>



<h4 class="wp-block-heading">Modify the Script and Rerun</h4>



<p>Add a comment or modify <code data-enlighter-language="python" class="EnlighterJSRAW">noop_train.py</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="65"># Added comment
def generate_model():
    ...
</pre>



<p>Run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="66">dvc repro
</pre>



<p>Now DVC sees the dependency changed, so it <strong>rebuilds</strong> the stage automatically.</p>



<h3 class="wp-block-heading">Inspecting the DVC Lock File </h3>



<p>Next to <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>, you will find the auto-generated lock file:</p>



<p><strong>dvc.lock</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="67">stages:
  generate_model:
    cmd: python src/noop_train.py
    deps:
      - path: src/noop_train.py
        md5: &lt;hash>
        size: &lt;bytes>
    outs:
      - path: models/model.ckpt
        md5: &lt;hash>
        size: &lt;bytes>
</pre>



<p>This file stores:</p>



<ul class="wp-block-list">
<li>Exact command used</li>



<li>Checksums of dependencies</li>



<li>Checksums of outputs</li>



<li>File sizes</li>
</ul>



<p><strong>Why This Is Important</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/image-6-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="303" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-6-1024x303.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55315"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-6-1024x303.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-6-1024x303.png?size=126x37&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-6-1024x303.png?size=252x75&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-6-1024x303.png?size=378x112&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-6-1024x303.png?size=504x149&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-6-1024x303.png?size=630x186&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 7:</strong> Benefits of the <code>dvc.lock</code> File</figcaption></figure></div>


<p>If <code data-enlighter-language="python" class="EnlighterJSRAW">model.ckpt</code> changes, its MD5 checksum will also change, signaling a new version.</p>



<h4 class="wp-block-heading">Verify the Lock State </h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="68">dvc status
</pre>



<p>Example output:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="69">Data and pipelines are up to date.
</pre>



<p>Change the script and the output becomes:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="70">Stage 'generate_model' changed.
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Full-Real-World-Workflow-with-Lightning-DVC-and-GitHub"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Full-Real-World-Workflow-with-Lightning-DVC-and-GitHub">Full Real-World Workflow with Lightning, DVC, and GitHub</a></h2>



<p>Everything you have done so far, including tracking datasets, tracking checkpoints, and defining a DVC pipeline, maps to a real-world workflow.</p>



<p>This section stitches it all together into a workflow where:</p>



<ul class="wp-block-list">
<li><strong>Lightning handles training</strong></li>



<li><strong>DVC handles dataset </strong><strong>and</strong><strong> checkpoint versioning</strong></li>



<li><strong>GitHub handles code </strong><strong>and </strong><strong>metadata</strong></li>



<li><strong>A remote store (</strong><strong>e.g., </strong><strong>S3</strong><strong>, </strong><strong>GCS</strong><strong>, or </strong><strong>Azure)</strong> stores large files</li>
</ul>



<p>This is the pattern used by most mature MLOps teams.</p>



<h3 class="wp-block-heading">Mapping Lightning Checkpoints to DVC Artifacts</h3>



<p>In DVC Lesson 1, you generated a dummy checkpoint (<code data-enlighter-language="python" class="EnlighterJSRAW">model.ckpt</code>) using <code data-enlighter-language="python" class="EnlighterJSRAW">noop_train.py</code>.</p>



<p>In real projects, PyTorch Lightning generates checkpoints automatically:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="71">lightning_logs/
└── version_0/
    └── checkpoints/
        ├── epoch=4-step=500.ckpt
        ├── last.ckpt
        └── best.ckpt
</pre>



<p>These files can easily reach <strong>hundreds of MB</strong> and change frequently.</p>



<p>This makes them <strong>terrible candidates for Git</strong>, but <strong>perfect candidates for DVC</strong>.</p>



<p><strong>How Lightning </strong><strong>and </strong><strong>DVC Connect</strong></p>



<p>Your workflow looks like this:</p>



<ul class="wp-block-list">
<li>Lightning trains the model</li>



<li>Lightning saves a checkpoint in <code data-enlighter-language="python" class="EnlighterJSRAW">checkpoints/</code></li>



<li>You tell DVC to track it:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="72">dvc add lightning_logs/version_0/checkpoints/best.ckpt
git add lightning_logs/version_0/checkpoints/best.ckpt.dvc
git commit -m "Track best Lightning checkpoint for experiment A"
</pre>



<ul class="wp-block-list">
<li>Push to remote storage:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="73">dvc push
</pre>



<ul class="wp-block-list">
<li>Commit code and metadata to GitHub:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="74">git push
</pre>



<p>Now teammates can reproduce your training output exactly:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="75">git pull
dvc pull  # downloads the checkpoint
</pre>



<p>No need to upload <code data-enlighter-language="python" class="EnlighterJSRAW">.ckpt</code> files manually, no need to Slack someone a 500 MB file, and no more “which model version did you use?” chaos.</p>



<h3 class="wp-block-heading">What Files Go to Git, DVC, and Remote Storage</h3>



<p>A clean ML workflow is all about <strong>putting each file in the right place</strong>.</p>



<p><strong>Table 8</strong> shows what goes where:</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/09/image-7.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1017" height="654" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-7.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55317"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-7.png?size=126x81&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-7-300x193.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-7.png?size=378x243&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-7.png?size=504x324&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-7.png?size=630x405&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-7-768x494.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/09/image-7.png?lossy=2&strip=1&webp=1 1017w" sizes="(max-width: 1017px) 100vw, 1017px" /></a><figcaption class="wp-element-caption"><strong>Table 8:</strong> Where to Store Machine Learning Project Files and Artifacts</figcaption></figure></div>


<p><em><strong>The Core Rule: </strong></em><em>Git tracks the metadata, DVC tracks the large files, a remote storage backend stores the bytes.</em></p>



<p>This separation prevents Git from bloating and keeps your repo lightning fast.</p>



<h3 class="wp-block-heading">Suggested Team Workflow (Industry-Grade)</h3>



<p>Below is the <strong>recommended daily workflow</strong> for ML teams using Lightning, DVC, and GitHub.</p>



<p>This is exactly how large production teams work.</p>



<h4 class="wp-block-heading">Step 1: Pull Code and Artifacts</h4>



<p>When starting on a fresh machine:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="76">git pull
dvc pull
</pre>



<p>You now have:</p>



<ul class="wp-block-list">
<li>Latest code</li>



<li>Latest dataset</li>



<li>Latest checkpoints</li>
</ul>



<h4 class="wp-block-heading">Step 2: Train Using Lightning</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="77">python train.py
</pre>



<p>Lightning writes:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="78">lightning_logs/version_3/checkpoints/best.ckpt
</pre>



<h4 class="wp-block-heading">Step 3: Version the New Checkpoint</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="79">dvc add lightning_logs/version_3/checkpoints/best.ckpt
git add lightning_logs/version_3/checkpoints/best.ckpt.dvc
git commit -m "Add checkpoint for experiment: bigger batch size"
</pre>



<h4 class="wp-block-heading">Step 4: Push Artifacts and Git Metadata</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="80">dvc push   # Uploads large files to DVC remote
git push   # Pushes code + metadata
</pre>



<p>Everyone on the team can now reproduce your run.</p>



<h4 class="wp-block-heading">Workflow Diagram (Textual)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="81">Lightning → produces checkpoint.ckpt  
        ↓  
DVC add → creates .dvc file  
        ↓  
git add *.dvc → store metadata  
        ↓  
dvc push → upload model to remote  
        ↓  
git push → sync metadata + code
</pre>



<p>Perfect reproducibility, every time.</p>



<h4 class="wp-block-heading">Bonus: Branch-Based Experimentation Pattern</h4>



<p>Teams often create experiment branches:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="82">experiments/
    ├── exp-bigger-lr
    ├── exp-more-layers
    ├── exp-new-augmentation
</pre>



<p>Each branch has:</p>



<ul class="wp-block-list">
<li>Code changes</li>



<li>DVC-tracked checkpoints</li>



<li>Git-tracked <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> metadata</li>
</ul>



<p>Merging into main requires:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="83">git merge &lt;branch>
dvc pull
</pre>



<p>DVC then restores the artifacts referenced by the merged metadata.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you learned why Git alone cannot support modern machine-learning workflows and how DVC solves that gap by versioning large datasets and model checkpoints without bloating your repository. You saw how DVC keeps Git lightweight by storing only metadata while placing real artifacts in a structured cache and optional remote storage.</p>



<p>You then worked through a hands-on workflow: inspecting a real dataset, adding it to DVC with <code data-enlighter-language="python" class="EnlighterJSRAW">dvc add</code>, generating a model checkpoint through a dummy training script, and versioning that checkpoint just like data. Along the way, you learned how <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc</code> files, <code data-enlighter-language="python" class="EnlighterJSRAW">.dvc/cache/</code>, and auto-generated <code data-enlighter-language="python" class="EnlighterJSRAW">.gitignore</code> entries work together to make data tracking seamless.</p>



<p>Next, you configured remotes for artifact storage. You started with a local directory and then examined how easily DVC integrates with cloud backends (e.g., S3, GCS, Azure, and SSH). You pushed and pulled artifacts, simulating team collaboration, and explored how Git manages code while DVC synchronizes the corresponding data.</p>



<p>Finally, you ran a mini DVC pipeline using the auto-generated <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.yaml</code>, reproduced it with <code data-enlighter-language="python" class="EnlighterJSRAW">dvc repro</code>, and inspected <code data-enlighter-language="python" class="EnlighterJSRAW">dvc.lock</code> to understand how DVC captures reproducibility. You also connected these concepts back to real PyTorch Lightning workflows, mapping where datasets, checkpoints, and code naturally fit into a Git and DVC ecosystem.</p>



<p>Together, these steps form a strong foundation for reproducible ML projects. In the next lesson, you will expand this foundation into multi-stage DVC pipelines, experiment tracking, and full workflow automation.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V. </strong>“DVC for MLOps: Versioning Your Data and Models the Right Way,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/bu1ya" target="_blank" rel="noreferrer noopener">https://pyimg.co/bu1ya</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="DVC for MLOps: Versioning Your Data and Models the Right Way" data-enlighter-group="84">@incollection{Singh_2026_dvc-for-mlops-versioning-data-models-right-way,
  author = {Vikram Singh},
  title = {{DVC for MLOps: Versioning Your Data and Models the Right Way}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/bu1ya},
}
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/09/07/dvc-for-mlops-versioning-your-data-and-models-the-right-way/">DVC for MLOps: Versioning Your Data and Models the Right Way</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling</title>
		<link>https://pyimagesearch.com/2026/08/31/train-yolo26-on-a-custom-dataset-with-yoloe-26-auto-labeling/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 31 Aug 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Deep Learning]]></category>
		<category><![CDATA[Object Detection]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[YOLO]]></category>
		<category><![CDATA[auto-labeling]]></category>
		<category><![CDATA[computer vision]]></category>
		<category><![CDATA[custom dataset]]></category>
		<category><![CDATA[fine-tuning]]></category>
		<category><![CDATA[object detection]]></category>
		<category><![CDATA[pseudo-labeling]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[ultralytics]]></category>
		<category><![CDATA[visual prompting]]></category>
		<category><![CDATA[yolo26]]></category>
		<category><![CDATA[yoloe-26]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=55172</guid>

					<description><![CDATA[<p>Table of Contents Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling Auto-Labeling a Custom Dataset for YOLO26 Object Detection Configuring Your Development Environment Project Structure Step 1: Define Custom Object Classes and Reference Images Step 2: Draw Visual Prompt&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/31/train-yolo26-on-a-custom-dataset-with-yoloe-26-auto-labeling/">Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Train-YOLO26-on-a-Custom-Dataset-with-YOLOE-26-Auto-Labeling"><a rel="noopener" target="_blank" href="#h1-Train-YOLO26-on-a-Custom-Dataset-with-YOLOE-26-Auto-Labeling">Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling</a></li>
    <li id="TOC-h2-Auto-Labeling-a-Custom-Dataset-for-YOLO26-Object-Detection"><a rel="noopener" target="_blank" href="#h2-Auto-Labeling-a-Custom-Dataset-for-YOLO26-Object-Detection">Auto-Labeling a Custom Dataset for YOLO26 Object Detection</a></li>
    <li id="TOC-h2-Configuring-Your-Development-Environment"><a rel="noopener" target="_blank" href="#h2-Configuring-Your-Development-Environment">Configuring Your Development Environment</a></li>
    <li id="TOC-h2-Project-Structure"><a rel="noopener" target="_blank" href="#h2-Project-Structure">Project Structure</a></li>
    <li id="TOC-h2-Step-1-Define-Custom-Object-Classes-and-Reference-Images"><a rel="noopener" target="_blank" href="#h2-Step-1-Define-Custom-Object-Classes-and-Reference-Images">Step 1: Define Custom Object Classes and Reference Images</a></li>
    <li id="TOC-h2-Step-2-Draw-Visual-Prompt-Boxes-for-YOLOE-26-Auto-Labeling"><a rel="noopener" target="_blank" href="#h2-Step-2-Draw-Visual-Prompt-Boxes-for-YOLOE-26-Auto-Labeling">Step 2: Draw Visual Prompt Boxes for YOLOE-26 Auto-Labeling</a></li>
    <li id="TOC-h2-Step-3-Auto-Label-a-Custom-Dataset-with-YOLOE-26-Visual-Prompting"><a rel="noopener" target="_blank" href="#h2-Step-3-Auto-Label-a-Custom-Dataset-with-YOLOE-26-Visual-Prompting">Step 3: Auto-Label a Custom Dataset with YOLOE-26 Visual Prompting</a></li>
    <li id="TOC-h2-Step-4-Clean-the-Pseudo-Labels"><a rel="noopener" target="_blank" href="#h2-Step-4-Clean-the-Pseudo-Labels">Step 4: Clean the Pseudo-Labels</a></li>
    <li id="TOC-h2-Step-5-Export-the-Custom-Dataset-in-Standard-YOLO-Format"><a rel="noopener" target="_blank" href="#h2-Step-5-Export-the-Custom-Dataset-in-Standard-YOLO-Format">Step 5: Export the Custom Dataset in Standard YOLO Format</a></li>
    <li id="TOC-h2-Step-6-Train-YOLO26-on-a-Custom-Dataset"><a rel="noopener" target="_blank" href="#h2-Step-6-Train-YOLO26-on-a-Custom-Dataset">Step 6: Train YOLO26 on a Custom Dataset</a></li>
    <li id="TOC-h2-Step-7-Test-YOLO26-with-Real-Time-Webcam-Object-Detection"><a rel="noopener" target="_blank" href="#h2-Step-7-Test-YOLO26-with-Real-Time-Webcam-Object-Detection">Step 7: Test YOLO26 with Real-Time Webcam Object Detection</a></li>
    <li id="TOC-h2-Step-8-Compare-YOLO26-YOLOE-26-and-Fine-Tuned-Object-Detection-Models"><a rel="noopener" target="_blank" href="#h2-Step-8-Compare-YOLO26-YOLOE-26-and-Fine-Tuned-Object-Detection-Models">Step 8: Compare YOLO26, YOLOE-26, and Fine-Tuned Object Detection Models</a></li>
    <li id="TOC-h2-Why-Fine-Tune-YOLO26-If-YOLOE-26-Already-Works"><a rel="noopener" target="_blank" href="#h2-Why-Fine-Tune-YOLO26-If-YOLOE-26-Already-Works">Why Fine-Tune YOLO26 If YOLOE-26 Already Works?</a></li>
    <li id="TOC-h2-What-This-Workflow-Covered-and-What-It-Did-Not"><a rel="noopener" target="_blank" href="#h2-What-This-Workflow-Covered-and-What-It-Did-Not">What This Workflow Covered and What It Did Not</a></li>
    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Train-YOLO26-on-a-Custom-Dataset-with-YOLOE-26-Auto-Labeling"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Auto-Labeling-a-Custom-Dataset-for-YOLO26-Object-Detection">Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling</a></h2>



<p>In this lesson, you will learn how to use YOLOE-26 visual prompting to auto-label a custom dataset, clean pseudo-labels, export YOLO training data, and fine-tune YOLO26 for class-specific object detection.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured.png?lossy=2&strip=1&webp=1" alt="train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured.png" class="wp-image-55223"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/train-yolo26-custom-dataset-yoloe-26-auto-labeling-featured.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p>This lesson is the last in a 2-part series on YOLOE-26 and open-vocabulary detection:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/mzpx3" target="_blank" rel="noreferrer noopener">YOLO26 Open-Vocabulary Object Detection with YOLOE-26</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/xrtzy" target="_blank" rel="noreferrer noopener">Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling</a></strong></em><strong> (this tutorial)</strong></li>
</ol>



<p><strong>To learn how to bootstrap a custom detector with YOLOE-26 and fine-tune YOLO26 on your own classes,</strong> <em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>In the first lesson, we stayed in the open-vocabulary world. We learned what <a href="https://arxiv.org/abs/2503.07465" target="_blank" rel="noreferrer noopener">YOLOE (Wang et al., 2025)</a> introduced, how YOLOE-26 extends those ideas into the YOLO26 family, and how text prompts, visual prompts, and prompt-free inference change the way we talk to a detector.</p>



<p>This lesson answers the next practical question:</p>



<p>What do we do once YOLOE-26 can already find the object we care about?</p>



<p>One good answer is to use it as a <strong>bootstrap engine</strong>.</p>



<p>Instead of labeling a small custom dataset box by box from scratch, we can use YOLOE-26 visual prompting to generate pseudo-labels, clean the noisy ones, export the results in standard YOLO format, and then fine-tune a smaller closed-set YOLO26 detector for the final deployment step.</p>



<p>That is exactly what we will do here.</p>



<p>We are going to build a small 2-class detector for 2 perfume bottles:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">creed_aventus</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">dior_elixir</code></li>
</ul>



<p>In this lesson, we will learn:</p>



<ul class="wp-block-list">
<li>how to structure a small pseudo-labeling workflow around YOLOE-26 visual prompts</li>



<li>how to define custom classes and reference images in a reusable config</li>



<li>how to generate raw pseudo-labels and consolidate duplicate detections</li>



<li>how to clean noisy labels and manually fix the small number of images that still fail</li>



<li>how to export the cleaned labels into standard YOLO training format</li>



<li>how to fine-tune <code data-enlighter-language="python" class="EnlighterJSRAW">yolo26s.pt</code> on the resulting dataset</li>



<li>how to compare baseline YOLO26, YOLOE-26, and the fine-tuned detector honestly</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Auto-Labeling-a-Custom-Dataset-for-YOLO26-Object-Detection"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Auto-Labeling-a-Custom-Dataset-for-YOLO26-Object-Detection">Auto-Labeling a Custom Dataset for YOLO26 Object Detection</a></h2>



<p>In Lesson 1, we showed that YOLOE-26 can find objects from prompts. That is already useful on its own.</p>



<p>But many real projects eventually want something narrower and more product-like:</p>



<ul class="wp-block-list">
<li>a fixed label list</li>



<li>a small deployment artifact</li>



<li>no prompt setup at inference time</li>



<li>a detector that can keep improving as we correct more data</li>
</ul>



<p>That is where a closed-set fine-tuned detector still makes sense.</p>



<p>In this lesson, the target problem is intentionally small and concrete. We want to detect 2 visually distinct bottles from our own photos, not from a public dataset. No Common Objects in Context (COCO) class will solve that directly. A standard YOLO26 model can usually tell us that the image contains a bottle-like object, but it cannot natively distinguish <em>which</em> bottle it is.</p>



<p>YOLOE-26 gives us a way to bridge that gap. We can show it clean reference examples of each bottle, let it search the rest of our images for similar objects, and then turn those predictions into a first-pass training set.</p>



<p>The important point is that we are not pretending the pseudo-labels will be perfect. They almost never are. We are using them to replace most of the manual labeling work, not all of it.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-82.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="517" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-82-1024x517.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55226"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-82-1024x517.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-82-1024x517.png?size=126x64&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-82-1024x517.png?size=252x127&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-82-1024x517.png?size=378x191&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-82-1024x517.png?size=504x254&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-82-1024x517.png?size=630x318&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> Workflow overview for the pseudo-labeling and fine-tuning pipeline.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuring-Your-Development-Environment"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuring-Your-Development-Environment">Configuring Your Development Environment</a></h2>



<p>To follow this guide, we need a Python environment with Ultralytics, OpenCV, Pillow, NumPy, and PyYAML installed.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="1">$ pip install -U ultralytics opencv-python pillow numpy pyyaml
</pre>



<p><strong>If you need help configuring your development environment for OpenCV, we </strong><em><strong>highly recommend</strong></em><strong> reading our </strong><em><strong><a href="https://pyimagesearch.com/2018/09/19/pip-install-opencv/" target="_blank" rel="noreferrer noopener">pip install OpenCV</a></strong></em><strong><a href="https://pyimagesearch.com/2018/09/19/pip-install-opencv/" target="_blank" rel="noreferrer noopener"> guide</a></strong>. It will have you up and running in minutes.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Project-Structure"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Project-Structure">Project Structure</a></h2>



<p>We first need to review our project directory structure.</p>



<p>Start by accessing this tutorial’s <em><strong>“Downloads”</strong></em> section to retrieve the source code and example images.</p>



<p>From there, take a look at the directory structure:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="2">yoloe26-bootstrap-finetuning/
├── configs/
│   ├── classes.yaml
│   └── reference_prompts.json
├── data/
│   ├── raw/
│   │   ├── reference/
│   │   │   ├── bottle_a/
│   │   │   └── bottle_b/
│   │   ├── train/
│   │   └── val/
│   ├── interim/
│   │   ├── pseudo_labels/
│   │   ├── cleaned_labels/
│   │   └── review_exports/
│   └── processed/
│       └── yolo_dataset/
│           ├── data.yaml
│           ├── images/
│           │   ├── train/
│           │   └── val/
│           └── labels/
│               ├── train/
│               └── val/
├── outputs/
│   ├── checkpoints/
│   │   └── yolo26s_perfume_pilot/
│   ├── figures/
│   │   └── comparison_val/
│   ├── metrics/
│   └── previews/
├── scripts/
│   ├── 01_build_reference_prompts.py
│   ├── 02_run_pseudo_labeling.py
│   ├── 03_clean_pseudo_labels.py
│   ├── 04_export_yolo_dataset.py
│   ├── 05_manual_fix_cleaned_labels.py
│   ├── 05_train_yolo26.py
│   ├── 06_webcam_demo.py
│   └── 07_evaluate_models.py
├── src/
│   └── config.py
└── yolo26s.pt</pre>



<p>Before we run anything, let us anchor the workflow to the files that matter most:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">configs/classes.yaml</code>: defines the classes, reference-image filenames, and the default pseudo-label confidence threshold.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">src/config.py</code>: centralizes path handling, class loading, and split discovery.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/01_build_reference_prompts.py</code>: lets us draw 1 or more prompt boxes on the reference images.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/02_run_pseudo_labeling.py</code>: runs YOLOE-26 visual prompting over a split and saves raw overlays.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/03_clean_pseudo_labels.py</code>: applies confidence filtering and overlap suppression.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/05_manual_fix_cleaned_labels.py</code>: lets us patch the small number of images that still need human correction.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/04_export_yolo_dataset.py</code>: converts the cleaned detections into YOLO <code data-enlighter-language="python" class="EnlighterJSRAW">.txt</code> label files and <code data-enlighter-language="python" class="EnlighterJSRAW">data.yaml</code>.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/05_train_yolo26.py</code>: fine-tunes a closed-set YOLO26 detector.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/06_webcam_demo.py</code>: loads the fine-tuned checkpoint for a live qualitative sanity check.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/07_evaluate_models.py</code>: generates side-by-side comparisons of baseline YOLO26, YOLOE-26, and the fine-tuned model.</li>
</ul>



<p>That file split is worth noticing. Instead of hiding the whole lesson inside a single notebook cell stream, we broke the workflow into small, repeatable stages. That makes the pipeline easier to rerun and easier to explain.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Step-1-Define-Custom-Object-Classes-and-Reference-Images"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Step-1-Define-Custom-Object-Classes-and-Reference-Images">Step 1: Define Custom Object Classes and Reference Images</a></h2>



<p>We start in <code data-enlighter-language="python" class="EnlighterJSRAW">configs/classes.yaml</code>.</p>



<p>This file defines our 2 custom classes, the prompt reference images for each class, and the default pseudo-label confidence threshold:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="3">project:
  model_path: "../yoloe-26s-seg.pt"
  default_split: "train"
  pseudo_label_confidence: 0.35

classes:
  - id: 0
    name: "creed_aventus"
    display_name: "Creed Aventus"
    reference_subdir: "bottle_a"
    reference_images:
      - "IMG_3825_Best.jpg"
      - "IMG_3826.jpg"
      - "IMG_3827.jpg"
      - "IMG_3849.JPG"

  - id: 1
    name: "dior_elixir"
    display_name: "Dior Sauvage Elixir"
    reference_subdir: "bottle_b"
    reference_images:
      - "IMG_3822_Best.jpg"
      - "IMG_3823.jpg"
      - "IMG_3824.jpg"
      - "IMG_3844.JPG"
      - "IMG_3846.JPG"
      - "IMG_3847.JPG"
      - "IMG_3848.JPG"</pre>



<p>There are 2 practical design choices here.</p>



<p>First, we do <strong>not</strong> rely on a single reference image per class. We let each class carry a small bundle of references. That makes the visual prompting stage more robust when a bottle is seen from a different angle, at a different size, or under slightly different lighting.</p>



<p>Second, the label names are the names we want in the final detector. The filenames of the training images do not become labels. Only the class entries in this config and the exported YOLO annotations determine the training target.</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">src/config.py</code> is the small plumbing layer that makes this ergonomic:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="4">def load_classes(paths: ProjectPaths | None = None) -> list[ClassSpec]:
    paths = paths or get_paths()
    raw = load_yaml(paths.classes_yaml)
    class_specs: list[ClassSpec] = []
    for item in raw["classes"]:
        reference_images = item.get("reference_images")
        if reference_images is None:
            reference_images = [item["reference_image"]]
        class_specs.append(
            ClassSpec(
                id=int(item["id"]),
                name=str(item["name"]),
                display_name=str(item["display_name"]),
                reference_subdir=str(item["reference_subdir"]),
                reference_images=tuple(str(image_name) for image_name in reference_images),
                review_color_bgr=tuple(item["review_color_bgr"]),
            )
        )
    return class_specs</pre>



<p>That helper is small, but it matters. Every later script reuses the same class definitions, paths, and split logic, so the whole pipeline stays consistent.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-83.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="773" height="607" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-83.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55231"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-83.png?size=126x99&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-83-300x236.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-83.png?size=378x297&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-83.png?size=504x396&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-83.png?size=630x495&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-83-768x603.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-83.png?lossy=2&strip=1&webp=1 773w" sizes="(max-width: 773px) 100vw, 773px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> The final reference prompt images used for Creed Aventus and Dior Sauvage Elixir.</figcaption></figure></div>


<p><em><strong>Note:</strong></em><em> The prompt boxes are intentionally thin in a few panels. Zoom in for a clearer view of the bounding boxes.</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Step-2-Draw-Visual-Prompt-Boxes-for-YOLOE-26-Auto-Labeling"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Step-2-Draw-Visual-Prompt-Boxes-for-YOLOE-26-Auto-Labeling">Step 2: Draw Visual Prompt Boxes for YOLOE-26 Auto-Labeling</a></h2>



<p>Once the reference images exist, <code data-enlighter-language="python" class="EnlighterJSRAW">scripts/01_build_reference_prompts.py</code> opens a region of interest (ROI) selector so we can draw 1 tight prompt box per reference image.</p>



<p>The core function is straightforward:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="5">def select_prompt_box(image_path: Path, class_spec: ClassSpec, prompt_index: int, total_prompts: int) -> list[int]:
    image = cv2.imread(str(image_path))
    window_name = f"{class_spec.display_name} ({prompt_index}/{total_prompts})"
    roi = cv2.selectROI(window_name, image, showCrosshair=True, fromCenter=False)
    cv2.destroyAllWindows()
    x, y, w, h = roi
    return [int(x), int(y), int(x + w), int(y + h)]</pre>



<p>This is one place where a graphical user interface (GUI) tool is exactly the right choice. We only do this once per reference image, and the output becomes a reusable JavaScript Object Notation (JSON) file (<code data-enlighter-language="python" class="EnlighterJSRAW">configs/reference_prompts.json</code>) for every later step.</p>



<p>Run it like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="6">python3 scripts/01_build_reference_prompts.py --overwrite</pre>



<p>The script also saves preview overlays so we can visually confirm that the prompt boxes are tight and centered on the full bottle.</p>



<h3 class="wp-block-heading">What reference_prompts.json Actually Stores</h3>



<p>It is worth pausing here, because <code data-enlighter-language="python" class="EnlighterJSRAW">configs/reference_prompts.json</code> becomes the contract between the interactive prompt-selection step and the automated pseudo-labeling step.</p>



<p>A simplified entry looks like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="7">{
  "classes": {
    "creed_aventus": {
      "class_id": 0,
      "display_name": "Creed Aventus",
      "references": [
        {
          "reference_image": "data/raw/reference/bottle_a/IMG_3825_Best.jpg",
          "xyxy": [1365, 1432, 2874, 4306],
          "preview_image": "data/interim/review_exports/reference_prompts/creed_aventus_reference_prompt_01.jpg"
        }
      ]
    }
  }
}</pre>



<p>The following 3 fields matter most:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">reference_image</code>: tells later scripts which source image to use as the prompt image</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">xyxy</code>: stores the tight visual prompt box in pixel coordinates</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">preview_image</code>: gives us a saved sanity-check overlay we can inspect before running the full pipeline</li>
</ul>



<p>This is a place where the codebase structure helps. We never have to redraw those boxes unless we deliberately want to change the prompt setup.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Step-3-Auto-Label-a-Custom-Dataset-with-YOLOE-26-Visual-Prompting"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Step-3-Auto-Label-a-Custom-Dataset-with-YOLOE-26-Visual-Prompting">Step 3: Auto-Label a Custom Dataset with YOLOE-26 Visual Prompting</a></h2>



<p>Now we can let YOLOE-26 do the first pass of the labeling work.</p>



<p>The heart of <code data-enlighter-language="python" class="EnlighterJSRAW">scripts/02_run_pseudo_labeling.py</code> is the <a href="https://docs.ultralytics.com/models/yoloe/" target="_blank" rel="noreferrer noopener">visual-prompt inference</a> call:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="8">model.set_classes([class_spec.name])
visual_prompts = {
    "bboxes": np.array([[x1, y1, x2, y2]], dtype=np.float32),
    "cls": np.array([0], dtype=np.int32),
}

results = model.predict(
    source=image_sources,
    refer_image=str(reference_image),
    visual_prompts=visual_prompts,
    predictor=YOLOEVPSegPredictor,
    conf=confidence,
    verbose=False,
)</pre>



<p>This is the critical shift from Lesson 1 to Lesson 2.</p>



<p>In Lesson 1, visual prompting was an inference trick. Here, it becomes a <strong>dataset-building primitive</strong>.</p>



<p>We take each class, loop over its reference images, and run that visual prompt across the whole split. Every detection is saved with:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">image_rel_path</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">class_id</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">class_name</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">confidence</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">x1</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">y1</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">x2</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">y2</code></li>
</ul>



<p>Because we are using multiple references per class, we also need a consolidation step. Otherwise, nearly identical prompt boxes can produce duplicate detections on the same image.</p>



<p>That is why the script includes <code data-enlighter-language="python" class="EnlighterJSRAW">consolidate_image_detections()</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="9">def consolidate_image_detections(
    detections: list[dict[str, object]],
    same_class_iou_threshold: float = 0.60,
    cross_class_iou_threshold: float = 0.80,
) -> list[dict[str, object]]:
    by_class: dict[str, list[dict[str, object]]] = defaultdict(list)
    for detection in detections:
        by_class[str(detection["class_name"])].append(detection)

    same_class_merged: list[dict[str, object]] = []
    for class_name in sorted(by_class):
        same_class_merged.extend(
            suppress_overlaps(by_class[class_name], iou_threshold=same_class_iou_threshold)
        )

    cross_class_merged = suppress_overlaps(
        same_class_merged,
        iou_threshold=cross_class_iou_threshold,
    )
    return sorted(cross_class_merged, key=lambda item: (int(item["class_id"]), -float(item["confidence"])))</pre>



<p>That function ended up being more important than it may look at first glance. Once we started using several reference images per class, raw detections could accumulate quickly. The intersection over union (IoU)-based consolidation keeps only the strongest overlapping box and makes the review artifacts much easier to inspect.</p>



<p>Run the pseudo-labeling pass like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="10">python3 scripts/02_run_pseudo_labeling.py --split train
python3 scripts/02_run_pseudo_labeling.py --split val</pre>



<p>The script saves machine-readable JavaScript Object Notation (JSON) and comma-separated values (CSV) outputs, along with human-review artifacts:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">data/interim/pseudo_labels/&lt;split&gt;_raw_predictions.json</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">data/interim/pseudo_labels/&lt;split&gt;_raw_predictions.csv</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">data/interim/review_exports/raw_predictions/*.jpg</code></li>
</ul>



<h3 class="wp-block-heading">What One Raw Pseudo-Label Record Looks Like</h3>



<p>The JSON output is designed to be both reviewable and script-friendly. A single image entry looks like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="11">{
  "image_rel_path": "data/raw/train/session_02_bottle_b_solo/IMG_3859.JPG",
  "preview_image": "data/interim/review_exports/raw_predictions/IMG_3859_raw_predictions.jpg",
  "detections": [
    {
      "class_id": 1,
      "class_name": "dior_elixir",
      "display_name": "Dior Sauvage Elixir",
      "confidence": 0.560877,
      "x1": 1559.81,
      "y1": 1861.83,
      "x2": 2542.63,
      "y2": 3571.91
    }
  ]
}</pre>



<p>This is already enough information to support the next 3 steps:</p>



<ul class="wp-block-list">
<li>visual review through the saved overlay image</li>



<li>confidence-based filtering</li>



<li>conversion into normalized YOLO training labels</li>
</ul>



<p>That is a small but important design choice. We are not saving opaque results. We are saving a format that can be inspected and corrected with simple Python scripts.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-84.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="777" height="586" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-84.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55235"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-84.png?size=126x95&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-84-300x226.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-84.png?size=378x285&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-84.png?size=504x380&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-84.png?size=630x475&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-84-768x579.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-84.png?lossy=2&strip=1&webp=1 777w" sizes="(max-width: 777px) 100vw, 777px" /></a><figcaption class="wp-element-caption"><strong>Figure 3: </strong>Raw YOLOE-26 pseudo-labels on a successful bottle-pair image before cleanup.</figcaption></figure></div>


<p>The good news is that YOLOE-26 did most of the tedious work for us.</p>



<p>The bad news is that it did not do all of it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Step-4-Clean-the-Pseudo-Labels"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Step-4-Clean-the-Pseudo-Labels">Step 4: Clean the Pseudo-Labels</a></h2>



<p>Raw pseudo-labels are a starting point, not a training set.</p>



<p>That is why <code data-enlighter-language="python" class="EnlighterJSRAW">scripts/03_clean_pseudo_labels.py</code> applies a second stage of filtering:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="12">def suppress_overlaps(
    detections: list[dict[str, object]],
    min_conf: float,
    iou_threshold: float,
) -> list[dict[str, object]]:
    filtered = [
        detection
        for detection in detections
        if float(detection["confidence"]) >= min_conf
    ]
    filtered.sort(key=lambda item: float(item["confidence"]), reverse=True)

    kept: list[dict[str, object]] = []
    for detection in filtered:
        if any(calculate_iou(detection, existing) >= iou_threshold for existing in kept):
            continue
        kept.append(detection)
    return kept</pre>



<p>This script is doing 2 things:</p>



<ul class="wp-block-list">
<li>dropping very weak detections below the confidence threshold</li>



<li>suppressing weaker overlapping boxes that still survived the earlier consolidation step</li>
</ul>



<p>We ran it like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="13">python3 scripts/03_clean_pseudo_labels.py --split train
python3 scripts/03_clean_pseudo_labels.py --split val</pre>



<p>For this project, the cleaned counts ended up like this:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-85.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="190" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-85-1024x190.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55237"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-85-1024x190.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-85-1024x190.png?size=126x23&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-85-1024x190.png?size=252x47&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-85-1024x190.png?size=378x70&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-85-1024x190.png?size=504x94&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-85-1024x190.png?size=630x117&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 1:</strong> Cleaned training and validation split summary after pseudo-label review.</figcaption></figure></div>


<p><strong>Table 1</strong> already tells an important story. This is a <strong>small pilot dataset</strong>, not a polished large-scale benchmark setup. We should expect the final fine-tuned detector to be useful, but we should not expect miracles from 41 training images.</p>



<h3 class="wp-block-heading">What One Cleaned Entry Looks Like</h3>



<p>After cleanup and manual correction, a cleaned image entry becomes the record that we trust enough to export:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="14">{
  "image_rel_path": "data/raw/train/session_02_bottle_b_solo/IMG_3858.JPG",
  "preview_image": "data/interim/review_exports/cleaned_predictions/IMG_3858_cleaned_predictions.jpg",
  "detections": [
    {
      "class_id": 1,
      "class_name": "dior_elixir",
      "display_name": "Dior Sauvage Elixir",
      "confidence": 1.0,
      "x1": 1263.0,
      "y1": 2145.0,
      "x2": 2292.0,
      "y2": 3815.0
    }
  ]
}</pre>



<p>The following 2 details are worth noticing.</p>



<p>First, the structure is intentionally almost identical to the raw prediction structure. That keeps the manual-fix script simple because it only needs to update the <code data-enlighter-language="python" class="EnlighterJSRAW">detections</code> list for an image entry instead of rewriting the whole format.</p>



<p>Second, a manual fix can store a confidence of <code data-enlighter-language="python" class="EnlighterJSRAW">1.0</code>. That does not mean the detection was magically certain. It means this box was human-approved and should survive the next export step as a trusted label.</p>



<h3 class="wp-block-heading">The 2 Training Images We Still Had to Fix</h3>



<p>Even after cleanup, 2 training images still needed human intervention:</p>



<ul class="wp-block-list">
<li>1 image had a box that needed to be relabeled from Creed to Dior</li>



<li>1 image missed the Dior bottle entirely and needed 1 manual box</li>
</ul>



<p>That is exactly why <code data-enlighter-language="python" class="EnlighterJSRAW">scripts/05_manual_fix_cleaned_labels.py</code> exists.</p>



<p>It supports the following 3 edit modes:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">--relabel-only</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--draw-box</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--clear-detections</code></li>
</ul>



<p>These were the 2 training fixes we applied:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="15">python3 scripts/05_manual_fix_cleaned_labels.py \
  --split train \
  --image IMG_3859.JPG \
  --class-name dior_elixir \
  --relabel-only

python3 scripts/05_manual_fix_cleaned_labels.py \
  --split train \
  --image IMG_3858.JPG \
  --class-name dior_elixir \
  --draw-box</pre>



<p>We also used <code data-enlighter-language="python" class="EnlighterJSRAW">--clear-detections</code> on true-negative validation images so that empty scenes stayed empty instead of carrying false positives forward.</p>



<p>For example, the helper supports a command like this for a true negative:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="16">python3 scripts/05_manual_fix_cleaned_labels.py \
  --split val \
  --image &lt;IMAGE_NAME> \
  --clear-detections</pre>



<p>That tiny branch matters more than it may look. Negative images are one of the easiest ways to teach the final closed-set detector when <strong>not</strong> to fire.</p>



<p>This is the part many pseudo-label tutorials try to glide past. We should not glide past it. The whole point of the workflow is not that the machine makes perfect labels. The point is that it reduces the manual effort to a short correction pass instead of a full box-by-box annotation session.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-86.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="565" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-86-1024x565.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55241"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-86-1024x565.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-86-1024x565.png?size=126x70&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-86-1024x565.png?size=252x139&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-86-1024x565.png?size=378x209&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-86-1024x565.png?size=504x278&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-86-1024x565.png?size=630x348&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> A raw pseudo-label failure case that still required manual correction.</figcaption></figure></div>


<p>That raw failure is exactly why we do not train directly on pseudo-labels. In this case, a short cleanup pass and 1 manual correction were enough to turn the same image into a usable training example.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-87.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="629" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-87-1024x629.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55244"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-87-1024x629.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-87-1024x629.png?size=126x77&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-87-1024x629.png?size=252x155&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-87-1024x629.png?size=378x232&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-87-1024x629.png?size=504x310&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-87-1024x629.png?size=630x387&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> The corrected cleaned-label overlay after thresholding and manual fixes.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Step-5-Export-the-Custom-Dataset-in-Standard-YOLO-Format"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Step-5-Export-the-Custom-Dataset-in-Standard-YOLO-Format">Step 5: Export the Custom Dataset in Standard YOLO Format</a></h2>



<p>After cleanup, we still need to convert the detections into standard YOLO label files.</p>



<p>That is what <code data-enlighter-language="python" class="EnlighterJSRAW">scripts/04_export_yolo_dataset.py</code> does.</p>



<p>The core conversion is handled by <code data-enlighter-language="python" class="EnlighterJSRAW">yolo_line()</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="17">def yolo_line(detection: dict[str, object], image_width: int, image_height: int) -> str:
    x1 = float(detection["x1"])
    y1 = float(detection["y1"])
    x2 = float(detection["x2"])
    y2 = float(detection["y2"])
    class_id = int(detection["class_id"])

    box_width = max(0.0, x2 - x1)
    box_height = max(0.0, y2 - y1)
    center_x = x1 + box_width / 2.0
    center_y = y1 + box_height / 2.0

    return " ".join(
        [
            str(class_id),
            f"{center_x / image_width:.6f}",
            f"{center_y / image_height:.6f}",
            f"{box_width / image_width:.6f}",
            f"{box_height / image_height:.6f}",
        ]
    )</pre>



<p>This converts corner coordinates into the <a href="https://docs.ultralytics.com/datasets/detect/" target="_blank" rel="noreferrer noopener">normalized format</a> (<code data-enlighter-language="python" class="EnlighterJSRAW">class_id</code> <code data-enlighter-language="python" class="EnlighterJSRAW">center_x</code> <code data-enlighter-language="python" class="EnlighterJSRAW">center_y</code> <code data-enlighter-language="python" class="EnlighterJSRAW">width</code> <code data-enlighter-language="python" class="EnlighterJSRAW">height</code>) that Ultralytics expects for standard object detection training.</p>



<p>Then the script writes:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">data/processed/yolo_dataset/images/&lt;split&gt;/</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">data/processed/yolo_dataset/labels/&lt;split&gt;/</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">data/processed/yolo_dataset/data.yaml</code></li>
</ul>



<p>Run it like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="18">python3 scripts/04_export_yolo_dataset.py --split train
python3 scripts/04_export_yolo_dataset.py --split val</pre>



<p>For the train split, the export summary was:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">41</code> images</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">50</code> boxes</li>
</ul>



<p>At this point, the open-vocabulary stage is done. We now have a standard YOLO detection dataset that required far less manual effort than labeling a dataset from scratch.</p>



<h3 class="wp-block-heading">What One Exported YOLO Label File Looks Like</h3>



<p>Once exported, the label files become plain YOLO detection labels. For example, a label file contains:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="19">1 0.414916 0.521709 0.240196 0.292367</pre>



<p>That line contains:</p>



<ul class="wp-block-list">
<li>class ID <code data-enlighter-language="python" class="EnlighterJSRAW">1</code>, which <code data-enlighter-language="python" class="EnlighterJSRAW">configs/classes.yaml</code> maps to <code data-enlighter-language="python" class="EnlighterJSRAW">dior_elixir</code></li>



<li>normalized center x-coordinate: <code data-enlighter-language="python" class="EnlighterJSRAW">0.414916</code> </li>



<li>normalized center y-coordinate: <code data-enlighter-language="python" class="EnlighterJSRAW">0.521709</code> </li>



<li>normalized width: <code data-enlighter-language="python" class="EnlighterJSRAW">0.240196</code> </li>



<li>normalized height: <code data-enlighter-language="python" class="EnlighterJSRAW">0.292367</code> </li>
</ul>



<p>On a pair image, we can have multiple lines:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="20">1 0.373234 0.602377 0.128143 0.507145
0 0.590858 0.507914 0.181373 0.572014</pre>



<p>That is the exact point where the open-vocabulary part of the workflow disappears. From here onward, Ultralytics training sees an ordinary 2-class object detection dataset.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Step-6-Train-YOLO26-on-a-Custom-Dataset"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Step-6-Train-YOLO26-on-a-Custom-Dataset">Step 6: Train YOLO26 on a Custom Dataset</a></h2>



<p>Now we can switch from YOLOE-26 to standard YOLO26 training.</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">scripts/05_train_yolo26.py</code> wraps the <a href="https://docs.ultralytics.com/modes/train/" target="_blank" rel="noreferrer noopener">training call</a> and records the main metadata for the run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="21">train_kwargs = {
    "data": str(dataset_yaml),
    "epochs": args.epochs,
    "imgsz": args.imgsz,
    "batch": args.batch,
    "workers": args.workers,
    "patience": args.patience,
    "project": str(checkpoints_dir),
    "name": args.run_name,
    "seed": args.seed,
    "exist_ok": args.exist_ok,
    "plots": True,
    "val": True,
}</pre>



<p>We trained the small (<code data-enlighter-language="python" class="EnlighterJSRAW">s</code>) checkpoint on Apple Metal Performance Shaders (MPS):</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="22">python3 scripts/05_train_yolo26.py \
  --weights yolo26s.pt \
  --epochs 25 \
  --imgsz 960 \
  --batch 8 \
  --patience 8 \
  --device mps</pre>



<p>Training stopped early after 15 epochs, and the best checkpoint occurred at epoch 7. <strong>Table 2</strong> lists the highest value observed for each validation metric in <code data-enlighter-language="python" class="EnlighterJSRAW">results.csv</code>:</p>



<p>Mean average precision (mAP) summarizes object detection performance. Mean average precision at an intersection over union (IoU) threshold of 0.50 (mAP50) measures performance at a single threshold. Mean average precision from 0.50 to 0.95 (mAP50-95) averages performance across multiple thresholds.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-88.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="291" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-88-1024x291.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55248"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-88-1024x291.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-88-1024x291.png?size=126x36&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-88-1024x291.png?size=252x72&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-88-1024x291.png?size=378x107&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-88-1024x291.png?size=504x143&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-88-1024x291.png?size=630x179&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 2:</strong> Best validation metrics from the fine-tuned YOLO26 perfume pilot.</figcaption></figure></div>


<p>Those are strong numbers for a small 2-class pilot, but we should interpret them carefully. The validation split here is only 9 images with 14 labeled boxes. That means 1 image can change the qualitative story by roughly 11 percentage points at the image level, and 1 missed object can move box recall by about 7 percentage points. The model learned the task well enough to become useful, yet the small data volume still shows up in the qualitative comparisons later, especially on the harder Creed holdout image.</p>



<p>It is also useful to read those numbers with the qualitative results in mind:</p>



<ul class="wp-block-list">
<li>the precision is very high, which matches the fact that the fine-tuned model stays fairly clean on negatives</li>



<li>the recall is lower, which matches the harder holdout example where Creed is still missed</li>



<li>the gap between <a href="https://docs.ultralytics.com/guides/yolo-performance-metrics/" target="_blank" rel="noreferrer noopener">mAP50 and mAP50-95</a> tells us the detector usually finds the right object, but box quality still has room to tighten up</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-90-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="512" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-90-1024x512.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55253"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-90-1024x512.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-90-1024x512.png?size=126x63&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-90-1024x512.png?size=252x126&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-90-1024x512.png?size=378x189&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-90-1024x512.png?size=504x252&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-90-1024x512.png?size=630x315&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> YOLO26 fine-tuning curves for the 2-class perfume pilot.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Step-7-Test-YOLO26-with-Real-Time-Webcam-Object-Detection"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Step-7-Test-YOLO26-with-Real-Time-Webcam-Object-Detection">Step 7: Test YOLO26 with Real-Time Webcam Object Detection</a></h2>



<p>Numerical metrics are useful, but they do not tell the whole story for a small custom detector.</p>



<p>That is why we added <code data-enlighter-language="python" class="EnlighterJSRAW">scripts/06_webcam_demo.py</code>.</p>



<p>The script loads the fine-tuned checkpoint, runs live inference on webcam frames, and overlays the current class counts:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="23">result = model.predict(
    source=frame,
    conf=args.conf,
    imgsz=args.imgsz,
    verbose=False,
)[0]
annotated = result.plot()</pre>



<p>Run it like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="24">python3 scripts/06_webcam_demo.py --conf 0.35 --device mps</pre>



<p>One practical lesson from our pilot run is that the webcam threshold needed to stay fairly low. Around <code data-enlighter-language="python" class="EnlighterJSRAW">0.35</code>, the detector recognized both bottles reliably. Pushing the threshold much higher made detections disappear too aggressively.</p>



<p>That does <strong>not</strong> mean the model is broken. It means we trained on a small, narrow dataset and should treat the confidence threshold as a tunable deployment parameter, not as a universal fixed value. In small custom detectors, a threshold of <code data-enlighter-language="python" class="EnlighterJSRAW">0.35</code> can be perfectly reasonable if it matches the behavior we want on real scenes.</p>



<p>Another useful way to frame it is this: the webcam demo is not a benchmark. It is a human-in-the-loop product check. If <code data-enlighter-language="python" class="EnlighterJSRAW">0.35</code> gives us the detections we actually want on live camera frames, that is a valid operating point for this pilot. We would only raise it once we had enough additional data to do so without collapsing recall.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-91.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="777" height="550" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-91.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55255"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-91.png?size=126x89&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-91-300x212.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-91.png?size=378x268&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-91.png?size=504x357&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-91.png?size=630x446&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-91-768x544.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-91.png?lossy=2&strip=1&webp=1 777w" sizes="(max-width: 777px) 100vw, 777px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> Live webcam inference with the fine-tuned YOLO26 perfume detector.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Step-8-Compare-YOLO26-YOLOE-26-and-Fine-Tuned-Object-Detection-Models"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Step-8-Compare-YOLO26-YOLOE-26-and-Fine-Tuned-Object-Detection-Models">Step 8: Compare YOLO26, YOLOE-26, and Fine-Tuned Object Detection Models</a></h2>



<p>The last script, <code data-enlighter-language="python" class="EnlighterJSRAW">scripts/07_evaluate_models.py</code>, creates the side-by-side figures that make the whole lesson defensible.</p>



<p>For each validation image, it renders:</p>



<ul class="wp-block-list">
<li>the original image</li>



<li>the baseline closed-set YOLO26 result</li>



<li>the YOLOE-26 visual-prompt result</li>



<li>the fine-tuned YOLO26 result</li>
</ul>



<p>Run it like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="25">python3 scripts/07_evaluate_models.py --split val --device mps</pre>



<p>The script writes the final comparison panels to <code data-enlighter-language="python" class="EnlighterJSRAW">outputs/figures/comparison_val/</code>.</p>



<p>This is where the story gets interesting, because the answer is not simply “the fine-tuned model wins everywhere.”</p>



<h3 class="wp-block-heading">What the Evaluation Script Is Doing Behind the Scenes</h3>



<p>The comparison script is worth a little extra attention because it does more than call <code data-enlighter-language="python" class="EnlighterJSRAW">predict()</code> for each of the 3 detectors.</p>



<p>For the fine-tuned YOLO26 result, we first convert the Ultralytics result object into a simple list of rows, suppress overlapping duplicates, and then rebuild a filtered result object before plotting:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="26">finetuned_rows = consolidate_image_detections(yolo_result_to_rows(finetuned_result))
finetuned_filtered_result = clone_result_with_rows(finetuned_result, finetuned_rows)
finetuned_tile = finetuned_filtered_result.plot(conf=False)</pre>



<p>That step ended up mattering for exactly the kind of issue we saw in <code data-enlighter-language="python" class="EnlighterJSRAW">IMG_3885</code>, where near-identical duplicate boxes can clutter a final figure even if the underlying prediction is basically correct.</p>



<p>The script also removes confidence text from the baseline and fine-tuned comparison panels. That was the right editorial choice here, because a YOLOE-26 confidence value and a fine-tuned YOLO26 confidence value are not directly comparable on one shared scale.</p>



<h3 class="wp-block-heading">Case 1: A Clean Success Example</h3>



<p>On <code data-enlighter-language="python" class="EnlighterJSRAW">IMG_3885</code>, the comparison is exactly what we hoped for:</p>



<ul class="wp-block-list">
<li>baseline YOLO26 sees generic bottles and extra furniture classes</li>



<li>YOLOE-26 visual prompting finds both custom bottles</li>



<li>the fine-tuned YOLO26 detector also finds both custom bottles</li>
</ul>



<p>This is the strongest example to show the workflow working end to end.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-92.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="775" height="725" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-92.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55257"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-92.png?size=126x118&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-92-300x281.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-92.png?size=378x354&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-92.png?size=504x471&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-92.png?size=630x589&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-92-768x718.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-92.png?lossy=2&strip=1&webp=1 775w" sizes="(max-width: 775px) 100vw, 775px" /></a><figcaption class="wp-element-caption"><strong>Figure 8:</strong> Side-by-side comparison on a clean validation image where all 3 approaches behave differently.</figcaption></figure></div>


<h3 class="wp-block-heading">Case 2: A Harder Holdout Where the Fine-Tuned Detector Still Misses Creed</h3>



<p>On <code data-enlighter-language="python" class="EnlighterJSRAW">IMG_3881</code>, the story changes:</p>



<ul class="wp-block-list">
<li>baseline YOLO26 still sees only generic bottle-like objects</li>



<li>YOLOE-26 visual prompting still finds both target classes</li>



<li>the fine-tuned YOLO26 detector finds Dior but misses Creed</li>
</ul>



<p>This is not a failure of the lesson. It is an honest reminder that 41 training images are enough to build a meaningful pilot, but not enough to erase every weak spot immediately.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-11.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="416" height="631" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-55260"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11.jpeg?size=126x191&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11-198x300.jpeg?lossy=2&strip=1&webp=1 198w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11.jpeg?size=252x382&lossy=2&strip=1&webp=1 252w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11.jpeg?lossy=2&strip=1&webp=1 416w" sizes="(max-width: 416px) 100vw, 416px" /></a><figcaption class="wp-element-caption"><strong>Figure 9:</strong> A harder validation image where YOLOE-26 still has stronger recall than the fine-tuned detector.</figcaption></figure></div>


<h3 class="wp-block-heading">Case 3: A Negative Image Where the Fine-Tuned Detector Stays Clean</h3>



<p>On <code data-enlighter-language="python" class="EnlighterJSRAW">IMG_3888</code>, the comparison tells a different kind of story:</p>



<ul class="wp-block-list">
<li>baseline YOLO26 predicts nothing</li>



<li>YOLOE-26 visual prompting produces a false positive</li>



<li>the fine-tuned YOLO26 detector predicts nothing</li>
</ul>



<p>This is a useful reminder that open-vocabulary prompting and closed-set specialization have different failure modes. YOLOE-26 is the better search tool. The fine-tuned YOLO26 model can be the cleaner deployment artifact once the class list is stable.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-12.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="416" height="631" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-55262"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12.jpeg?size=126x191&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12-198x300.jpeg?lossy=2&strip=1&webp=1 198w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12.jpeg?size=252x382&lossy=2&strip=1&webp=1 252w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12.jpeg?lossy=2&strip=1&webp=1 416w" sizes="(max-width: 416px) 100vw, 416px" /></a><figcaption class="wp-element-caption"><strong>Figure 10:</strong> Negative-image comparison showing a YOLOE-26 false positive and a cleaner fine-tuned YOLO26 output.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Fine-Tune-YOLO26-If-YOLOE-26-Already-Works"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Fine-Tune-YOLO26-If-YOLOE-26-Already-Works">Why Fine-Tune YOLO26 If YOLOE-26 Already Works?</a></h2>



<p>This is the right question to ask, especially after looking at the comparison figures.</p>



<p>In our pilot, YOLOE-26 visual prompting is often the strongest approach in terms of raw recall. It also tends to assign higher confidence scores to those detections.</p>



<p>That does <strong>not</strong> make the fine-tuning step pointless.</p>



<p>Here is the practical framing:</p>



<ul class="wp-block-list">
<li><strong>YOLOE-26 is the discovery and bootstrapping tool.</strong></li>



<li><strong>Fine-tuned YOLO26 is the deployment candidate for a stable label set.</strong></li>
</ul>



<p>There are 4 reasons that distinction still matters.</p>



<h3 class="wp-block-heading">Prompt Setup Disappears at Inference Time</h3>



<p>The fine-tuned YOLO26 model does not need prompt boxes, reference images, or prompt management logic. We just load the checkpoint and run inference.</p>



<h3 class="wp-block-heading">The Artifact Is Narrower and Easier to Reason About</h3>



<p>Once the class list is fixed, a 2-class closed-set detector is simpler to package, explain, and maintain than a prompt-driven open-vocabulary workflow.</p>



<h3 class="wp-block-heading">It Can Get Cleaner as Corrected Data Accumulates</h3>



<p>The current pilot used 41 training images. If we keep correcting labels and adding more scenes, the fine-tuned detector should get better exactly where it is weakest today.</p>



<h3 class="wp-block-heading">Confidence Values Are Not Directly Comparable Across the 2 Models</h3>



<p>This point matters a lot. A 0.90 score from YOLOE-26 and a 0.45 score from the fine-tuned YOLO26 model are not the same kind of number. They come from different training setups and different decision surfaces. We should compare them by behavior on held-out images, not by treating the raw confidence values as if they lived on one universal scale.</p>



<p>So the right takeaway is not “fine-tuning instantly beats YOLOE-26.” The right takeaway is:</p>



<p>YOLOE-26 helped us create a usable class-specific detector <strong>far faster</strong> than a manual labeling workflow would have.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-What-This-Workflow-Covered-and-What-It-Did-Not"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-What-This-Workflow-Covered-and-What-It-Did-Not">What This Workflow Covered and What It Did Not</a></h2>



<p>In this lesson, we covered the entire practical pipeline:</p>



<ul class="wp-block-list">
<li>class configuration</li>



<li>reference prompt selection</li>



<li>visual-prompt pseudo-labeling</li>



<li>confidence cleanup</li>



<li>manual patching of edge cases</li>



<li>YOLO-format export</li>



<li>fine-tuning</li>



<li>webcam inference</li>



<li>qualitative comparison</li>
</ul>



<p>What we did <strong>not</strong> cover is equally important:</p>



<ul class="wp-block-list">
<li>we did not train YOLOE-26 itself</li>



<li>we did not build a large-scale benchmark</li>



<li>we did not prove that 41 images are enough for a production-ready custom detector in every setting</li>
</ul>



<p>This was a pilot workflow, and it succeeded as a pilot workflow. It gave us a fast, credible way to move from open-vocabulary search to a real fine-tuned detector on our own classes.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>Lesson 1 showed how YOLOE-26 expands YOLO into the open-vocabulary world.</p>



<p>Lesson 2 showed why that matters in practice.</p>



<p>We used YOLOE-26 visual prompting to auto-label a small custom dataset, cleaned the noisy predictions, fixed the few images that still needed human help, exported the result into standard YOLO format, and fine-tuned <code data-enlighter-language="python" class="EnlighterJSRAW">yolo26s.pt</code> into a 2-class perfume detector that we could run both on validation images and in a live webcam demo.</p>



<p>The most important result is not a single metric. It is the workflow itself.</p>



<p>Instead of starting with unlabeled data and creating every annotation by hand, we used open-vocabulary detection as the first draft of a custom training set. That is the bridge between flexible promptable detection and a simpler deployment model.</p>



<p>If we wanted to keep going from here, the next improvements would be obvious:</p>



<ul class="wp-block-list">
<li>collect more Creed-heavy training views</li>



<li>add more negative scenes</li>



<li>add more cluttered backgrounds</li>



<li>rerun the exact same pipeline</li>
</ul>



<p>That is a good sign. It means we do not need a different system. We just need to process more corrected data through the same pipeline.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V</strong><strong>. </strong>“Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/xrtzy" target="_blank" rel="noreferrer noopener">https://pyimg.co/xrtzy</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling" data-enlighter-group="27">@incollection{Singh_2026_train-yolo26-custom-dataset-yoloe-26-auto-labeling,
  author = {Vikram Singh},
  title = {{Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/xrtzy},
}
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/31/train-yolo26-on-a-custom-dataset-with-yoloe-26-auto-labeling/">Train YOLO26 on a Custom Dataset with YOLOE-26 Auto-Labeling</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>YOLO26 Open-Vocabulary Object Detection with YOLOE-26</title>
		<link>https://pyimagesearch.com/2026/08/24/yolo26-open-vocabulary-object-detection-with-yoloe-26/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 24 Aug 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Deep Learning]]></category>
		<category><![CDATA[Object Detection]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[YOLO]]></category>
		<category><![CDATA[computer vision]]></category>
		<category><![CDATA[object detection]]></category>
		<category><![CDATA[open-vocabulary detection]]></category>
		<category><![CDATA[text prompting]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[ultralytics]]></category>
		<category><![CDATA[visual prompting]]></category>
		<category><![CDATA[yolo26]]></category>
		<category><![CDATA[yoloe-26]]></category>
		<category><![CDATA[zero-shot object detection]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=55086</guid>

					<description><![CDATA[<p>Table of Contents YOLO26 Open-Vocabulary Object Detection with YOLOE-26 Understanding Closed-Set YOLO Object Detection Where YOLO Fits Among Object Detection Models Why Open-Vocabulary and Zero-Shot Object Detection Matter What YOLOE Introduced How YOLOE-26 Extends Open-Vocabulary Detection to YOLO26 How the&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/24/yolo26-open-vocabulary-object-detection-with-yoloe-26/">YOLO26 Open-Vocabulary Object Detection with YOLOE-26</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-YOLO26-Open-Vocabulary-Object-Detection-YOLOE-26"><a rel="noopener" target="_blank" href="#h1-YOLO26-Open-Vocabulary-Object-Detection-YOLOE-26">YOLO26 Open-Vocabulary Object Detection with YOLOE-26</a></li>

    <li id="TOC-h2-Understanding-Closed-Set-YOLO-Object-Detection"><a rel="noopener" target="_blank" href="#h2-Understanding-Closed-Set-YOLO-Object-Detection">Understanding Closed-Set YOLO Object Detection</a></li>

    <li id="TOC-h2-Where-YOLO-Fits-Among-Object-Detection-Models"><a rel="noopener" target="_blank" href="#h2-Where-YOLO-Fits-Among-Object-Detection-Models">Where YOLO Fits Among Object Detection Models</a></li>

    <li id="TOC-h2-Why-Open-Vocabulary-Zero-Shot-Object-Detection-Matter"><a rel="noopener" target="_blank" href="#h2-Why-Open-Vocabulary-Zero-Shot-Object-Detection-Matter">Why Open-Vocabulary and Zero-Shot Object Detection Matter</a></li>

    <li id="TOC-h2-What-YOLOE-Introduced"><a rel="noopener" target="_blank" href="#h2-What-YOLOE-Introduced">What YOLOE Introduced</a></li>

    <li id="TOC-h2-How-YOLOE-26-Extends-Open-Vocabulary-Detection-YOLO26"><a rel="noopener" target="_blank" href="#h2-How-YOLOE-26-Extends-Open-Vocabulary-Detection-YOLO26">How YOLOE-26 Extends Open-Vocabulary Detection to YOLO26</a></li>

    <li id="TOC-h2-How-YOLOE-26-Flow-Works"><a rel="noopener" target="_blank" href="#h2-How-YOLOE-26-Flow-Works">How the YOLOE-26 Flow Works</a></li>

    <li id="TOC-h2-How-YOLOE-26-Trains-Open-Vocabulary-Object-Detection"><a rel="noopener" target="_blank" href="#h2-How-YOLOE-26-Trains-Open-Vocabulary-Object-Detection">How YOLOE-26 Trains for Open-Vocabulary Object Detection</a></li>

    <li id="TOC-h2-Configuring-Development-Environment"><a rel="noopener" target="_blank" href="#h2-Configuring-Development-Environment">Configuring Your Development Environment</a></li>

    <li id="TOC-h2-YOLOE-26-Object-Detection-Benchmarks-Performance"><a rel="noopener" target="_blank" href="#h2-YOLOE-26-Object-Detection-Benchmarks-Performance">YOLOE-26 Object Detection Benchmarks and Performance</a></li>

    <li id="TOC-h2-Hands-On-Text-Prompting"><a rel="noopener" target="_blank" href="#h2-Hands-On-Text-Prompting">Hands-On with Text Prompting</a></li>

    <li id="TOC-h2-Why-Visual-Prompting-Is-Real-Superpower"><a rel="noopener" target="_blank" href="#h2-Why-Visual-Prompting-Is-Real-Superpower">Why Visual Prompting Is the Real Superpower</a></li>

    <li id="TOC-h2-Optional-Extension-Prompting-Separate-Reference-Image"><a rel="noopener" target="_blank" href="#h2-Optional-Extension-Prompting-Separate-Reference-Image">Optional Extension: Prompting from a Separate Reference Image</a></li>

    <li id="TOC-h2-YOLOE-26-Prompt-Free-Open-Vocabulary-Object-Detection"><a rel="noopener" target="_blank" href="#h2-YOLOE-26-Prompt-Free-Open-Vocabulary-Object-Detection">YOLOE-26 Prompt-Free Open-Vocabulary Object Detection</a></li>

    <li id="TOC-h2-Where-YOLOE-26-Beats-YOLO26-Where-It-Does-Not"><a rel="noopener" target="_blank" href="#h2-Where-YOLOE-26-Beats-YOLO26-Where-It-Does-Not">Where YOLOE-26 Beats YOLO26, and Where It Does Not</a></li>

    <li id="TOC-h2-Common-Failure-Modes-How-Debug-Them"><a rel="noopener" target="_blank" href="#h2-Common-Failure-Modes-How-Debug-Them">Common Failure Modes and How to Debug Them</a></li>

    <li id="TOC-h2-One-Deployment-Detail-You-Should-Not-Miss"><a rel="noopener" target="_blank" href="#h2-One-Deployment-Detail-You-Should-Not-Miss">One Deployment Detail You Should Not Miss</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-YOLO26-Open-Vocabulary-Object-Detection-YOLOE-26"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-YOLO26-Open-Vocabulary-Object-Detection-YOLOE-26">YOLO26 Open-Vocabulary Object Detection with YOLOE-26</a></h2>



<p>In this lesson, you will learn how YOLOE evolved into YOLOE-26, how open-vocabulary detection works, and how to use text prompts, visual prompts, and prompt-free inference with Ultralytics.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured.png?lossy=2&strip=1&webp=1" alt="yolo26-open-vocabulary-object-detection-yoloe-26-featured.png" class="wp-image-55123"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/yolo26-open-vocabulary-object-detection-yoloe-26-featured.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p>This lesson is the 1st in a 2-part series on <strong>YOLOE-26 and open-vocabulary detection</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/mzpx3" target="_blank" rel="noreferrer noopener">YOLO26 Open-Vocabulary Object Detection with YOLOE-26</a></strong></em><strong> (this tutorial)</strong></li>



<li><em>Lesson 2</em></li>
</ol>



<p><strong>To learn how to use YOLOE-26 for open-vocabulary object detection with text prompts, visual prompts, and prompt-free inference,</strong> <em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<p>In the last YOLO26 lesson, we stayed in the closed-set world. We loaded a modern YOLO detector, ran it on images and video, and saw just how fast and polished the Ultralytics pipeline has become.</p>



<p>But closed-set detection has a hard limit. A model can only detect the classes it learned during training.</p>



<p>That sounds obvious until you hit it in practice.</p>



<p>You may want to detect a multimeter, a barcode scanner, a microscope slide box, a soldering iron, or a specific logo in a warehouse photo. A standard YOLO model cannot suddenly understand those categories just because you typed their names. If the class was not part of training, the model either misses it or maps it to the closest thing it already knows.</p>



<p>That is the problem <a href="https://docs.ultralytics.com/models/yoloe/" target="_blank" rel="noreferrer noopener">YOLOE</a> was built to solve.</p>



<p>This lesson is the foundation piece for the two-part series. We are not fine-tuning anything yet. We are first building the mental model you need before the workflow gets more ambitious in Lesson 2.</p>



<p>In this lesson, you will learn:</p>



<ul class="wp-block-list">
<li>what open-vocabulary detection actually means in plain language</li>



<li>what YOLOE introduced to the YOLO family</li>



<li>how YOLOE-26 extends that idea into the YOLO26 generation</li>



<li>how to run text-prompted, visual-prompted, and prompt-free inference</li>



<li>when YOLOE-26 is the right tool and when a standard YOLO26 model is still the better choice</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Understanding-Closed-Set-YOLO-Object-Detection"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Understanding-Closed-Set-YOLO-Object-Detection">Understanding Closed-Set YOLO Object Detection</a></h2>



<p>Let us begin with the constraint that defines standard YOLO models.</p>



<p>A regular YOLO26 detector is excellent at recognizing the categories it was trained on. But it still operates inside a fixed label space. If the class is outside that label space, the model has no mechanism to dynamically add it at inference time.</p>



<p>That is the itch this lesson scratches.</p>



<p>The first example below runs a standard YOLO26 detector on the familiar <code data-enlighter-language="python" class="EnlighterJSRAW">bus.jpg</code> sample image that ships with Ultralytics:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="1">from ultralytics import YOLO
from ultralytics.utils import ASSETS

baseline_model = YOLO("yolo26s.pt")
baseline_results = baseline_model.predict(ASSETS / "bus.jpg", conf=0.25)
baseline_results[0].show()
</pre>



<p>In that image, YOLO26 does exactly what you would expect. It finds the obvious closed-set classes such as people and the bus itself.</p>



<p>The problem shows up when your application stops looking like a benchmark and starts looking like the real world. In production, the target object list is often messy, narrow, and unstable. Maybe your robotics pipeline needs to find a clamp that appears in only one manufacturing line. Maybe your e-commerce workflow needs to find a specific product family whose packaging changed last quarter. Maybe your lab automation setup needs to distinguish one kind of equipment tray from another.</p>



<p>In all of those scenarios, a closed-set model puts you in one of 2 boxes:</p>



<ul class="wp-block-list">
<li>the class already exists in the model, so you are fine</li>



<li>the class does not exist, so you need a new data collection and training workflow</li>
</ul>



<p>That second path is expensive. You need images. You need labels. You need training time. You need evaluation. You probably need iteration because the first pass will not be clean enough.</p>



<p>This is why open-vocabulary detection matters. It does not replace training forever, but it gives you a much more flexible first step.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-62.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="646" height="662" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-62.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55126"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-62.png?size=126x129&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-62-293x300.png?lossy=2&strip=1&webp=1 293w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-62.png?size=378x387&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-62.png?size=504x516&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-62.png?lossy=2&strip=1&webp=1 646w" sizes="(max-width: 646px) 100vw, 646px" /></a><figcaption class="wp-element-caption"><strong>Figure 1: </strong>Standard YOLO26 inference on the sample bus image, illustrating the closed-set starting point.</figcaption></figure></div>


<p>This is the key mental shift for the rest of the article. YOLOE does not just make YOLO “a bit better.” It changes how you specify what the model should look for.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Where-YOLO-Fits-Among-Object-Detection-Models"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Where-YOLO-Fits-Among-Object-Detection-Models">Where YOLO Fits Among Object Detection Models</a></h2>



<p>Before going further, it helps to know where YOLO sits among object detection models generally, since not every detector works the same way under the hood.</p>



<p>Most object detection models fall into 1 of 2 architectural families. <strong>Two-stage detectors</strong> (e.g., Faster Region-based Convolutional Neural Network (R-CNN) implementations available through Detectron2) first propose candidate regions in an image, then classify each region separately. This tends to produce strong accuracy, but the two-pass design costs speed. <strong>One-stage detectors</strong>, the family YOLO belongs to, predict boxes and classes in a single forward pass. That speed advantage helped make YOLO a common choice for real-time applications, historically trading some accuracy for substantially faster inference.</p>



<p>That architectural split is only half the picture, though, and it is the half most tutorials stop at. A second, independent axis matters just as much for this lesson: whether the class list is closed or open. Whether a detector is one-stage or two-stage, the overwhelming majority share the same constraint: a fixed set of classes, usually drawn from a benchmark like Common Objects in Context (COCO), locked in at training time. To detect anything outside that list, you have to collect new images, label them, and train an object detection model from scratch on the updated data. That process works, but it is slow, and it has to be repeated every time your target categories change.</p>



<p>YOLO26 is a fast, one-stage, closed-set detector. It is excellent at what it was trained to recognize but is bound by that same COCO-style constraint everywhere else. YOLOE-26 keeps YOLO&#8217;s one-stage speed but breaks the second constraint. That is the shift the rest of this lesson is about.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Open-Vocabulary-Zero-Shot-Object-Detection-Matter"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Open-Vocabulary-Zero-Shot-Object-Detection-Matter">Why Open-Vocabulary and Zero-Shot Object Detection Matter</a></h2>



<p>Before we touch YOLOE itself, let us make the term <strong>open vocabulary</strong> concrete.</p>



<p>In a closed-set detector, the label list is fixed ahead of time. In an open-vocabulary detector, the model can work from language prompts, visual references, or a built-in broader vocabulary instead of a single hardcoded class list.</p>



<p>The practical benefit is simple: you can ask the detector for new concepts without rebuilding the whole model every time the problem changes. That is especially useful when the object is outside everyday benchmark classes, when the label list changes often, or when you want to bootstrap a later fine-tuning workflow.</p>



<p>This is closely related to what is called zero-shot detection in the research literature: the ability to detect object categories the model was never explicitly trained on. Open-vocabulary detection is the broader, more flexible version of that idea, built around accepting arbitrary prompts at inference time rather than a fixed set of unseen classes.</p>



<p>Earlier systems (e.g., Grounded Language-Image Pre-training (GLIP), Open-World Localization Vision Transformer (OWL-ViT), and <a href="https://github.com/IDEA-Research/GroundingDINO" target="_blank" rel="noreferrer noopener">Grounding DINO</a>) proved that open-vocabulary detection works, but they also made clear how expensive the usual vision-language path can become.</p>



<p>YOLOE matters because it tries to keep that promptable behavior while staying in the real-time YOLO regime.</p>



<p>For PyImageSearch readers who have already trained detectors before, the right framing is this: YOLOE-26 is not a replacement for every older YOLO workflow. It is a new capability layer on top of familiar YOLO deployment patterns.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-What-YOLOE-Introduced"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-What-YOLOE-Introduced">What YOLOE Introduced</a></h2>



<p>YOLOE, introduced in the paper <a href="https://arxiv.org/abs/2503.07465" target="_blank" rel="noreferrer noopener">YOLOE: Real-Time Seeing Anything</a>, takes the familiar YOLO workflow and adds open-vocabulary behavior on top of it.</p>



<p>Instead of being locked to a fixed class list, YOLOE can operate through 3 different prompting modes:</p>



<ul class="wp-block-list">
<li><strong>Text prompting:</strong> tells the model what classes to look for using words</li>



<li><strong>Visual prompting:</strong> shows the model an example object and asks it to find more like it</li>



<li><strong>Prompt-free mode:</strong> uses a built-in open vocabulary without supplying your own prompts at inference time</li>
</ul>



<p>This is the core idea you should carry forward. YOLOE is still part of the YOLO family. It still feels like Ultralytics. But it replaces the “fixed class list only” assumption with a more flexible prompting interface.</p>



<p>The reason that matters is simple. In a text-prompted setting, the detector is no longer just asking, “Does this image contain one of the classes already in its label space?” It is also asking, “How well does this region align with the prompt concept it was given?” That is the conceptual jump.</p>



<p>You do not need to understand every detail of the paper to benefit from it. For this lesson, the important point is that YOLOE creates a bridge between image features and promptable concepts. Sometimes those concepts come from text. Sometimes they come from visual examples. Sometimes they come from a prebuilt broader vocabulary.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-63.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="424" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-63-1024x424.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55128"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-63-1024x424.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-63-1024x424.png?size=126x52&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-63-1024x424.png?size=252x104&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-63-1024x424.png?size=378x157&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-63-1024x424.png?size=504x209&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-63-1024x424.png?size=630x261&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> The 3 prompting modes supported by YOLOE (<a href="https://cdn.jsdelivr.net/gh/ultralytics/assets@main/docs/yoloe-visualization.avif" target="_blank" rel="noreferrer noopener">source</a>)</figcaption></figure></div>


<p>At this point, the most useful question is not “How does every module work?” It is “What new things can we now ask the model to do?” The answer is that YOLOE gives you multiple ways to specify the object of interest, which is exactly what a closed-set detector lacks.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-YOLOE-26-Extends-Open-Vocabulary-Detection-YOLO26"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-YOLOE-26-Extends-Open-Vocabulary-Detection-YOLO26">How YOLOE-26 Extends Open-Vocabulary Detection to YOLO26</a></h2>



<p>Earlier YOLOE releases were shipped in YOLOv8-based and YOLO11-based model families. YOLOE-26 brings the same open-vocabulary idea into the YOLO26 family.</p>



<p>The following 2 points matter here. YOLO26 introduced a cleaner end-to-end design with native non-maximum suppression (NMS)-free inference, simpler heads, and a stronger accuracy-latency balance. YOLOE-26 inherits that foundation while keeping the promptable modes that made YOLOE interesting in the first place.</p>



<p>Architecturally, it still follows the familiar YOLO pattern: a backbone for feature extraction, a neck for multi-scale fusion, and a prediction head that turns fused features into boxes, masks, and region-level representations.</p>



<p>According to the Ultralytics docs, YOLOE-26:</p>



<ul class="wp-block-list">
<li>extends the YOLOE family onto the YOLO26 backbone</li>



<li>inherits the NMS-free end-to-end design of YOLO26</li>



<li>supports text prompting, visual prompting, and prompt-free inference</li>



<li>is available across 5 scales: <code data-enlighter-language="python" class="EnlighterJSRAW">n</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">s</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">m</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">l</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">x</code></li>
</ul>



<h3 class="wp-block-heading">Model Family and Checkpoints</h3>



<p>The naming convention is easy to miss when you first see the model files:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">yoloe-26n-*</code>: nano variant</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">yoloe-26s-*</code>: small variant</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">yoloe-26m-*</code>: medium variant</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">yoloe-26l-*</code>: large variant</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">yoloe-26x-*</code>: extra-large variant</li>
</ul>



<p>For this lesson, we are using the <code data-enlighter-language="python" class="EnlighterJSRAW">s</code> scale so the workflow stays light enough to reproduce easily. We are also using <code data-enlighter-language="python" class="EnlighterJSRAW">-seg</code> checkpoints because the overlays are easier to inspect visually and align well with the official Ultralytics examples. The released pretrained YOLOE checkpoints are segmentation-first, which is why the lesson uses segmentation weights even when the conceptual discussion focuses on detection behavior.</p>



<h3 class="wp-block-heading">Choosing the Right Checkpoint</h3>



<p>In practice, there are really 2 checkpoint families to keep straight:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">yoloe-26*-seg.pt</code>: for <strong>text prompting and visual prompting</strong></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">yoloe-26*-seg-pf.pt</code>: for <strong>prompt-free inference</strong></li>
</ul>



<p>The scale suffix then controls the usual size-speed-accuracy tradeoff:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">n</code>: for the smallest and lightest deployment target</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">s</code>: for a practical small-model starting point</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">m</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">l</code>: when you can spend more compute for stronger quality</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">x</code>: when you want the highest-end model in the family</li>
</ul>



<p>For Lesson 1, the <code data-enlighter-language="python" class="EnlighterJSRAW">s</code> variant is the right teaching checkpoint. It is large enough to show the behavior clearly, but still light enough that readers can reproduce the examples without needing a heavyweight setup. The segmentation-first checkpoints also help because masks make qualitative inspection easier, even when our discussion is mostly about the detection logic.</p>



<p>This is also where Lesson 2 starts to come into focus. We are not reproducing the full YOLOE <a href="https://docs.ultralytics.com/modes/train/" target="_blank" rel="noreferrer noopener">training pipeline</a> here, but we will borrow the same high-level idea later: use an open-vocabulary model to help create labels for a narrower downstream detector.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-64-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="373" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-64-1024x373.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55130"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-64-1024x373.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-64-1024x373.png?size=126x46&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-64-1024x373.png?size=252x92&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-64-1024x373.png?size=378x138&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-64-1024x373.png?size=504x184&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-64-1024x373.png?size=630x229&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> YOLOE architecture overview from the official documentation or paper (<a href="https://raw.githubusercontent.com/THU-MIG/yoloe/main/figures/pipeline.svg" target="_blank" rel="noreferrer noopener">source</a>)</figcaption></figure></div>


<p>The following 3 architecture details are worth calling out while that figure is on screen:</p>



<ul class="wp-block-list">
<li><strong>Re-parameterizable Region-Text Alignment (</strong><strong>RepRTA</strong><strong>):</strong> supports text-prompted detection.</li>



<li><strong>Semantic-Activated Visual Prompt Encoder (</strong><strong>SAVPE</strong><strong>):</strong> supports visual-prompted detection.</li>



<li><strong>Lazy Region-Prompt Contrast (</strong><strong>LRPC</strong><strong>):</strong> supports prompt-free open-vocabulary inference.</li>
</ul>



<p>You do not need to memorize those names. The practical takeaway is that YOLOE adds dedicated machinery for each prompting mode while preserving the familiar YOLO deployment feel.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-YOLOE-26-Flow-Works"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-YOLOE-26-Flow-Works">How the YOLOE-26 Flow Works</a></h2>



<p>At this point, it helps to slow down and build a slightly more mechanical mental model of what is happening inside the model.</p>



<p>At a high level, YOLOE-26 still begins like a normal YOLO pipeline. The input image goes through a backbone that extracts visual features, then through a neck that fuses those features across scales so small, medium, and large objects can all be represented well. Up to that point, the story still feels very familiar to anyone who has used YOLO before.</p>



<p>The open-vocabulary twist happens after those visual features have been built.</p>



<p>Conceptually, you can think of YOLOE-26 as doing 5 stages:</p>



<ul class="wp-block-list">
<li><strong>Input image to visual features:</strong> the image is converted into multi-scale feature maps by the backbone.</li>



<li><strong>Feature fusion:</strong> the neck combines information across scales so object evidence is available at multiple resolutions.</li>



<li><strong>Region prediction:</strong> the model predicts boxes and, in the segmentation checkpoints used here, masks as well.</li>



<li><strong>Prompt alignment:</strong> the model also needs a way to compare each candidate region against some notion of “what you are asking for.”</li>



<li><strong>Similarity scoring to final detections:</strong> region-level visual features are matched against prompt embeddings, and the best matches become named detections.</li>
</ul>



<p>Another useful way to think about the head is this: boxes and masks still come from a normal YOLO-style prediction path, but class scoring is no longer a fixed set of logits over a closed label list. Instead, each candidate region is compared against prompt embeddings, and that similarity score takes over the role that fixed class scores play in a standard detector.</p>



<p>If you want a simple intuition, think of standard YOLO as answering, “Where are the objects already in the label space?” YOLOE-26 answers two questions at once: “Where are the candidate objects?” and “Which of these candidate regions best matches the prompt concept it was given?”</p>



<p>One important note: the simplified flow diagram below is a <strong>conceptual teaching figure</strong>, not a literal one-to-one tensor graph from the paper. That is intentional. For a blog lesson, the goal is to make the pipeline intuitive before readers dive into implementation details.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-65.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="409" height="614" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-65.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55132"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-65.png?size=126x189&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-65-200x300.png?lossy=2&strip=1&webp=1 200w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-65.png?size=252x378&lossy=2&strip=1&webp=1 252w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-65.png?lossy=2&strip=1&webp=1 409w" sizes="(max-width: 409px) 100vw, 409px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> Conceptual YOLOE-26 inference flow, showing how candidate region features are matched against text, visual, or built-in vocabulary representations.</figcaption></figure></div>


<h3 class="wp-block-heading">What Each Prompt Mode Changes</h3>



<p>The cleanest way to understand the architecture is to ask what changes between the 3 prompting modes.</p>



<p>For <strong>text prompting</strong>, YOLOE uses <strong>RepRTA</strong>. According to the YOLOE paper, this module refines pretrained text embeddings through a lightweight auxiliary network so the prompt representation aligns better with the detector’s visual region features. The important deployment detail is that this lightweight refinement can be folded back into the model at inference time, which is why text prompting does not introduce the kind of heavy runtime penalty many vision-language models do.</p>



<p>For <strong>visual prompting</strong>, YOLOE uses <strong>SAVPE</strong>. Instead of starting from words, it starts from a reference object. The prompt encoder uses semantic and activation cues from that reference so the detector can look for visually similar regions elsewhere. That is why visual prompting feels closer to one-shot retrieval than to ordinary closed-set classification.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-66.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="687" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-66-1024x687.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55135"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-66-1024x687.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-66-1024x687.png?size=126x85&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-66-1024x687.png?size=252x169&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-66-1024x687.png?size=378x254&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-66-1024x687.png?size=504x338&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-66-1024x687.png?size=630x423&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> Conceptual visual-prompting flow in YOLOE-26, from reference object to matched regions.</figcaption></figure></div>


<p>For <strong>prompt-free mode</strong>, YOLOE uses <strong>LRPC</strong>. Here the model does not wait for an external text prompt at all. It uses a built-in vocabulary and specialized internal embeddings, then scores candidate regions against that internal vocabulary. The Ultralytics docs describe this as open-set recognition using internal embeddings trained on large vocabularies, which is what lets prompt-free mode run without an external prompt encoder at inference time.</p>



<p>This also explains an important checkpoint detail for readers. Text and visual prompting use the same main YOLOE checkpoint family, while prompt-free mode uses separate <code data-enlighter-language="python" class="EnlighterJSRAW">-pf</code> weights because those models are trained as built-in large-vocabulary variants rather than prompt-conditioned ones.</p>



<h3 class="wp-block-heading">Where YOLO26 Changes the Base</h3>



<p>Now add the YOLO26 side of the story.</p>



<p>The earlier YOLOE families were built on prior YOLO backbones, and YOLOE-26 inherits the lighter, native end-to-end design of YOLO26. According to the YOLO26 paper, that includes NMS-free end-to-end inference, a lighter head with Distribution Focal Loss (DFL) removed, and a training recipe designed to better match the inference-time head. In practice, that means YOLOE-26 is not just “YOLOE with a new name.” It is YOLOE running on a cleaner deployment-oriented detector backbone.</p>



<p>The key intuition is this: <strong>YOLOE contributes the promptable alignment machinery, and YOLO26 contributes the faster, simpler end-to-end detector base.</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-YOLOE-26-Trains-Open-Vocabulary-Object-Detection"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-YOLOE-26-Trains-Open-Vocabulary-Object-Detection">How YOLOE-26 Trains for Open-Vocabulary Object Detection</a></h2>



<p>Readers usually do not need the full training recipe, but they do benefit from understanding what is trained differently.</p>



<h3 class="wp-block-heading">How Text, Visual, and Prompt-Free Training Diverge</h3>



<p>For text prompting, the paper explains that pretrained text embeddings are refined by a lightweight auxiliary network before being aligned with visual region features. That refinement step is a big part of why YOLOE can work with prompts effectively without dragging a large language branch through the full deployment path.</p>



<p>For visual prompting, the model learns how to turn a reference object into a useful prompt representation rather than treating the crop as a raw patch match. That is what SAVPE is doing conceptually: learning a compact visual prompt that can be compared against candidate regions.</p>



<p>For prompt-free mode, the model is trained to work against a built-in vocabulary and internal embedding space, so it can still perform open-vocabulary recognition even when no external prompt is supplied.</p>



<p>Again, the lesson-level takeaway is not every training detail. The takeaway is that YOLOE is trained to bring <strong>regions and prompts into a comparable embedding space</strong>, then YOLOE-26 places that behavior on top of the more deployment-friendly YOLO26 detector.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-67.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="586" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-67-1024x586.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55137"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-67-1024x586.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-67-1024x586.png?size=126x72&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-67-1024x586.png?size=252x144&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-67-1024x586.png?size=378x216&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-67-1024x586.png?size=504x288&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-67-1024x586.png?size=630x361&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> Conceptual training intuition for RepRTA, in which cached text prompts are refined and aligned with the detector’s object embeddings before being re-parameterized for efficient inference.</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuring-Development-Environment"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuring-Development-Environment">Configuring Your Development Environment</a></h2>



<p>To follow this guide, you need to install the Ultralytics package.</p>



<p>Luckily, Ultralytics is pip-installable:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="3">$ pip install -U ultralytics
</pre>



<p>If you want to be a little safer for local runs, you can use:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="4">$ pip install -U ultralytics opencv-python matplotlib
</pre>



<p><strong>If you need help configuring your development environment for OpenCV, we </strong><em><strong>highly recommend</strong></em><strong> reading our </strong><a href="https://pyimagesearch.com/2018/09/19/pip-install-opencv/" target="_blank" rel="noreferrer noopener"><strong><em>pip install OpenCV</em> guide</strong></a>. It will have you up and running in minutes.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-YOLOE-26-Object-Detection-Benchmarks-Performance"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-YOLOE-26-Object-Detection-Benchmarks-Performance">YOLOE-26 Object Detection Benchmarks and Performance</a></h2>



<p>Before writing code, let us calibrate expectations.</p>



<p>Ultralytics reports that YOLOE-L and YOLOE26-L preserve near-identical inference speed to their underlying closed-set counterparts, while adding open-vocabulary capability. The same documentation also reports stronger Large Vocabulary Instance Segmentation (LVIS) open-vocabulary performance for the YOLO26-based branch.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-68.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1018" height="310" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-68.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55139"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-68.png?size=126x38&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-68-300x91.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-68.png?size=378x115&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-68.png?size=504x153&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-68.png?size=630x192&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-68-768x234.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-68.png?lossy=2&strip=1&webp=1 1018w" sizes="(max-width: 1018px) 100vw, 1018px" /></a><figcaption class="wp-element-caption"><strong>Table 1.</strong> Benchmark snapshot comparing closed-set YOLO models with YOLOE and YOLOE-26 on COCO, LVIS, and T4 inference speed.</figcaption></figure></div>


<p>The important pattern is not just that YOLOE-26 scores higher on LVIS. It does so while preserving the same reported T4 latency as YOLO11-L and YOLOE-L, which is exactly why the open-vocabulary story is practical rather than just academic.</p>



<p>The docs also make an important practical claim: in the regular closed-set case, the open-world additions in YOLOE can be re-parameterized back into a standard YOLO-style path, so you do not pay extra inference cost just for carrying the capability.</p>



<p>It is also useful to unpack the benchmark language briefly so readers do not treat the numbers like magic:</p>



<ul class="wp-block-list">
<li><strong>LVIS</strong> is a long-tail detection benchmark, which makes it a more meaningful place to discuss open-vocabulary behavior than a short everyday-class benchmark</li>



<li><strong>AP</strong> is average precision, so higher is better</li>



<li><strong>T4 latency</strong> gives you a rough sense of runtime cost on a standard GPU reference point</li>
</ul>



<p>The benchmark story here is narrower and more useful than hype. YOLOE-26 improves the open-vocabulary side of the problem while staying in the performance neighborhood practitioners expect from modern YOLO models.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Hands-On-Text-Prompting"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Hands-On-Text-Prompting">Hands-On with Text Prompting</a></h2>



<p>Now let us move from concept to code.</p>



<p>The most beginner-friendly way to meet YOLOE-26 is through <strong>text prompting</strong>. You load a YOLOE model, call <code data-enlighter-language="python" class="EnlighterJSRAW">set_classes()</code> once, and then run <code data-enlighter-language="python" class="EnlighterJSRAW">predict()</code> just like you would with a normal Ultralytics model.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="5">from ultralytics import YOLOE
from ultralytics.utils import ASSETS

text_model = YOLOE("yoloe-26s-seg.pt")
text_model.set_classes(["person", "bus"])
text_results = text_model.predict(ASSETS / "bus.jpg", conf=0.25)
text_results[0].show()
</pre>



<p>The following 2 points are worth noticing. The application programming interface (API) is almost boringly simple, and the class list is no longer hardwired into the checkpoint in the same way a standard closed-set detector is. You are telling the model what to care about at inference time.</p>



<p>In this first text-prompt example, we are using <code data-enlighter-language="python" class="EnlighterJSRAW">person</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">bus</code> because they are stable and easy to reproduce in the official sample image. Once you understand the workflow, you can swap in more interesting prompts that match your own data.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-69.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="650" height="638" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-69.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55144"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-69.png?size=126x124&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-69-300x294.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-69.png?size=378x371&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-69.png?size=504x495&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-69.png?lossy=2&strip=1&webp=1 650w" sizes="(max-width: 650px) 100vw, 650px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> YOLOE-26 text-prompted inference on the sample bus image.</figcaption></figure></div>


<p>That basic example is intentionally conservative. It is not trying to prove that YOLOE-26 magically does something a closed-set COCO model never could. It is showing you the mechanics in the simplest reproducible way.</p>



<p>Once that is clear, the real value comes from changing the prompt list.</p>



<h3 class="wp-block-heading">How to Think About Prompt Design</h3>



<p>This is where readers often make a subtle mistake. They treat prompting like keyword search. Open-vocabulary detection is not just string matching, so prompt quality matters.</p>



<p>A few practical rules help:</p>



<ul class="wp-block-list">
<li>start with short, concrete nouns</li>



<li>avoid vague multi-object phrases</li>



<li>avoid over-describing the object unless needed</li>



<li>try synonyms if the first prompt underperforms</li>



<li>if possible, test singular and plural forms only after trying the simplest base term</li>
</ul>



<p>For example, <code data-enlighter-language="python" class="EnlighterJSRAW">soldering iron</code> is likely a better prompt than <code data-enlighter-language="python" class="EnlighterJSRAW">small metal electronics repair tool with handle</code>. The latter contains more words, but that does not automatically make it better.</p>



<p>You should also expect some prompts to fail for perfectly normal reasons:</p>



<ul class="wp-block-list">
<li>the object may be tiny</li>



<li>the prompt may be semantically broad</li>



<li>the object may be heavily occluded</li>



<li>the prompt may describe a category the model has weak visual grounding for</li>
</ul>



<p>This is simply the cost of asking a flexible model to generalize beyond a fixed short class list.</p>



<h3 class="wp-block-heading">Good Prompts vs. Weak Prompts</h3>



<p>A few concrete examples make this easier to internalize:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">soldering iron</code>: stronger than <code data-enlighter-language="python" class="EnlighterJSRAW">tool</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">traffic light</code>: stronger than <code data-enlighter-language="python" class="EnlighterJSRAW">street object</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">barcode scanner</code>: stronger than <code data-enlighter-language="python" class="EnlighterJSRAW">electronics device</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">bus</code>: stronger than <code data-enlighter-language="python" class="EnlighterJSRAW">large road vehicle with windows</code></li>
</ul>



<p>The pattern is consistent. Good prompts are usually short, concrete, and visually grounded. Weak prompts are often too broad, too abstract, or too wordy. If the first prompt underperforms, try a nearby synonym before you conclude the model cannot handle the category at all.</p>



<h3 class="wp-block-heading">Reading the Result, Not Just Looking At It</h3>



<p>Annotated images are useful, but the result object tells you more than the picture does. Printing the top class names and confidence values makes it easier to inspect the output programmatically.</p>



<p>Open-vocabulary work often involves a short loop:</p>



<ul class="wp-block-list">
<li>try a prompt</li>



<li>inspect detections</li>



<li>revise the prompt</li>



<li>re-run inference</li>
</ul>



<p>This feedback loop is why an interactive workflow works well for Lesson 1. You are not yet building a packaged application. You are exploring the detector’s behavior iteratively.</p>



<h3 class="wp-block-heading">Inspecting the Result Object</h3>



<p>If you want to move one step beyond screenshots, inspect the prediction object directly:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="6">result = text_results[0]

print(result.names)
print(result.boxes.xyxy[:3])
print(result.boxes.conf[:3])
print(result.boxes.cls[:3])

if result.masks is not None:
    print(result.masks.data.shape)
</pre>



<p>That quick inspection tells you almost everything you need for downstream work:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">result.names</code>: maps class identifiers (IDs) to label names</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">result.boxes.xyxy</code>: stores box coordinates</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">result.boxes.conf</code>: stores confidence scores</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">result.boxes.cls</code>: stores predicted class IDs</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">result.masks</code>: appears because we are using segmentation-first checkpoints, so mask tensors are available alongside the boxes</li>
</ul>



<p>This is the easiest bridge from “nice demo” to “usable building block.” Once readers understand where the predictions live, they can start saving detections, filtering them, or passing them into a larger pipeline.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-Visual-Prompting-Is-Real-Superpower"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-Visual-Prompting-Is-Real-Superpower">Why Visual Prompting Is the Real Superpower</a></h2>



<p>Text prompts are the easiest entry point, but visual prompting is where YOLOE-26 starts to feel genuinely different from a normal detector.</p>



<p>Sometimes words are not enough. Maybe the object is a very specific industrial part. Maybe the text label is ambiguous. Maybe the thing you want to find is easier to show than describe.</p>



<p>That is what visual prompting is for.</p>



<h3 class="wp-block-heading">How Visual Prompts Are Structured</h3>



<p>With visual prompts, you give the model one or more bounding boxes around reference objects. YOLOE-26 then uses those examples to find visually similar instances.</p>



<p>The next example uses the exact same <code data-enlighter-language="python" class="EnlighterJSRAW">bus.jpg</code> sample image from the docs. To keep the first visual-prompt workflow stable and easy to reproduce, we place 1 prompt box around a person.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="7">import numpy as np

from ultralytics import YOLOE
from ultralytics.models.yolo.yoloe import YOLOEVPSegPredictor
from ultralytics.utils import ASSETS

visual_model = YOLOE("yoloe-26s-seg.pt")
visual_model.set_classes(["person"])
visual_prompts = dict(
   bboxes=np.array(
       [
           [221.52, 405.8, 344.98, 857.54],
       ]
   ),
   cls=np.array([0]),
)

visual_result = visual_model.predict(
   ASSETS / "bus.jpg",
   visual_prompts=visual_prompts,
   predictor=YOLOEVPSegPredictor,
   conf=0.25,
)[0]
visual_result.names = {0: "person"}
visual_result.show()
</pre>



<p>The only slightly unusual part here is the <code data-enlighter-language="python" class="EnlighterJSRAW">visual_prompts</code> dictionary:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">bboxes</code>: contains the reference box</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">cls</code>: contains a sequential ID that associates that box with the prompt category</li>
</ul>



<p>These are not global COCO class IDs. They are temporary identifiers for the prompt session.</p>



<p>For visualization, we also relabel that temporary prompt ID so the plotted output says <code data-enlighter-language="python" class="EnlighterJSRAW">person</code> instead of <code data-enlighter-language="python" class="EnlighterJSRAW">object0</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-4.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="768" height="1024" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-4-768x1024.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-55149"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-4-768x1024.jpeg?lossy=2&strip=1&webp=1 768w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-4-768x1024.jpeg?size=126x168&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-4-768x1024.jpeg?size=252x336&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-4-768x1024.jpeg?size=378x504&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-4-768x1024.jpeg?size=504x672&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-4-768x1024.jpeg?size=630x840&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 768px) 100vw, 768px" /></a><figcaption class="wp-element-caption"><strong>Figure 8: </strong>The same sample image annotated with a person reference box before YOLOE-26 inference.</figcaption></figure></div>


<p>This is the mode many tutorials skip, but it is extremely practical. If you can show the model what an object looks like once, you can turn that into a lightweight one-shot retrieval workflow.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-5.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="768" height="1024" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-5-768x1024.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-55152"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-5-768x1024.jpeg?lossy=2&strip=1&webp=1 768w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-5-768x1024.jpeg?size=126x168&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-5-768x1024.jpeg?size=252x336&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-5-768x1024.jpeg?size=378x504&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-5-768x1024.jpeg?size=504x672&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-5-768x1024.jpeg?size=630x840&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 768px) 100vw, 768px" /></a><figcaption class="wp-element-caption"><strong>Figure 9:</strong> YOLOE-26 visual-prompted inference using the person reference box to retrieve visually similar instances.</figcaption></figure></div>


<h3 class="wp-block-heading">When Visual Prompting Beats Text Prompting</h3>



<p>Visual prompting is most useful when the object is easier to show than to describe. Suppose you are looking for a very specific wrench, connector, or control knob. A text label like <code data-enlighter-language="python" class="EnlighterJSRAW">metal connector</code> may be too broad, but a carefully drawn reference box can tell the model, “Find more objects that look like this.” In practice, make the prompt box tight, representative, and light on background clutter. Very small prompt regions can be brittle, so start with larger, visually distinctive examples when you are learning the workflow.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Optional-Extension-Prompting-Separate-Reference-Image"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Optional-Extension-Prompting-Separate-Reference-Image">Optional Extension: Prompting from a Separate Reference Image</a></h2>



<p>One optional demo is also worth mentioning: the prompt does not have to come from the same image as the target. The <code data-enlighter-language="python" class="EnlighterJSRAW">refer_image</code> argument handles that case. This is closer to a real retrieval workflow, where you have one known example and want to search a different frame set or batch for similar objects.</p>



<p>If you want to include this extension, here is a minimal working example:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="9">import numpy as np

from ultralytics import YOLOE
from ultralytics.models.yolo.yoloe import YOLOEVPSegPredictor
from ultralytics.utils import ASSETS

reference_model = YOLOE("yoloe-26s-seg.pt")
reference_model.set_classes(["person"])

reference_prompts = dict(
   bboxes=np.array([[221.52, 405.8, 344.98, 857.54]]),
   cls=np.array([0]),
)

reference_result = reference_model.predict(
   ASSETS / "zidane.jpg",
   refer_image=ASSETS / "bus.jpg",
   visual_prompts=reference_prompts,
   predictor=YOLOEVPSegPredictor,
   conf=0.10,
)[0]

reference_result.names = {0: "person"}
reference_result.show()
</pre>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-6-scaled.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="576" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-6-1024x576.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-55155"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-6-1024x576.jpeg?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-6-1024x576.jpeg?size=126x71&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-6-1024x576.jpeg?size=252x142&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-6-1024x576.jpeg?size=378x213&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-6-1024x576.jpeg?size=504x284&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-6-1024x576.jpeg?size=630x354&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 10:</strong> Optional cross-image visual prompting, in which YOLOE-26 uses a person reference from <code>bus.jpg</code> to search for similar instances in <code>zidane.jpg</code>.</figcaption></figure></div>


<p>Treat this as an advanced extension, not a required part of the first pass through the lesson.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-YOLOE-26-Prompt-Free-Open-Vocabulary-Object-Detection"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-YOLOE-26-Prompt-Free-Open-Vocabulary-Object-Detection">YOLOE-26 Prompt-Free Open-Vocabulary Object Detection</a></h2>



<p>YOLOE-26 also has <strong>prompt-free variants</strong>. These models come with a built-in open vocabulary and do not require your own text prompts or visual prompts at inference time.</p>



<h3 class="wp-block-heading">How Prompt-Free Inference Works in Practice</h3>



<p>Here is the corresponding code:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="11">from ultralytics import YOLOE
from ultralytics.utils import ASSETS

prompt_free_model = YOLOE("yoloe-26s-seg-pf.pt")
prompt_free_results = prompt_free_model.predict(ASSETS / "bus.jpg", conf=0.25)
prompt_free_results[0].show()
</pre>



<p>This mode is useful when you want a larger built-in vocabulary with minimal setup. According to the Ultralytics documentation, the prompt-free models use a built-in large vocabulary and internal embeddings for open-set recognition.</p>



<p>But there is a tradeoff, and it is important enough to say clearly: prompt-free convenience costs accuracy.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-7.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="768" height="1024" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7-768x1024.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-55158"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7-768x1024.jpeg?lossy=2&strip=1&webp=1 768w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7-768x1024.jpeg?size=126x168&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7-768x1024.jpeg?size=252x336&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7-768x1024.jpeg?size=378x504&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7-768x1024.jpeg?size=504x672&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7-768x1024.jpeg?size=630x840&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 768px) 100vw, 768px" /></a><figcaption class="wp-element-caption"><strong>Figure 11:</strong> Prompt-free YOLOE-26 inference with the built-in vocabulary.</figcaption></figure></div>


<p>Prompt-free mode is appealing because it feels effortless. Load the model, run inference, and get a wide-vocabulary result without deciding on prompts first. In practice, it is best for exploratory analysis and quick discovery passes, not for the cases where you need the strongest possible precision on one narrow concept. If text prompting is you saying “look for this,” prompt-free mode is closer to saying “show me what the built-in vocabulary thinks is here.”</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Where-YOLOE-26-Beats-YOLO26-Where-It-Does-Not"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Where-YOLOE-26-Beats-YOLO26-Where-It-Does-Not">Where YOLOE-26 Beats YOLO26, and Where It Does Not</a></h2>



<p>YOLOE-26 beats standard YOLO26 whenever the target category is dynamic, long-tail, or simply outside the fixed closed-set label space. If your environment changes often, or if you need to search for unusual objects without retraining a detector from scratch, YOLOE-26 is the more flexible tool.</p>



<p>It also has a strong headline benchmark story. Ultralytics reports that the <code data-enlighter-language="python" class="EnlighterJSRAW">x</code> model reaches:</p>



<ul class="wp-block-list">
<li><strong>40.6 AP</strong><strong>:</strong> on LVIS minival with text prompts</li>



<li><strong>38.5 AP</strong><strong>:</strong> with visual prompts</li>



<li><strong>31.1 AP</strong><strong>:</strong> in the prompt-free non-E2E setting</li>
</ul>



<p>That is a meaningful spread. Prompt-free mode is convenient, but you pay for that convenience in accuracy.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-70.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1025" height="470" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-70.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55162"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-70.png?size=126x58&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-70-300x138.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-70.png?size=378x173&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-70.png?size=504x231&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-70.png?size=630x289&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-70-768x352.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-70.png?lossy=2&strip=1&webp=1 1025w" sizes="(max-width: 1025px) 100vw, 1025px" /></a><figcaption class="wp-element-caption"><strong>Table 2.</strong> YOLOE-26 prompt mode tradeoff on LVIS minival, showing the accuracy cost of moving from explicit prompting to prompt-free inference.</figcaption></figure></div>


<p>The pattern is straightforward: the more explicit guidance you give YOLOE-26, the better it performs. Prompt-free mode is still useful, but it should be treated as a convenience mode, not the default path when accuracy matters most.</p>



<h3 class="wp-block-heading">YOLO26 and YOLOE-26 at a Glance</h3>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-71.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="550" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-71-1024x550.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55163"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-71-1024x550.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-71-1024x550.png?size=126x68&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-71-1024x550.png?size=252x135&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-71-1024x550.png?size=378x203&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-71-1024x550.png?size=504x271&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-71-1024x550.png?size=630x338&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 3.</strong> YOLO26 and YOLOE-26 at a glance.</figcaption></figure></div>


<h3 class="wp-block-heading">A Practical Decision Framework</h3>



<p>At the same time, YOLOE-26 is not automatically “better” than YOLO26 for every production workload. If your class list is fixed, your latency budget is tight, and you already know exactly what you need to detect, a standard fine-tuned YOLO26 model is often the simpler production artifact.</p>



<p>Here is a clean decision framework:</p>



<ul class="wp-block-list">
<li><strong>YOLO26:</strong> choose when the classes are fixed and you care most about a lean production artifact</li>



<li><strong>YOLOE-26 text prompting:</strong> choose when you know the concept but want the flexibility to change classes without retraining</li>



<li><strong>YOLOE-26 visual prompting:</strong> choose when the object is easier to show than to describe</li>



<li><strong>YOLOE-26 prompt-free:</strong> choose when you want broad exploratory coverage and are willing to trade accuracy for convenience</li>
</ul>



<p>This decision framework is exactly why Lesson 1 comes before Lesson 2. You need to understand where YOLOE-26 shines before you can use it intelligently as a labeling engine later.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Common-Failure-Modes-How-Debug-Them"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Common-Failure-Modes-How-Debug-Them">Common Failure Modes and How to Debug Them</a></h2>



<p>Open-vocabulary detection is powerful, but it is not magic. Here are the most common reasons your first run may disappoint you:</p>



<h3 class="wp-block-heading">The Prompt Is Too Broad</h3>



<p>A prompt (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">tool</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">electronics device</code>) may be semantically valid but visually broad. The model has too many ways to satisfy the prompt, so detections may become noisy.</p>



<p><strong>Fix:</strong> start narrower. Use the most concrete noun you can.</p>



<h3 class="wp-block-heading">The Prompt Is Too Obscure</h3>



<p>Sometimes the opposite happens. You use a very domain-specific term that the model has weak grounding for.</p>



<p><strong>Fix: </strong>try a simpler synonym or a more common parent concept first.</p>



<h3 class="wp-block-heading">The Object Is Too Small</h3>



<p>Tiny objects are hard for almost every detector. Open-vocabulary capability does not remove that challenge.</p>



<p><strong>Fix:</strong> use a larger input size, crop the image, or test on closer examples before judging the prompt.</p>



<h3 class="wp-block-heading">The Visual Prompt Box Is Poor</h3>



<p>If your reference box includes too much background or cuts off the object, you are teaching the model the wrong visual concept.</p>



<p><strong>Fix: </strong>redraw the prompt box tightly and try again.</p>



<h3 class="wp-block-heading">You Are Expecting Prompt-Free Mode to Behave Like Curated Prompting</h3>



<p>Prompt-free is convenient, not optimal. If the results are underwhelming, that does not mean YOLOE-26 as a whole is weak. It may simply mean you should move to text or visual prompting.</p>



<p><strong>Fix: </strong>switch to a more controlled prompt mode before drawing conclusions.</p>



<h3 class="wp-block-heading">Your Prompts Overlap Semantically</h3>



<p>Some prompt sets are too close to each other. If you ask for overlapping concepts (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">person</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">pedestrian</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">worker</code>, or <code data-enlighter-language="python" class="EnlighterJSRAW">tool</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">hand tool</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">screwdriver</code>), the detector may produce messy or merged behavior because multiple prompts can plausibly match the same region.</p>



<p><strong>Fix:</strong> start with clearly separated categories. Add finer-grained prompts only after the coarse categories are behaving the way you expect.</p>



<p>The right question is not “Did one random prompt work perfectly on the first try?” It is “How much controllability do we get, and how quickly can we improve the result through prompting?”</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-One-Deployment-Detail-You-Should-Not-Miss"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-One-Deployment-Detail-You-Should-Not-Miss">One Deployment Detail You Should Not Miss</a></h2>



<p>One operational detail from the Ultralytics docs deserves explicit attention. When you export a YOLOE model, the configured classes are baked into the exported weights. After that, you cannot keep swapping prompt classes on the exported artifact. To change them, you need to re-export from the original checkpoint.</p>



<p>During experimentation, this is not a big deal. You just change the prompts and rerun the code. But if you are preparing a deployable artifact, the prompt configuration becomes part of what you are exporting.</p>



<p>This is one of the clearest ways to understand the line between experimentation and production:</p>



<ul class="wp-block-list">
<li><strong>exploration mode:</strong> YOLOE-26 is highly flexible</li>



<li><strong>deployment mode:</strong> some of that flexibility gets frozen into the exported artifact</li>
</ul>



<p>That detail will matter a lot in Lesson 2, because Lesson 2 is about what happens when you stop exploring and start converging on a narrower detector workflow.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>The easiest way to think about this lesson is:</p>



<ul class="wp-block-list">
<li><strong>YOLO26</strong> is your fast closed-set baseline.</li>



<li><strong>YOLOE</strong> introduced promptable open-vocabulary detection to the YOLO family.</li>



<li><strong>YOLOE-26</strong> brings that same idea into the YOLO26 generation with stronger open-vocabulary performance and the same real-time mindset.</li>
</ul>



<p>By now, you have seen 3 concrete workflows:</p>



<ul class="wp-block-list">
<li><strong>Text prompting:</strong> inference with <code data-enlighter-language="python" class="EnlighterJSRAW">set_classes()</code></li>



<li><strong>Visual prompting:</strong> inference with reference boxes</li>



<li><strong>Prompt-free inference:</strong> inference with a built-in vocabulary</li>
</ul>



<p>You have also seen the deeper point behind those workflows. YOLOE-26 changes how quickly we can move from “we have an object in mind” to “we have a detector doing something useful.”</p>



<p>That is already enough to start experimenting. But it also raises the next practical question.</p>



<p>What if you do not want a permanently flexible open-vocabulary detector? What if you want to use YOLOE-26 to discover and pre-label a niche category, then turn that into a small, fast, deployable closed-set detector?</p>



<p>That is exactly what we will do in Lesson 2.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V</strong><strong>. </strong>“YOLO26 Open-Vocabulary Object Detection with YOLOE-26,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/mzpx3" target="_blank" rel="noreferrer noopener">https://pyimg.co/mzpx3</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="YOLO26 Open-Vocabulary Object Detection with YOLOE-26" data-enlighter-group="13">@incollection{Singh_2026_yolo26-open-vocabulary-object-detection-yoloe-26,
  author = {Vikram Singh},
  title = {{YOLO26 Open-Vocabulary Object Detection with YOLOE-26}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/mzpx3},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/24/yolo26-open-vocabulary-object-detection-with-yoloe-26/">YOLO26 Open-Vocabulary Object Detection with YOLOE-26</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API</title>
		<link>https://pyimagesearch.com/2026/08/17/make-a-chrome-extension-to-digest-webpages-with-manifest-v3-and-groq-api/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 17 Aug 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Chrome Extensions]]></category>
		<category><![CDATA[Large Language Models]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[ai chrome extension]]></category>
		<category><![CDATA[chrome api]]></category>
		<category><![CDATA[chrome extension]]></category>
		<category><![CDATA[groq]]></category>
		<category><![CDATA[groq api]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[large language models]]></category>
		<category><![CDATA[llm]]></category>
		<category><![CDATA[manifest v3]]></category>
		<category><![CDATA[streaming llm]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[webpage summarization]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=54989</guid>

					<description><![CDATA[<p>Table of Contents Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API Meet the Project Configuring Your Development Environment Project Structure The Chrome Extension Mental Model Walking Through manifest.json Understanding popup.html Understanding styles.css Reading README.md the&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/17/make-a-chrome-extension-to-digest-webpages-with-manifest-v3-and-groq-api/">Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Make-Chrome-Extension-Digest-Webpages-Manifest-V3-Groq-API"><a rel="noopener" target="_blank" href="#h1-Make-Chrome-Extension-Digest-Webpages-Manifest-V3-Groq-API">Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API</a></li>

    <li id="TOC-h2-Meet-Project"><a rel="noopener" target="_blank" href="#h2-Meet-Project">Meet the Project</a></li>

    <li id="TOC-h2-Configuring-Development-Environment"><a rel="noopener" target="_blank" href="#h2-Configuring-Development-Environment">Configuring Your Development Environment</a></li>

    <li id="TOC-h2-Project-Structure"><a rel="noopener" target="_blank" href="#h2-Project-Structure">Project Structure</a></li>

    <li id="TOC-h2-Chrome-Extension-Mental-Model"><a rel="noopener" target="_blank" href="#h2-Chrome-Extension-Mental-Model">The Chrome Extension Mental Model</a></li>

    <li id="TOC-h2-Walking-Through-manifest-json"><a rel="noopener" target="_blank" href="#h2-Walking-Through-manifest-json">Walking Through manifest.json</a></li>

    <li id="TOC-h2-Understanding-popup-html"><a rel="noopener" target="_blank" href="#h2-Understanding-popup-html">Understanding popup.html</a></li>

    <li id="TOC-h2-Understanding-styles-css"><a rel="noopener" target="_blank" href="#h2-Understanding-styles-css">Understanding styles.css</a></li>

    <li id="TOC-h2-Reading-README-md-Right-Way"><a rel="noopener" target="_blank" href="#h2-Reading-README-md-Right-Way">Reading README.md the Right Way</a></li>

    <li id="TOC-h2-Walking-Through-popup-js"><a rel="noopener" target="_blank" href="#h2-Walking-Through-popup-js">Walking Through popup.js</a></li>

    <li id="TOC-h2-How-Extension-Reads-Current-Webpage"><a rel="noopener" target="_blank" href="#h2-How-Extension-Reads-Current-Webpage">How the Extension Reads the Current Webpage</a></li>

    <li id="TOC-h2-How-Groq-Streaming-Call-Works"><a rel="noopener" target="_blank" href="#h2-How-Groq-Streaming-Call-Works">How the Groq Streaming Call Works</a></li>

    <li id="TOC-h2-How-Conversation-State-Is-Managed"><a rel="noopener" target="_blank" href="#h2-How-Conversation-State-Is-Managed">How Conversation State Is Managed</a></li>

    <li id="TOC-h2-How-Summarize-Workflow-Works"><a rel="noopener" target="_blank" href="#h2-How-Summarize-Workflow-Works">How the Summarize Workflow Works</a></li>

    <li id="TOC-h2-How-Clear-Button-Resets-Interface"><a rel="noopener" target="_blank" href="#h2-How-Clear-Button-Resets-Interface">How the Clear Button Resets the Interface</a></li>

    <li id="TOC-h2-End-to-End-Flow-From-Click-Answer"><a rel="noopener" target="_blank" href="#h2-End-to-End-Flow-From-Click-Answer">End-to-End Flow, From Click to Answer</a></li>

    <li id="TOC-h2-Practical-Engineering-Takeaways"><a rel="noopener" target="_blank" href="#h2-Practical-Engineering-Takeaways">Practical Engineering Takeaways</a></li>

    <li id="TOC-h2-Where-You-Could-Take-Project-Next"><a rel="noopener" target="_blank" href="#h2-Where-You-Could-Take-Project-Next">Where You Could Take This Project Next</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Make-Chrome-Extension-Digest-Webpages-Manifest-V3-Groq-API"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Make-Chrome-Extension-Digest-Webpages-Manifest-V3-Groq-API">Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API</a></h2>



<p>In this lesson, you will learn how to build a <a href="https://chromewebstore.google.com/category/extensions" target="_blank" rel="noreferrer noopener">Chrome extension</a> that can read the current webpage, send that content to a large language model (LLM), and stream the answer back into a popup interface.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured.png?lossy=2&strip=1&webp=1" alt="make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured.png" class="wp-image-55072"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/make-chrome-extension-digest-webpages-manifest-v3-groq-api-featured.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p><strong>To learn how to build a Chrome extension that summarizes webpages with Manifest V3 and the Groq API, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<p>This is exactly the kind of project we want for a first lesson. The codebase is small enough to understand in one sitting, but practical enough to expose the moving parts that actually matter in a real browser-based artificial intelligence (AI) tool:</p>



<ul class="wp-block-list">
<li><a href="https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3" target="_blank" rel="noreferrer noopener">Manifest V3</a> configuration</li>



<li>Extension permissions</li>



<li>Popup user interface (UI) structure</li>



<li>Persistent storage with <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.storage.local</code></li>



<li>Reading live page content with <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.scripting.executeScript</code></li>



<li>Calling the <a href="https://groq.com" target="_blank" rel="noreferrer noopener">Groq</a> API with <code data-enlighter-language="python" class="EnlighterJSRAW">fetch</code></li>



<li>Streaming tokens back into the interface</li>



<li>Managing conversation history across turns</li>
</ul>



<p>By the end of this lesson, you will understand what each file does, how the pieces fit together, and why this design works so well for a first Chrome LLM extension.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Meet-Project"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Meet-Project">Meet the Project</a></h2>



<p>Our project is called <strong>Page Intelligence</strong>. When the user clicks the extension icon, Chrome opens a popup. From that popup, the user can:</p>



<ul class="wp-block-list">
<li>Save a Groq application programming interface (API) key</li>



<li>Summarize the current webpage</li>



<li>Ask follow-up questions about that page</li>



<li>Clear the current conversation and start over</li>
</ul>



<p>At first glance, that workflow feels straightforward. Under the hood, though, it touches 3 separate environments:</p>



<ul class="wp-block-list">
<li>The popup UI, rendered by Chrome as an extension page</li>



<li>The active tab, which contains the webpage we want to read</li>



<li>The Groq API, which generates the response</li>
</ul>



<p>That separation is the key architectural idea in this lesson. The popup cannot directly read the page Document Object Model (DOM) just because both are open in the same browser window. Instead, it has to ask Chrome for permission and inject code into the active tab at the right moment.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-43.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="558" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-43-1024x558.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55024"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-43-1024x558.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-43-1024x558.png?size=126x69&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-43-1024x558.png?size=252x137&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-43-1024x558.png?size=378x206&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-43-1024x558.png?size=504x275&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-43-1024x558.png?size=630x343&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> High-level architecture of the Page Intelligence Chrome extension (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuring-Development-Environment"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuring-Development-Environment">Configuring Your Development Environment</a></h2>



<p>To follow this lesson, you only need Google Chrome, a Groq API key, and the project files for the extension. Since this is a lightweight Chrome extension project, there are no Python packages or JavaScript dependencies to install.</p>



<p>Then follow these steps:</p>



<ul class="wp-block-list">
<li>Download or open the project folder.</li>



<li>Get a Groq API key.</li>



<li>Open <code data-enlighter-language="python" class="EnlighterJSRAW">chrome://extensions</code>.</li>



<li>Enable Developer mode.</li>



<li>Click Load unpacked and select the project folder.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Project-Structure"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Project-Structure">Project Structure</a></h2>



<p>We first need to review our project directory structure.</p>



<p>Start by accessing this tutorial’s <em><strong>“Downloads”</strong></em> section to retrieve the source code.</p>



<p>From there, take a look at the directory structure:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="1">chrome-llm-extension/
├── manifest.json
├── popup.html
├── popup.js
├── styles.css
└── README.md</pre>



<p>The minimal layout is intentional. It keeps the lesson focused. There is no framework, no extra build system, and no separate content script file to chase through. All of the user-facing logic lives in the popup, and the extension reaches into the current page only when it needs to read content.</p>



<h3 class="wp-block-heading">What Each File Is Responsible For</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">manifest.json</code></p>



<ul class="wp-block-list">
<li>Declares that this is a Manifest V3 extension</li>



<li>Requests the permissions needed to read the active tab, inject a script, and store the API key</li>



<li>Registers <code data-enlighter-language="python" class="EnlighterJSRAW">popup.html</code> as the default popup</li>
</ul>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">popup.html</code></p>



<ul class="wp-block-list">
<li>Defines the structure of the popup interface</li>



<li>Creates the API key panel, chat area, quick action buttons, and message box</li>
</ul>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">styles.css</code></p>



<ul class="wp-block-list">
<li>Controls layout, spacing, colors, bubble styles, and the overall feel of the popup</li>
</ul>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">popup.js</code></p>



<ul class="wp-block-list">
<li>Contains the extension&#8217;s runtime logic</li>



<li>Loads and stores the API key</li>



<li>Reads page content from the active tab</li>



<li>Calls the Groq API</li>



<li>Streams the response token by token</li>



<li>Maintains <code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code></li>
</ul>



<h3 class="wp-block-heading">Why This Structure Works Well for a Lesson</h3>



<p>For a first Chrome extension project, fewer files usually mean faster understanding. Instead of bouncing between a popup, a background worker, and multiple content scripts, you get to follow one direct path:</p>



<ul class="wp-block-list">
<li>Chrome opens the popup.</li>



<li>The popup loads saved state.</li>



<li>The popup requests page text when needed.</li>



<li>The popup sends a streaming LLM request.</li>



<li>The popup updates the UI in real time.</li>
</ul>



<p>For a teaching project, that is a great tradeoff. We stay practical without burying the learner in scaffolding.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-44-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="332" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-44-1024x332.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55026"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-44-1024x332.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-44-1024x332.png?size=126x41&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-44-1024x332.png?size=252x82&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-44-1024x332.png?size=378x123&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-44-1024x332.png?size=504x163&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-44-1024x332.png?size=630x204&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> The Brave extensions page with Page Intelligence loaded as an unpacked extension (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Chrome-Extension-Mental-Model"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Chrome-Extension-Mental-Model">The Chrome Extension Mental Model</a></h2>



<p>Before we go any further, let us make the execution model crystal clear.</p>



<h3 class="wp-block-heading">A popup is not the webpage</h3>



<p>When you click a Chrome extension icon, Chrome opens a small Hypertext Markup Language (HTML) page that belongs to the extension. That page has its own DOM, JavaScript context, and permissions.</p>



<p>This means:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">popup.js</code> can manipulate elements in <code data-enlighter-language="python" class="EnlighterJSRAW">popup.html</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">popup.js</code> cannot directly access <code data-enlighter-language="python" class="EnlighterJSRAW">document.body</code> from the currently open website</li>



<li>To read the website, the extension must ask Chrome to run code inside the active tab</li>
</ul>



<p>This is exactly why <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.scripting.executeScript</code> matters in this project.</p>



<h3 class="wp-block-heading">Storage Belongs to the Extension, Not the Site</h3>



<p>There is a second separation we need to keep in mind, and that is storage.</p>



<p>If you used <code data-enlighter-language="python" class="EnlighterJSRAW">localStorage</code> inside a normal webpage, that data would belong to that page&#8217;s origin. Here, we want the API key to belong to the extension itself, regardless of which site the user is visiting.</p>



<p>For that reason, the code uses:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="2">chrome.storage.local</pre>



<p>This storage area is managed by Chrome and shared across extension contexts.</p>



<h3 class="wp-block-heading">Network Requests Must Be Explicitly Allowed</h3>



<p>Extensions are permission-driven. If you want to send requests to the Groq API, you declare that in <code data-enlighter-language="python" class="EnlighterJSRAW">manifest.json</code> using <code data-enlighter-language="python" class="EnlighterJSRAW">host_permissions</code>.</p>



<p>This is one of the core Chrome extension design principles:</p>



<ul class="wp-block-list">
<li>UI and code live inside the extension</li>



<li>Privileges are declared in the manifest</li>



<li>Cross-context access is granted only through the proper APIs</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Walking-Through-manifest-json"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Walking-Through-manifest-json">Walking Through manifest.json</a></h2>



<p>Open <code data-enlighter-language="python" class="EnlighterJSRAW">manifest.json</code>, and you will find the contract between your extension and Chrome.</p>



<p>Here is the full file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="3">{
  "manifest_version": 3,
  "name": "Page Intelligence",
  "version": "1.0",
  "description": "Chat with any webpage using an LLM. Built for the Agent AI course.",
  "permissions": ["activeTab", "scripting", "storage"],
  "host_permissions": ["https://api.groq.com/*"],
  "action": {
    "default_popup": "popup.html",
    "default_title": "Page Intelligence"
  }
}</pre>



<h3 class="wp-block-heading">manifest_version: 3</h3>



<p>Chrome expects modern extensions to use Manifest V3. That affects how extensions are structured, how scripts are injected, and how background logic is handled.</p>



<p>In this project, Manifest V3 gives us a clean, current, production-relevant starting point.</p>



<h3 class="wp-block-heading">Why These Permissions Matter</h3>



<h4 class="wp-block-heading">activeTab</h4>



<p>This permission allows the extension to interact with the tab the user is currently using. In a tool like this, that is essential because the extension must read the active page when the user clicks <strong>Summarize Page</strong>.</p>



<h4 class="wp-block-heading">scripting</h4>



<p>This permission unlocks <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.scripting.executeScript</code>, which is the modern Manifest V3 (MV3) way to inject code into a page.</p>



<h4 class="wp-block-heading">storage</h4>



<p>This permission lets the extension persist the Groq API key using <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.storage.local</code>.</p>



<h3 class="wp-block-heading">Why host_permissions Matters</h3>



<p>The extension calls <code data-enlighter-language="python" class="EnlighterJSRAW">https://api.groq.com/openai/v1/chat/completions</code>.</p>



<p>Without the proper host permission, that request would be blocked. This line gives the extension permission to communicate with the Groq API host.</p>



<h3 class="wp-block-heading">Why action.default_popup Matters</h3>



<p>Here we define the entry point for the user experience. This tells Chrome, &#8220;when the extension icon is clicked, open <code data-enlighter-language="python" class="EnlighterJSRAW">popup.html</code>.&#8221;</p>



<p>One line is all it takes to wire the entire interface into the browser.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Understanding-popup-html"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Understanding-popup-html">Understanding popup.html</a></h2>



<p>Next, open <code data-enlighter-language="python" class="EnlighterJSRAW">popup.html</code>. If <code data-enlighter-language="python" class="EnlighterJSRAW">manifest.json</code> is the contract, <code data-enlighter-language="python" class="EnlighterJSRAW">popup.html</code> is the stage where the user interacts with the extension.</p>



<h3 class="wp-block-heading">The Popup Is Intentionally Split into Clear UI Regions</h3>



<p>The HTML defines 5 major regions:</p>



<ul class="wp-block-list">
<li>Header</li>



<li>API key section</li>



<li>Chat history</li>



<li>Quick actions</li>



<li>Input area</li>
</ul>



<p>That separation matters because each region maps directly to behavior in <code data-enlighter-language="python" class="EnlighterJSRAW">popup.js</code>.</p>



<p>Here is the structural core of the file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="4">&lt;div class="container">
  &lt;div class="header">...&lt;/div>
  &lt;div id="api-key-section" class="api-key-section">...&lt;/div>
  &lt;div id="chat-history" class="chat-history">...&lt;/div>
  &lt;div class="quick-actions">...&lt;/div>
  &lt;div class="input-area">...&lt;/div>
&lt;/div></pre>



<h3 class="wp-block-heading">The Header Sets Up Identity and Settings Access</h3>



<p>At the top, we have:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="5">&lt;h1>Page Intelligence&lt;/h1>
&lt;button id="settings-btn" class="icon-btn">...&lt;/button></pre>



<p>The lesson here is simple but important. Even a tiny extension UI needs clear affordances. The settings button gives the user a predictable place to reopen the API key panel after the initial save.</p>



<h3 class="wp-block-heading">The API Key Section Introduces Stateful UI</h3>



<p>The API key input exists in the DOM from the beginning, but JavaScript decides whether it should be visible based on whether a key has already been saved.</p>



<p>A common frontend pattern is at work here:</p>



<ul class="wp-block-list">
<li>Keep the structure in HTML</li>



<li>Let JavaScript decide the current state</li>
</ul>



<h3 class="wp-block-heading">The Chat Area Is the Main Feedback Surface</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">#chat-history</code> container starts with a welcome message:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="6">&lt;div class="welcome-msg">
  Ask me anything about this page,&lt;br />
  or click &lt;strong>Summarize&lt;/strong> to get started.
&lt;/div></pre>



<p>This does more than decorate the popup. It solves the empty-state problem. Before the first interaction, the popup still feels guided and purposeful.</p>



<h3 class="wp-block-heading">Quick Actions Reduce Friction</h3>



<p>The 2 action buttons are:</p>



<ul class="wp-block-list">
<li><strong>Summarize Page</strong></li>



<li><strong>Clear</strong></li>
</ul>



<p>These are excellent teaching examples because they show 2 different UI intents:</p>



<ul class="wp-block-list">
<li><strong>Task button:</strong> launches a multi-step workflow</li>



<li><strong>Reset button:</strong> clears local state</li>
</ul>



<h3 class="wp-block-heading">The Input Area Supports Chat-Like Behavior</h3>



<p>The textarea plus send button give the user a second interaction path. They can either:</p>



<ul class="wp-block-list">
<li>Click the guided summary flow first</li>



<li>Skip directly to a custom question</li>
</ul>



<p>Practically, this supports both beginners and more confident users.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-45-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="827" height="1024" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-45-827x1024.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55034"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-45-827x1024.png?lossy=2&strip=1&webp=1 827w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-45-827x1024.png?size=126x156&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-45-827x1024.png?size=252x312&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-45-827x1024.png?size=378x468&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-45-827x1024.png?size=504x624&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-45-827x1024.png?size=630x780&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 827px) 100vw, 827px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> Initial popup UI showing the API key panel, welcome message, quick actions, and input area (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Understanding-styles-css"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Understanding-styles-css">Understanding styles.css</a></h2>



<p>Now open <code data-enlighter-language="python" class="EnlighterJSRAW">styles.css</code>. This is where the extension begins to feel polished instead of merely functional.</p>



<h3 class="wp-block-heading">The Layout Uses a Simple But Effective Flex Column</h3>



<p>The 2 most important layout rules are:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="css" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="7">body {
  width: 400px;
}

.container {
  display: flex;
  flex-direction: column;
  height: 560px;
}</pre>



<p>This gives the popup a fixed footprint and lets the chat area grow while the header, actions, and input stay anchored.</p>



<h3 class="wp-block-heading">Why the Chat Panel Works Well</h3>



<p>The chat container uses:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="css" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="8">.chat-history {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
}</pre>



<p>You will see this pattern often in messaging interfaces:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">flex: 1</code>: lets the panel absorb remaining height</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">overflow-y: auto</code>: makes long conversations scrollable</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">gap</code>: keeps messages visually separated without hard-to-maintain margins</li>
</ul>



<h3 class="wp-block-heading">Message Bubbles Encode Role Visually</h3>



<p>The Cascading Style Sheets (CSS) differentiate:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">.message.user</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.message.assistant</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.message.error</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">.message.system</code></li>
</ul>



<p>This is not just cosmetic. Good styling teaches the user how to read the conversation. With one glance, they can tell which text came from them, which came from the model, and whether a message is informational or an error.</p>



<h3 class="wp-block-heading">Small Animation Helps the Interface Feel Alive</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">fadeIn</code> keyframe is subtle:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="css" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="9">@keyframes fadeIn {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}</pre>



<p>It is also a good reminder that user experience (UX) is part of engineering. Because the model response streams in incrementally, the interface benefits from motion that feels lightweight and responsive.</p>



<h3 class="wp-block-heading">Input and Actions Are Styled for Fast Iteration</h3>



<p>The quick action buttons and textarea focus states give the extension a clean, modern feel without pulling in a UI framework.</p>



<p>This leads to another useful lesson from the project:</p>



<ul class="wp-block-list">
<li><code><code data-enlighter-language="python" class="EnlighterJSRAW">HTML</code></code>: handles structure</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">CSS</code>: handles presentation</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">JavaScript</code>: handles behavior</li>
</ul>



<p>For a beginner-friendly extension, that separation is exactly what we want.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Reading-README-md-Right-Way"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Reading-README-md-Right-Way">Reading README.md the Right Way</a></h2>



<p>It is easy to ignore <code data-enlighter-language="python" class="EnlighterJSRAW">README.md</code>, but in a teaching repository, that file matters.</p>



<h3 class="wp-block-heading">The README Is the Learner&#8217;s Runway</h3>



<p>This project&#8217;s README does 3 useful jobs:</p>



<ul class="wp-block-list">
<li><strong>Explains:</strong> what concepts the extension teaches</li>



<li><strong>Shows:</strong> setup steps for getting a Groq API key and loading the extension</li>



<li><strong>Presents:</strong> the codebase structure in a fast, approachable way</li>
</ul>



<p>So while <code data-enlighter-language="python" class="EnlighterJSRAW">README.md</code> is not runtime code, it is still part of the learning architecture of the project.</p>



<h3 class="wp-block-heading">Why This Matters in Real Projects</h3>



<p>When you teach or ship developer tooling, code alone is not enough. Learners need:</p>



<ul class="wp-block-list">
<li>Context</li>



<li>Setup steps</li>



<li>A map of what to read first</li>
</ul>



<p>This repository already does a good job of that by aligning the README sections with the files learners will inspect next.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Walking-Through-popup-js"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Walking-Through-popup-js">Walking Through popup.js</a></h2>



<p>With the surrounding files covered, we can move to the heart of the extension.</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">popup.js</code> is where the extension:</p>



<ul class="wp-block-list">
<li>Loads saved state</li>



<li>Handles button clicks</li>



<li>Reads the active page</li>



<li>Calls the Groq API</li>



<li>Streams the assistant output</li>



<li>Updates the chat UI</li>
</ul>



<p>This single file is small enough to follow in one sitting, which makes it ideal for a hands-on lesson.</p>



<h3 class="wp-block-heading">Configuration and State Live at the Top</h3>



<p>The file begins with configuration values:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="10">const GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";
const MODEL = "meta-llama/llama-4-scout-17b-16e-instruct";</pre>



<p>Then it defines a reusable system prompt and 2 key pieces of runtime state:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="11">let apiKey = "";
let chatHistory = [];</pre>



<p>This pattern is worth teaching because it keeps the mental model clean:</p>



<ul class="wp-block-list">
<li>Constants describe how the app talks to the model</li>



<li>Mutable state tracks what the user has done in this popup session</li>
</ul>



<h3 class="wp-block-heading">DOM References Create a Bridge from HTML to Behavior</h3>



<p>The next block grabs the key UI elements:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="12">const apiKeySection = document.getElementById("api-key-section");
const apiKeyInput   = document.getElementById("api-key-input");
const saveKeyBtn    = document.getElementById("save-key-btn");
const settingsBtn   = document.getElementById("settings-btn");
const chatHistoryEl = document.getElementById("chat-history");
const summarizeBtn  = document.getElementById("summarize-btn");
const clearBtn      = document.getElementById("clear-btn");
const userInput     = document.getElementById("user-input");
const sendBtn       = document.getElementById("send-btn");</pre>



<p>This is why the element identifiers (IDs) in <code data-enlighter-language="python" class="EnlighterJSRAW">popup.html</code> matter. They are the handles that let JavaScript attach behavior to markup.</p>



<h3 class="wp-block-heading">DOMContentLoaded Restores Saved Extension State</h3>



<p>When the popup opens, this listener runs:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="13">document.addEventListener("DOMContentLoaded", async () => {
  const stored = await chrome.storage.local.get("groq_api_key");

  if (stored.groq_api_key) {
    apiKey = stored.groq_api_key;
    apiKeySection.style.display = "none";
  }
});</pre>



<p>Here is one of the first places where theory and practice meet:</p>



<ul class="wp-block-list">
<li>The popup is ephemeral. It opens when the user clicks the extension icon.</li>



<li>Any in-memory JavaScript state disappears when the popup closes.</li>



<li>Persistent state must therefore live outside regular variables.</li>
</ul>



<p>This is exactly why <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.storage.local</code> is essential here. It gives us persistence without forcing us to introduce a database or a heavier architecture.</p>



<h3 class="wp-block-heading">Saving the API Key</h3>



<p>When the user clicks <strong>Save</strong>, the extension:</p>



<ul class="wp-block-list">
<li>Reads the input value</li>



<li>Validates that it is not empty</li>



<li>Stores it with <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.storage.local.set(...)</code></li>



<li>Copies it into the in-memory <code data-enlighter-language="python" class="EnlighterJSRAW">apiKey</code> variable</li>



<li>Hides the API key panel</li>



<li>Shows a confirmation message</li>
</ul>



<p>What you are seeing is a tidy example of state synchronization between:</p>



<ul class="wp-block-list">
<li>The DOM</li>



<li>Extension storage</li>



<li>In-memory JavaScript state</li>
</ul>



<h3 class="wp-block-heading">The Settings Button Reopens the Key Panel</h3>



<p>This small handler is worth pointing out:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="14">settingsBtn.addEventListener("click", () => {
  apiKeySection.style.display =
    apiKeySection.style.display === "none" ? "flex" : "none";
});</pre>



<p>Again, not every useful behavior needs a framework. For a focused popup, direct DOM manipulation is perfectly reasonable and easy to understand.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-Extension-Reads-Current-Webpage"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-Extension-Reads-Current-Webpage">How the Extension Reads the Current Webpage</a></h2>



<p>This is the most important architectural jump in the lesson, so take a moment to make sure the flow is clear before moving on.</p>



<h3 class="wp-block-heading">getPageText() Starts by Locating the Active Tab</h3>



<p>The function begins with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="15">const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });</pre>



<p>In plain English, this asks Chrome to return the tab the user is looking at right now.</p>



<h3 class="wp-block-heading">The Extension Then Injects a Function into That Tab</h3>



<p>Here is the core pattern:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="16">const results = await chrome.scripting.executeScript({
  target: { tabId: tab.id },
  func: () => {
    const clone = document.body.cloneNode(true);
    clone.querySelectorAll("script, style, nav, footer, aside")
      .forEach(el => el.remove());

    return {
      title: document.title,
      url: window.location.href,
      text: clone.innerText.replace(/\s+/g, " ").trim().slice(0, 8000),
    };
  },
});</pre>



<p>As a teaching example, this snippet is excellent because it shows the exact boundary between extension code and page code.</p>



<h4 class="wp-block-heading">Why Clone the Page Body First</h4>



<p>The code uses:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="17">const clone = document.body.cloneNode(true);</pre>



<p>Cloning first is smart because it lets the extension clean the content without altering the actual webpage the user is viewing.</p>



<h4 class="wp-block-heading">Why Remove Script, Style, Nav, Footer, and Aside</h4>



<p>This is a lightweight content-cleaning step. The extension wants the main readable text, not the surrounding noise.</p>



<p>This gives learners a practical introduction to preprocessing before sending data to an LLM.</p>



<h4 class="wp-block-heading">Why Normalize Whitespace and Cap Text Length</h4>



<p>This line matters:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="18">clone.innerText.replace(/\s+/g, " ").trim().slice(0, 8000)</pre>



<p>It does 3 useful things:</p>



<ul class="wp-block-list">
<li>Collapses repeated whitespace</li>



<li>Trims leading and trailing space</li>



<li>Limits the payload to 8,000 characters</li>
</ul>



<p>The final limit is especially important. Even though modern models have large context windows, good engineering still means keeping inputs focused and predictable.</p>



<h3 class="wp-block-heading">The Return Value Comes Back to the Popup</h3>



<p>After <code data-enlighter-language="python" class="EnlighterJSRAW">executeScript</code> finishes, the popup receives:</p>



<ul class="wp-block-list">
<li>The page title</li>



<li>The page uniform resource locator (URL)</li>



<li>Cleaned text content</li>
</ul>



<p>This makes <code data-enlighter-language="python" class="EnlighterJSRAW">getPageText()</code> the bridge from the browser page to an LLM-ready prompt.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-46.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1020" height="563" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-46.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55050"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-46.png?size=126x70&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-46-300x166.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-46.png?size=378x209&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-46.png?size=504x278&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-46.png?size=630x348&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-46-768x424.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-46.png?lossy=2&strip=1&webp=1 1020w" sizes="(max-width: 1020px) 100vw, 1020px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> How <code>chrome.scripting.executeScript</code> injects page-reading logic into the active tab (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-Groq-Streaming-Call-Works"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-Groq-Streaming-Call-Works">How the Groq Streaming Call Works</a></h2>



<p>Once the extension has page content, it needs to send a chat completion request and render the answer in real time.</p>



<h3 class="wp-block-heading">streamResponse() Checks for the API Key First</h3>



<p>The function opens with a guard clause:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="19">if (!apiKey) {
  apiKeySection.style.display = "flex";
  showSystem("Please set your Groq API key first.", "error");
  return;
}</pre>



<p>This is good defensive programming. Before the app makes a network request, it verifies that the minimum required state is present.</p>



<h3 class="wp-block-heading">The Request Body Sends the Full Conversation</h3>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">fetch</code> call includes:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="20">body: JSON.stringify({
  model: MODEL,
  messages: chatHistory,
  stream: true,
  temperature: 0.7,
  max_tokens: 1024,
})</pre>



<p>There are 2 especially important details here.</p>



<h4 class="wp-block-heading">messages: chatHistory</h4>



<p>This is how the model gets context. Instead of sending only the newest user message, the extension sends the full running conversation.</p>



<p>As a result, the model can handle:</p>



<ul class="wp-block-list">
<li>Follow-up questions</li>



<li>Clarifications</li>



<li>Multi-turn interaction grounded in prior turns</li>
</ul>



<h4 class="wp-block-heading">stream: true</h4>



<p>This tells the API to return response content instead of waiting for the full answer to be finished.</p>



<p>That is what gives the extension its chat-like feel.</p>



<h3 class="wp-block-heading">The Response Is Read as a Stream</h3>



<p>The code then creates:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="21">const reader  = response.body.getReader();
const decoder = new TextDecoder();</pre>



<p>This is where the frontend side starts to feel more advanced. Instead of calling <code data-enlighter-language="python" class="EnlighterJSRAW">await response.json()</code>, the extension reads raw chunks from the response body.</p>



<h3 class="wp-block-heading">Server-Sent Events (SSE) Are Parsed Line by Line</h3>



<p>The code looks for lines that begin with: <code data-enlighter-language="python" class="EnlighterJSRAW">data:</code></p>



<p>It then trims the prefix, checks for <code data-enlighter-language="python" class="EnlighterJSRAW">[DONE]</code>, and parses the JavaScript Object Notation (JSON) payload:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="22">const parsed = JSON.parse(payload);
const token  = parsed.choices?.[0]?.delta?.content ?? "";</pre>



<p>If content is present, the function sends it to <code data-enlighter-language="python" class="EnlighterJSRAW">onChunk(token)</code>.</p>



<h3 class="wp-block-heading">Why the UI Feels Responsive</h3>



<p>Every arriving content chunk updates the assistant bubble immediately. That means the user does not stare at a frozen popup waiting for a full paragraph to appear all at once.</p>



<p>There is a practical UX lesson here:</p>



<ul class="wp-block-list">
<li>Streaming improves perceived speed</li>



<li>Real-time rendering makes the app feel more interactive</li>



<li>Even a simple UI can feel polished if feedback arrives continuously</li>
</ul>



<h3 class="wp-block-heading">One Subtle Engineering Note</h3>



<p>This implementation splits each decoded chunk by newline and skips JSON parsing errors when a partial fragment arrives at a chunk boundary.</p>



<p>For a teaching demo, that tradeoff is acceptable. In a more robust production version, you would usually maintain a rolling buffer so partial SSE lines can be reconstructed correctly before parsing.</p>



<p>That is a valuable lesson too: simple implementations are often ideal for learning, even when they are not yet industrial-strength.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-47.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1002" height="566" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-47.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55054"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-47.png?size=126x71&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-47-300x169.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-47.png?size=378x214&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-47.png?size=504x285&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-47.png?size=630x356&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-47-768x434.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-47.png?lossy=2&strip=1&webp=1 1002w" sizes="(max-width: 1002px) 100vw, 1002px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> Token streaming flow from the Groq API into the popup chat bubble (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-Conversation-State-Is-Managed"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-Conversation-State-Is-Managed">How Conversation State Is Managed</a></h2>



<p>A good chat interface is really a state-management problem wearing a friendly UI.</p>



<h3 class="wp-block-heading">sendMessage() Handles the Main Interaction Loop</h3>



<p>At a high level, <code data-enlighter-language="python" class="EnlighterJSRAW">sendMessage()</code> does the following:</p>



<ul class="wp-block-list">
<li>Cleans the user input</li>



<li>Pushes the user message into <code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code></li>



<li>Renders the user bubble</li>



<li>Disables the input while the model responds</li>



<li>Creates an empty assistant bubble</li>



<li>Streams response content into that bubble</li>



<li>Saves the final assistant response back into <code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code></li>



<li>Re-enables the input</li>
</ul>



<p>This is the core interaction cycle of the whole extension.</p>



<h3 class="wp-block-heading">Why Create an Empty Assistant Bubble First</h3>



<p>This line is the trick:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="23">const assistantBubble = appendMessage("assistant", "");</pre>



<p>Instead of waiting for the final answer, the UI creates a placeholder bubble and fills it as response content arrive. That is what makes streaming visible to the user.</p>



<h3 class="wp-block-heading">Auto-Scroll Keeps the Newest Response Content in View</h3>



<p>During streaming, the code does:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="24">chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;</pre>



<p>This small line is easy to miss, but it matters. Without it, long responses would stream out of view and the interface would feel clumsy.</p>



<h3 class="wp-block-heading">The System Prompt Is Injected When Needed</h3>



<p>When the user clicks <strong>Send</strong> on a fresh chat, the code makes sure the system prompt is present:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="25">if (chatHistory.length === 0) {
  chatHistory.push({ role: "system", content: SYSTEM_PROMPT });
}</pre>



<p>This is a compact but important pattern. It ensures that every conversation begins with the assistant&#8217;s behavioral instructions, even if the user skips the summarize flow and goes straight to a custom question.</p>



<h3 class="wp-block-heading">The Enter Key Is Tuned for Chat UX</h3>



<p>This handler:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="26">if (e.key === "Enter" &amp;&amp; !e.shiftKey) {
  e.preventDefault();
  sendBtn.click();
}</pre>



<p>gives the interface a familiar messaging behavior:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">Enter</code> sends</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Shift + Enter</code> can still be used for a multiline prompt</li>
</ul>



<p>Small details like this make the project feel thoughtful.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-Summarize-Workflow-Works"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-Summarize-Workflow-Works">How the Summarize Workflow Works</a></h2>



<p>The <strong>Summarize Page</strong> button is where all the major ideas in the project come together.</p>



<h3 class="wp-block-heading">The Button First Updates Its Own UI State</h3>



<p>When clicked, it temporarily changes to: <code data-enlighter-language="python" class="EnlighterJSRAW">"Reading page..."</code></p>



<p>This gives the user immediate feedback that work has started.</p>



<h3 class="wp-block-heading">The History Is Reset for a Page-First Conversation</h3>



<p>Inside the click handler, the code sets:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="27">chatHistory = [
  { role: "system", content: SYSTEM_PROMPT },
];</pre>



<p>This is a deliberate design decision. The summarize flow creates a fresh conversation centered on the current page.</p>



<h3 class="wp-block-heading">The Page Content Is Converted into a User Message</h3>



<p>The extension builds a message like this:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="28">const userMessage =
  `Please summarise the following webpage.\n\n` +
  `Title: ${page.title}\nURL: ${page.url}\n\nContent:\n${page.text}`;</pre>



<p>This is a clever and practical technique. Instead of inventing a separate prompt schema, the extension simply turns page context into a normal user message. That means the rest of the chat pipeline can stay unchanged.</p>



<h3 class="wp-block-heading">Why This Design Is Elegant</h3>



<p>The same <code data-enlighter-language="python" class="EnlighterJSRAW">sendMessage()</code> function handles:</p>



<ul class="wp-block-list">
<li>Free-form user questions</li>



<li>A page summary request</li>
</ul>



<p>That reduces duplicated logic and keeps the codebase teachable.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-How-Clear-Button-Resets-Interface"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-How-Clear-Button-Resets-Interface">How the Clear Button Resets the Interface</a></h2>



<p>The clear handler is short, but it teaches an important UI principle.</p>



<h3 class="wp-block-heading">Reset Both State and Visible Output</h3>



<p>When the user clicks <strong>Clear</strong>, the code:</p>



<ul class="wp-block-list">
<li>Empties <code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code></li>



<li>Restores the welcome message HTML inside <code data-enlighter-language="python" class="EnlighterJSRAW">#chat-history</code></li>
</ul>



<p>This gives the user a clean slate both logically and visually.</p>



<p>In many small projects, bugs appear because developers reset one layer but forget the other. This extension avoids that trap by resetting both.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-End-to-End-Flow-From-Click-Answer"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-End-to-End-Flow-From-Click-Answer">End-to-End Flow, From Click to Answer</a></h2>



<p>Let us put the whole lesson together in one concrete sequence.</p>



<h3 class="wp-block-heading">What Happens When the User Clicks Summarize Page</h3>



<ul class="wp-block-list">
<li>Chrome opens the extension popup.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">popup.js</code> loads and restores any saved API key from <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.storage.local</code>.</li>



<li>The user clicks <strong>Summarize Page</strong>.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">getPageText()</code> asks Chrome for the active tab.</li>



<li>Chrome injects a function into that page using <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.scripting.executeScript</code>.</li>



<li>The injected function clones the page body, removes noisy elements, and returns title, URL, and cleaned text.</li>



<li>The popup resets <code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code> with the system prompt.</li>



<li>The popup creates a user message containing the page content.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">sendMessage()</code> renders the user bubble and creates an empty assistant bubble.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">streamResponse()</code> sends the chat request to Groq with <code data-enlighter-language="python" class="EnlighterJSRAW">stream: true</code>.</li>



<li>The popup reads SSE chunks, extracts response content, and updates the assistant bubble live.</li>



<li>When streaming ends, the full assistant message is saved into <code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code>.</li>
</ul>



<p>That is the full extension lifecycle in one pass.</p>



<h3 class="wp-block-heading">What Happens on the Next Follow-Up Question</h3>



<p>The second turn is even more interesting:</p>



<ul class="wp-block-list">
<li>The user asks a follow-up question in the textarea.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">sendMessage()</code> appends that new message to the existing <code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code>.</li>



<li>The full conversation is sent again to the model.</li>



<li>The model now has access to both the page context and the prior summary.</li>
</ul>



<p>That is what makes the extension feel conversational instead of one-shot.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-48-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="704" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-48-1024x704.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55059"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-48-1024x704.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-48-1024x704.png?size=126x87&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-48-1024x704.png?size=252x173&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-48-1024x704.png?size=378x260&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-48-1024x704.png?size=504x347&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-48-1024x704.png?size=630x433&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> The popup after summarizing a real webpage, showing the user’s summary request and the streamed assistant response (source: author)</figcaption></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-49-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="743" height="1024" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-49-743x1024.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55061"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-49-743x1024.png?lossy=2&strip=1&webp=1 743w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-49-743x1024.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-49-743x1024.png?size=252x347&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-49-743x1024.png?size=378x521&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-49-743x1024.png?size=504x695&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-49-743x1024.png?size=630x868&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 743px) 100vw, 743px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> A follow-up question in the popup, demonstrating that <code>chatHistory</code> preserves multi-turn context (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Practical-Engineering-Takeaways"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Practical-Engineering-Takeaways">Practical Engineering Takeaways</a></h2>



<p>This extension may be small, but it teaches several patterns that show up in larger AI products too.</p>



<h3 class="wp-block-heading">Pattern 1: Retrieve First, Generate Second</h3>



<p>The extension does not ask the model to guess what is on the page. It retrieves the page content first, then sends that content to the model.</p>



<p>The retrieval step is simple, but it is foundational.</p>



<h3 class="wp-block-heading">Pattern 2: Separate UI State from Persistent State</h3>



<p>The API key lives in <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.storage.local</code>.</p>



<p>The active conversation lives in memory as <code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code>.</p>



<p>This is a healthy separation because:</p>



<ul class="wp-block-list">
<li>Long-lived secrets survive popup closures</li>



<li>Short-lived conversations reset naturally when the session changes</li>
</ul>



<h3 class="wp-block-heading">Pattern 3: Stream Whenever Responsiveness Matters</h3>



<p>Streaming is not just a flashy feature. It changes how the user experiences latency.</p>



<p>Even if the total response time stays similar, streaming makes the system feel faster and more alive.</p>



<h3 class="wp-block-heading">Pattern 4: Keep the First Version Intentionally Small</h3>



<p>This extension could have included:</p>



<ul class="wp-block-list">
<li>A background service worker</li>



<li>Conversation persistence across popup sessions</li>



<li>Rich markdown rendering</li>



<li>Better page extraction heuristics</li>



<li>More robust streaming buffering</li>
</ul>



<p>But for a lesson, the current scope is exactly right. It is complete enough to be useful and small enough to fully understand.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-50-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="366" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-50-1024x366.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-55064"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-50-1024x366.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-50-1024x366.png?size=126x45&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-50-1024x366.png?size=252x90&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-50-1024x366.png?size=378x135&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-50-1024x366.png?size=504x180&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-50-1024x366.png?size=630x225&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 8:</strong> Inspecting the Groq chat completion request in DevTools to debug extension networking and payload structure (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Where-You-Could-Take-Project-Next"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Where-You-Could-Take-Project-Next">Where You Could Take This Project Next</a></h2>



<p>Once learners understand this version, there are several natural improvements worth exploring.</p>



<h3 class="wp-block-heading">Improve Content Extraction</h3>



<p>Right now, the extension removes a few noisy tags and then uses <code data-enlighter-language="python" class="EnlighterJSRAW">innerText</code>.</p>



<p>A stronger version could:</p>



<ul class="wp-block-list">
<li>Prefer <code data-enlighter-language="python" class="EnlighterJSRAW">main</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">article</code>-like containers when present</li>



<li>Skip repeated sidebar content more aggressively</li>



<li>Chunk long pages instead of truncating them at 8,000 characters</li>
</ul>



<h3 class="wp-block-heading">Improve Streaming Robustness</h3>



<p>The current SSE parsing is easy to understand, which is excellent for a first lesson. A more production-oriented version could keep a line buffer across chunk boundaries before parsing JSON.</p>



<h3 class="wp-block-heading">Improve Conversation Persistence</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">chatHistory</code> currently lives only in memory. If you close the popup, the conversation disappears.</p>



<p>That is fine for a lesson, but a future version could store conversations in <code data-enlighter-language="python" class="EnlighterJSRAW">chrome.storage.local</code> or IndexedDB.</p>



<h3 class="wp-block-heading">Improve Prompting</h3>



<p>The current system prompt is short and sensible. A later lesson could show how to:</p>



<ul class="wp-block-list">
<li>Ask for citation-style answers grounded in extracted text</li>



<li>Detect when the page content is too thin</li>



<li>Adapt response style for summarization versus question answering</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you saw how a compact Chrome extension can combine browser APIs and LLM APIs into a practical, real-time workflow.</p>



<p>You learned how:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">manifest.json</code>: declares the extension&#8217;s privileges and popup entry point</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">popup.html</code>: structures the user interface</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">styles.css</code>: makes the UI readable and responsive</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">README.md</code>: supports learning and onboarding</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">popup.js</code>: ties everything together through storage, page extraction, streaming, and chat state</li>
</ul>



<p>Most importantly, you learned the core architectural idea behind the project:</p>



<ul class="wp-block-list">
<li>read the current page with the proper Chrome API</li>



<li>turn that content into model-ready context</li>



<li>stream the answer back into a lightweight interface</li>
</ul>



<p>This pattern is simple, useful, and widely applicable. Once you understand it here, you can reuse it in richer browser tools, agent workflows, and AI-powered productivity extensions.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V</strong><strong>. </strong>“Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/8q9l7" target="_blank" rel="noreferrer noopener">https://pyimg.co/8q9l7</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API" data-enlighter-group="29">@incollection{Singh_2026_make-chrome-extension-digest-webpages-manifest-v3-groq-api,
  author = {Vikram Singh},
  title = {{Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/8q9l7},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/17/make-a-chrome-extension-to-digest-webpages-with-manifest-v3-and-groq-api/">Make a Chrome Extension to Digest Webpages with Manifest V3 and Groq API</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning</title>
		<link>https://pyimagesearch.com/2026/08/10/scaling-optimizing-and-exporting-transformers-with-pytorch-lightning/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 10 Aug 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Deep Learning]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Model Deployment]]></category>
		<category><![CDATA[PyTorch]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[ddp]]></category>
		<category><![CDATA[distributed data parallel]]></category>
		<category><![CDATA[fsdp]]></category>
		<category><![CDATA[gradient accumulation]]></category>
		<category><![CDATA[mixed precision training]]></category>
		<category><![CDATA[mlops]]></category>
		<category><![CDATA[model deployment]]></category>
		<category><![CDATA[onnx]]></category>
		<category><![CDATA[onnx export]]></category>
		<category><![CDATA[pytorch lightning]]></category>
		<category><![CDATA[torchscript]]></category>
		<category><![CDATA[transformer models]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=54889</guid>

					<description><![CDATA[<p>Table of Contents Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning Introduction to Scaling PyTorch Lightning Transformer Training Configuring Your Development Environment Preparing PyTorch Lightning Models for Scalable Multi-GPU Training Revisiting the Code Architecture Enabling Mixed Precision Training with PyTorch&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/10/scaling-optimizing-and-exporting-transformers-with-pytorch-lightning/">Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Scaling-Optimizing-Exporting-Transformers-PyTorch-Lightning"><a rel="noopener" target="_blank" href="#h1-Scaling-Optimizing-Exporting-Transformers-PyTorch-Lightning">Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning</a></li>

    <li id="TOC-h2-Introduction-Scaling-PyTorch-Lightning-Transformer-Training"><a rel="noopener" target="_blank" href="#h2-Introduction-Scaling-PyTorch-Lightning-Transformer-Training">Introduction to Scaling PyTorch Lightning Transformer Training</a></li>

    <li id="TOC-h2-Configuring-Development-Environment"><a rel="noopener" target="_blank" href="#h2-Configuring-Development-Environment">Configuring Your Development Environment</a></li>

    <li id="TOC-h2-Preparing-PyTorch-Lightning-Models-Scalable-Multi-GPU-Training"><a rel="noopener" target="_blank" href="#h2-Preparing-PyTorch-Lightning-Models-Scalable-Multi-GPU-Training">Preparing PyTorch Lightning Models for Scalable Multi-GPU Training</a></li>

    <li id="TOC-h2-Revisiting-Code-Architecture"><a rel="noopener" target="_blank" href="#h2-Revisiting-Code-Architecture">Revisiting the Code Architecture</a></li>

    <li id="TOC-h2-Enabling-Mixed-Precision-Training-PyTorch-Lightning-AMP"><a rel="noopener" target="_blank" href="#h2-Enabling-Mixed-Precision-Training-PyTorch-Lightning-AMP">Enabling Mixed Precision Training with PyTorch Lightning AMP</a></li>

    <li id="TOC-h2-Distributed-Training-PyTorch-Lightning-DDP-Multi-GPU-Scaling"><a rel="noopener" target="_blank" href="#h2-Distributed-Training-PyTorch-Lightning-DDP-Multi-GPU-Scaling">Distributed Training with PyTorch Lightning DDP for Multi-GPU Scaling</a></li>

    <li id="TOC-h2-Gradient-Accumulation-Large-Effective-Batch-Sizes"><a rel="noopener" target="_blank" href="#h2-Gradient-Accumulation-Large-Effective-Batch-Sizes">Gradient Accumulation for Large Effective Batch Sizes</a></li>

    <li id="TOC-h2-Exporting-PyTorch-Lightning-Transformer-Models-ONNX-TorchScript"><a rel="noopener" target="_blank" href="#h2-Exporting-PyTorch-Lightning-Transformer-Models-ONNX-TorchScript">Exporting PyTorch Lightning Transformer Models to ONNX and TorchScript</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Scaling-Optimizing-Exporting-Transformers-PyTorch-Lightning"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Scaling-Optimizing-Exporting-Transformers-PyTorch-Lightning">Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning</a></h2>



<p>In this lesson, you will learn how to scale, optimize, and export Transformer models using PyTorch Lightning, from enabling mixed precision and distributed training to generating ONNX and TorchScript exports ready for production inference. You will see how Lightning’s configuration-driven design, combined with Hydra, lets you take the exact same training code from Lesson 1 and push it into a high-performance, deployment-friendly workflow.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured.png?lossy=2&strip=1&webp=1" alt="scaling-optimizing-exporting-transformers-pytorch-lightning-featured.png" class="wp-image-54920"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/scaling-optimizing-exporting-transformers-pytorch-lightning-featured.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p>This lesson is the last in a 2-part series on <strong>PyTorch Lightning</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/5fe4l" target="_blank" rel="noreferrer noopener">Training with PyTorch Lightning: Structured MLOps Development</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/2xntj" target="_blank" rel="noreferrer noopener">Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning</a></strong></em><strong> (this tutorial)</strong></li>
</ol>



<p><strong>To learn how to scale, optimize, and export Transformer models with PyTorch Lightning,</strong><em><strong> just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Introduction-Scaling-PyTorch-Lightning-Transformer-Training"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Introduction-Scaling-PyTorch-Lightning-Transformer-Training">Introduction to Scaling PyTorch Lightning Transformer Training</a></h2>



<p>Scaling Transformer training and preparing models for real-world deployment typically requires complex engineering effort such as distributed training, mixed-precision optimization, and multiple export formats. But thanks to PyTorch Lightning and Hydra, you can achieve all of this without rewriting your codebase or adding low-level boilerplate. In this lesson, we will take the sentiment-classification project you built earlier and upgrade it into a fast, scalable, and deployment-ready workflow.</p>



<h3 class="wp-block-heading">What We Built Previously</h3>



<p>In the previous tutorial, you created a complete sentiment classification pipeline using DistilBERT.</p>



<p>You structured your project with:</p>



<ul class="wp-block-list">
<li>A <strong>LightningDataModule</strong> to handle dataset loading, tokenization, and data loaders</li>



<li>A <strong>LightningModule</strong> containing the model, forward pass, loss, metrics, and optimizer</li>



<li>A clean <strong>Hydra configuration hierarchy</strong> that controlled every part of the training run</li>



<li>A flexible <strong>training script</strong> that handled seeding, callbacks, logging, and TensorBoard</li>



<li>An <strong>inference script</strong> for single, batch, and interactive predictions</li>
</ul>



<p>This foundation gave you a fully reproducible training workflow that runs the same way on CPU, GPU, or MPS, with settings that can be overridden easily using Hydra’s command-line syntax.</p>



<p>However, training a base model is only the first step in a production ML workflow.</p>



<h3 class="wp-block-heading">Why Scaling and Exporting Matter</h3>



<p>Real-world Transformer workloads often push beyond what a single GPU or even a single machine can handle.</p>



<p>Teams need:</p>



<ul class="wp-block-list">
<li><strong>Mixed precision (FP16</strong><strong> or </strong><strong>BF16):</strong> for faster training and lower memory usage</li>



<li><strong>Distributed data parallel training:</strong> to use multiple GPUs efficiently</li>



<li><strong>Gradient accumulation:</strong> to simulate large batch sizes</li>



<li><strong>Model exports (ONNX</strong><strong> or </strong><strong>TorchScript):</strong> for deployment in production systems</li>



<li><strong>Lightweight inference runtimes:</strong> faster than PyTorch eager mode</li>



<li><strong>Stable artifact folders:</strong> ready for DVC, CI/CD, or cloud deployment pipelines</li>
</ul>



<p>These features turn a research-grade script into an MLOps-grade training system, and that is exactly what you will build in this lesson.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-28.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="358" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-28-1024x358.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54974"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-28-1024x358.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-28-1024x358.png?size=126x44&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-28-1024x358.png?size=252x88&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-28-1024x358.png?size=378x132&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-28-1024x358.png?size=504x176&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-28-1024x358.png?size=630x220&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> The 4 stages for scaling Transformer workflows from single-GPU training to mixed precision, multi-GPU distributed training, and finally ONNX or TorchScript export for optimized inference.</figcaption></figure></div>


<h3 class="wp-block-heading">How Lightning Makes It Easy</h3>



<p>The best part is that none of this requires rewriting your training loop.</p>



<p>PyTorch Lightning abstracts the hard parts:</p>



<ul class="wp-block-list">
<li><strong>Mixed precision:</strong> uses a single configuration key</li>



<li><strong>Distributed training (DDP</strong><strong> or </strong><strong>FSDP):</strong> requires only a strategy change</li>



<li><strong>Export logic:</strong> can be cleanly added without modifying model code</li>



<li><strong>Hydra:</strong> lets you switch between configurations instantly</li>



<li><strong>Checkpoints, logs, and metrics:</strong> remain fully reproducible</li>
</ul>



<p>Instead of modifying your DataModule or LightningModule, you will extend the <strong>trainer configuration</strong> and add a dedicated <strong>export script</strong>.</p>



<p>That is the power of Lightning: the same codebase now supports single-GPU training, multi-GPU scaling, and deployment-ready exports with minimal changes.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuring-Development-Environment"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuring-Development-Environment">Configuring Your Development Environment</a></h2>



<p>Lesson 2 introduces new capabilities (e.g., distributed training, mixed precision, and exporting models to ONNX or TorchScript), so the environment now includes additional dependencies specifically meant for scaling and production-grade inference.</p>



<p>Below is the exact <code data-enlighter-language="python" class="EnlighterJSRAW">requirements.txt</code> used in this lesson:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="1"># Lesson 2 Requirements
# Additional dependencies for production features

# PyTorch and Lightning
torch>=2.1.0
torchvision>=0.16.0
torchaudio>=2.1.0
pytorch-lightning>=2.1.0

# Hugging Face ecosystem
transformers>=4.35.0
datasets>=2.14.0
tokenizers>=0.15.0

# Configuration management
hydra-core>=1.3.0
omegaconf>=2.3.0

# Metrics and monitoring
torchmetrics>=1.2.0
tensorboard>=2.15.0

# Model export (LESSON 2 specific)
onnx>=1.15.0
onnxruntime>=1.16.0  # CPU version works on all platforms

# Data processing
numpy>=1.24.0
pandas>=2.0.0

# Utilities
tqdm>=4.66.0
pyyaml>=6.0.0

# Note: For NVIDIA GPUs on Linux, you can optionally install:
# onnxruntime-gpu>=1.16.0  # Requires CUDA, Linux only</pre>



<p>​​This environment enables 3 major Lesson 2 features:</p>



<h3 class="wp-block-heading">1. Multi-GPU Distributed Training</h3>



<p>Powered by:</p>



<ul class="wp-block-list">
<li>PyTorch Lightning (DDP or FSDP strategies)</li>



<li>Hydra configurations for trainer selection</li>
</ul>



<h3 class="wp-block-heading">2. Mixed Precision</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">torch &gt;= 2.x</code> unlocks CPU BF16, GPU AMP, and faster matrix kernels.</p>



<h3 class="wp-block-heading">3. ONNX Export and Runtime Inference</h3>



<p>The addition of <code data-enlighter-language="python" class="EnlighterJSRAW">onnx</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">onnxruntime</code> enables:</p>



<ul class="wp-block-list">
<li>model export to ONNX format</li>



<li>cross-platform CPU inference</li>



<li>benchmarking ONNX vs PyTorch (optional)</li>
</ul>



<p>If you are using an NVIDIA GPU on Linux, you can optionally install:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="2">pip install onnxruntime-gpu</pre>



<p>This is <strong>not</strong> required for the lesson (we stay framework-agnostic), but readers who want GPU ONNX inference can enable it easily.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Preparing-PyTorch-Lightning-Models-Scalable-Multi-GPU-Training"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Preparing-PyTorch-Lightning-Models-Scalable-Multi-GPU-Training">Preparing PyTorch Lightning Models for Scalable Multi-GPU Training</a></h2>



<p>Before we scale training, enable DDP or FSDP, or export models to ONNX or TorchScript, it is important that the project structure and configuration layout are set up correctly. Lesson 2 builds directly on the modular design from Lesson 1, but introduces specialized trainer configs and improved environment setup that unlock multi-GPU training and optimized inference.</p>



<p>Your updated project structure now reflects these goals.</p>



<h3 class="wp-block-heading">Project Structure Refresher (Updated for Lesson 2)</h3>



<p>Below is the exact directory layout used in this lesson:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="3">.
├── configs
│   ├── config.yaml
│   ├── data
│   │   └── imdb.yaml
│   ├── model
│   │   └── distilbert.yaml
│   └── trainer
│       ├── ddp.yaml
│       ├── fsdp.yaml
│       └── single_gpu.yaml
├── README.md
├── requirements.txt
├── RUN.md
├── sample_reviews.txt
└── src
    ├── data_module.py
    ├── inference.py
    ├── model_module.py
    └── train.py</pre>



<p>This layout introduces <strong>3 major upgrades</strong> compared to Lesson 1:</p>



<h4 class="wp-block-heading">Dedicated Trainer Configurations</h4>



<p>You now have separate Hydra configurations under <code data-enlighter-language="python" class="EnlighterJSRAW">configs/trainer/</code> for:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">single_gpu.yaml</code>: standard 1-GPU or CPU training</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">ddp.yaml</code>: multi-GPU Distributed Data Parallel (DDP)</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">fsdp.yaml</code>: Fully Sharded Data Parallel (memory-efficient large-model training)</li>
</ul>



<p>This keeps the scaling logic completely <em>outside</em> the Python code (a major MLOps advantage).</p>



<h4 class="wp-block-heading">Unified Root Configuration (config.yaml)</h4>



<p>The root configuration composes the model, data, and trainer settings into a single experiment specification.</p>



<p>Switching between single-GPU and DDP training is as simple as:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="4">python src/train.py trainer=ddp</pre>



<p>No code changes or additional flags are required. By default, the project uses the DDP trainer (<code data-enlighter-language="python" class="EnlighterJSRAW">trainer=ddp</code>), but you can switch to single-GPU training by running <code data-enlighter-language="python" class="EnlighterJSRAW">python src/train.py trainer=single_gpu</code>.</p>



<h4 class="wp-block-heading">Clean Separation of Code Files</h4>



<p>Your <code data-enlighter-language="python" class="EnlighterJSRAW">src/</code> folder remains identical to Lesson 1 (<code data-enlighter-language="python" class="EnlighterJSRAW">data_module.py</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">model_module.py</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">inference.py</code>). This reinforces the Lesson 2 philosophy:</p>



<p><strong>“Scaling and exporting should not require modifying your model code.”</strong></p>



<p>Only the <code data-enlighter-language="python" class="EnlighterJSRAW">configs/</code> files change, not the implementation.</p>



<h3 class="wp-block-heading">Hydra Enhancements for Scaling</h3>



<p>Lesson 2 is where Hydra truly shines.</p>



<p>Instead of hardcoding distributed training logic in Python, you now have clean trainer profiles:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="5">configs/trainer/
│── single_gpu.yaml
│── ddp.yaml
└── fsdp.yaml</pre>



<p>Each YAML file defines:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">accelerator</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">devices</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">strategy</code> (<code data-enlighter-language="python" class="EnlighterJSRAW">ddp</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">fsdp</code>)</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">precision</code></li>



<li>logging settings</li>
</ul>



<p>Examples a reader will use later:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="6">python src/train.py trainer=single_gpu
python src/train.py trainer=ddp
python src/train.py trainer=fsdp</pre>



<p>Hydra swaps in the right configuration automatically.</p>



<p>This is exactly what you expect in a real MLOps pipeline.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-34.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="937" height="687" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-34.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54987"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-34.png?size=126x92&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-34-300x220.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-34.png?size=378x277&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-34.png?size=504x370&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-34.png?size=630x462&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-34-768x563.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-34.png?lossy=2&strip=1&webp=1 937w" sizes="(max-width: 937px) 100vw, 937px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> Hydra composes the single-GPU, DDP, and FSDP trainer configurations into the PyTorch Lightning Trainer, enabling scalable training without changing any code.</figcaption></figure></div>


<h3 class="wp-block-heading">Why This Setup Matters (Before We Scale)</h3>



<p>This lightweight structure unlocks all the heavy features coming next:</p>



<ul class="wp-block-list">
<li><strong>Training:</strong> can scale from <strong>CPU</strong> to <strong>GPU</strong> to <strong>m</strong><strong>ulti-GPU</strong> without touching code</li>



<li><strong>Export pipelines (ONNX</strong><strong> or </strong><strong>TorchScript):</strong> work consistently because configs capture preprocessing</li>



<li><strong>FSDP and mixed precision:</strong> require stable config-driven Trainer settings</li>



<li><strong>Model reproducibility:</strong> is guaranteed across environments</li>
</ul>



<p>Most importantly:</p>



<p><strong>You now have a production-grade pattern:</strong></p>



<ul class="wp-block-list">
<li><strong>Model logic:</strong> stays in <code data-enlighter-language="python" class="EnlighterJSRAW">model_module.py</code></li>



<li><strong>Data logic:</strong> stays in <code data-enlighter-language="python" class="EnlighterJSRAW">data_module.py</code></li>



<li><strong>Training logic:</strong> stays in <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code></li>



<li><strong>Scaling </strong><strong>and </strong><strong>exporting logic:</strong> stays in <code data-enlighter-language="python" class="EnlighterJSRAW">configs/*</code></li>
</ul>



<p>This is the exact separation used in serious MLOps workflows.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Revisiting-Code-Architecture"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Revisiting-Code-Architecture">Revisiting the Code Architecture</a></h2>



<p>Before we introduce mixed precision, distributed training, gradient accumulation, and model export, it is important to ground ourselves in the architecture we built in the previous tutorial. Lesson 1 gave us a clean, modular foundation. Lesson 2 builds directly on this foundation without changing most of the code. That is the real strength of PyTorch Lightning and Hydra: scaling does not require rewriting your project.</p>



<h3 class="wp-block-heading">DataModule, LightningModule, and Hydra Recap</h3>



<p>Our project continues to rely on the same 3 core abstractions:</p>



<h4 class="wp-block-heading">LightningDataModule</h4>



<p>Handles all data concerns:</p>



<ul class="wp-block-list">
<li><strong>Dataset:</strong> downloading the IMDB dataset</li>



<li><strong>Tokenization:</strong> using a Hugging Face tokenizer</li>



<li><strong>Data loaders:</strong> creating training, validation, and test DataLoaders</li>



<li><strong>Separation of concerns:</strong> keeping preprocessing separate from training logic</li>
</ul>



<p>This structure remains untouched in Lesson 2. AMP, DDP, and export do <strong>not</strong> require modifying <code data-enlighter-language="python" class="EnlighterJSRAW">data_module.py</code>.</p>



<h4 class="wp-block-heading">LightningModule</h4>



<p>Encapsulates the model architecture and training logic:</p>



<ul class="wp-block-list">
<li><strong>Encoder:</strong> DistilBERT </li>



<li><strong>Forward pass:</strong> processing model inputs</li>



<li><strong>L</strong><strong>oss </strong><strong>and </strong><strong>metric</strong><strong>s:</strong> computing training loss and evaluation metrics</li>



<li><strong>O</strong><strong>ptimizer:</strong> configuring optimization</li>
</ul>



<p>Lesson 2 also does not modify the internal model logic.</p>



<p>Instead, we scale training through the Trainer and Hydra configuration.</p>



<h4 class="wp-block-heading">Hydra</h4>



<p>Hydra remains the <em>central controller</em> of the entire workflow.</p>



<p>It composes the configuration from:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">configs/model/*.yaml</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">configs/data/*.yaml</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">configs/trainer/*.yaml</code></li>
</ul>



<p>Lesson 2 extends Hydra with:</p>



<ul class="wp-block-list">
<li>DDP trainer configurations</li>



<li>FSDP configurations (if desired)</li>



<li>AMP configurations</li>



<li>gradient accumulation</li>



<li>export configurations</li>
</ul>



<p>But the training code (<code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code>) stays nearly the same.</p>



<p>This consistent architecture is what makes the next steps (i.e., scaling, exporting, and optimizing) feel incremental rather than overwhelming.</p>



<h3 class="wp-block-heading">Preparing for Advanced Training</h3>



<p>Lesson 2 introduces new capabilities commonly needed in real-world pipelines:</p>



<ul class="wp-block-list">
<li>multi-GPU distributed training</li>



<li>mixed precision training</li>



<li>larger effective batch sizes</li>



<li>ONNX or TorchScript model export</li>



<li>ONNX Runtime production inference </li>
</ul>



<p>Here is the key design rule we follow:</p>



<p><strong>We scale the system without touching the model or data code unless absolutely necessary.</strong></p>



<p>Lightning was built exactly for this: features (e.g., AMP and DDP) are injected through the <strong>Trainer</strong>, not through the model.</p>



<p>Hydra was built to help you <strong>swap configurations</strong> without modifying Python files. This keeps your codebase stable (a critical requirement for teams and MLOps pipelines).</p>



<p>We continue using Lightning’s <code data-enlighter-language="python" class="EnlighterJSRAW">ModelCheckpoint</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">LearningRateMonitor</code> callbacks to automatically track the best model and log learning-rate schedules during training.</p>



<h3 class="wp-block-heading">Configuration Extensions</h3>



<p>Lesson 2 adds new YAML files to support advanced features, but again, no source code changes are required. Each Hydra run is automatically logged to <code data-enlighter-language="python" class="EnlighterJSRAW">outputs/YYYY-MM-DD/HH-MM-SS/</code>, keeping experiments isolated and reproducible.</p>



<p>Your directory now includes:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="7">configs/
    trainer/
        single_gpu.yaml
        ddp.yaml
        fsdp.yaml</pre>



<p>These configurations enable:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">single_gpu.yaml</code>: local training</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">ddp.yaml</code>: distributed GPU training</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">fsdp.yaml</code>: model sharding for large models</li>



<li>future extensions (AMP, gradient accumulation, etc.)</li>
</ul>



<p>Because of Hydra’s override system, switching between them is as simple as:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="8">python src/train.py trainer=ddp
python src/train.py trainer=fsdp
python src/train.py trainer=single_gpu</pre>



<p>No code changes. No rewriting loops. No new training scripts.</p>



<p>This is exactly why Lightning and Hydra are so powerful: <strong>the same codebase can serve research, training at scale, and export pipelines without branching or duplication.</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Enabling-Mixed-Precision-Training-PyTorch-Lightning-AMP"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Enabling-Mixed-Precision-Training-PyTorch-Lightning-AMP">Enabling Mixed Precision Training with PyTorch Lightning AMP</a></h2>



<p>Mixed precision training is one of the easiest “free wins” you can get when training Transformer models. Instead of doing every operation in full 32-bit floating point (FP32), we let the GPU run most of the math in 16-bit (FP16 or BF16) while keeping a few critical values in FP32 for numerical stability. The result is faster training, lower memory usage, and almost identical accuracy, especially on modern GPUs and Apple Silicon.</p>



<p>PyTorch Lightning wraps all of this inside the Trainer so you do not have to touch autocast contexts or manual gradient scaling. In this lesson, we will enable mixed precision purely through Hydra configurations, and Lightning will handle the rest.</p>



<h3 class="wp-block-heading">Why Mixed Precision Boosts Transformers</h3>



<p>Transformer models are heavy on matrix multiplications, which GPUs are extremely good at accelerating in half precision. When you switch from FP32 to a mixed precision mode (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">16-mixed</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">bf16-mixed</code>):</p>



<ul class="wp-block-list">
<li>Many tensor operations run 1.5-2.5× faster.</li>



<li>Activations and gradients consume roughly half the memory.</li>



<li>You can often increase batch size without running out of video random-access memory (VRAM).</li>
</ul>



<p>Lightning uses PyTorch’s Automatic Mixed Precision (AMP) under the hood, so you still get stable training through automatic loss scaling. From an MLOps point of view, this is a small configuration change that can dramatically reduce training time and GPU cost without changing your codebase.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-30.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1007" height="674" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-30.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54976"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-30.png?size=126x84&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-30-300x201.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-30.png?size=378x253&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-30.png?size=504x337&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-30.png?size=630x422&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-30-768x514.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-30.png?lossy=2&strip=1&webp=1 1007w" sizes="(max-width: 1007px) 100vw, 1007px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> FP32 versus FP16 mixed precision (AMP), showing reduced memory usage and faster training through improved computational efficiency.</figcaption></figure></div>


<h3 class="wp-block-heading">Updating Hydra: precision: 16-mixed</h3>



<p>You do not need to modify <code data-enlighter-language="python" class="EnlighterJSRAW">data_module.py</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">model_module.py</code>. Mixed precision is controlled entirely through the <strong>trainer configuration</strong>.</p>



<p>Here is the <strong>single-GPU</strong> trainer configuration in <code data-enlighter-language="python" class="EnlighterJSRAW">configs/trainer/single_gpu.yaml</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="9"># Single GPU Configuration with Mixed Precision
# For experimentation and development

strategy: auto
devices: 1
accelerator: auto

# Mixed precision for faster training
precision: 16-mixed

# Training configuration
max_epochs: 3

# Performance
deterministic: false
benchmark: true

# Checkpointing
enable_checkpointing: true

# Progress
enable_progress_bar: true
log_every_n_steps: 10</pre>



<p>Compared with an FP32 trainer, the required configuration change for mixed precision is:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="10">precision: 16-mixed</pre>



<p>Your <strong>DDP trainer</strong> in <code data-enlighter-language="python" class="EnlighterJSRAW">configs/trainer/ddp.yaml</code> is already AMP-ready too:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="11"># DDP (Distributed Data Parallel) Strategy
# Best for: Multi-GPU training on single node or multi-node setups
# Use when: Model fits in GPU memory, you want data parallelism
# Core distributed settings
strategy: ddp
devices: auto  # Use all available GPUs
accelerator: auto  # Auto-detect GPU/CPU

# Mixed precision for faster training
precision: 16-mixed  # FP16 automatic mixed precision

# Training configuration
max_epochs: 5  # More epochs for production
accumulate_grad_batches: 1  # Gradient accumulation (increase if OOM)

# Performance optimizations
deterministic: false  # Set to true for full reproducibility (slower)
benchmark: true  # Optimize CUDA kernels for input size

# Checkpointing
enable_checkpointing: true

# Progress tracking
enable_progress_bar: true
log_every_n_steps: 10

# DDP-specific optimizations
# ddp_find_unused_parameters: false  # Uncomment if you get DDP warnings</pre>



<p>For <strong>FSDP</strong>, you are using BF16 mixed precision by default (better stability on very large models):</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="12"># FSDP (Fully Sharded Data Parallel) Strategy
# Best for: Very large models that don't fit in single GPU memory
# Use when: Model parameters exceed GPU memory, need memory efficiency

# Core distributed settings
strategy: fsdp
devices: auto  # Use all available GPUs
accelerator: auto

# Mixed precision - BF16 recommended for FSDP
precision: bf16-mixed  # Better stability than FP16 for large models

# Training configuration
max_epochs: 5
accumulate_grad_batches: 1

# Performance settings
deterministic: false
benchmark: true

# Checkpointing
enable_checkpointing: true

# Progress tracking
enable_progress_bar: true
log_every_n_steps: 10
# FSDP-specific settings
# Note: FSDP automatically shards model parameters, gradients, and optimizer states
# This allows training models that wouldn't fit on a single GPU</pre>



<p>All of these configurations are passed to the <code data-enlighter-language="python" class="EnlighterJSRAW">Trainer</code> through:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="13">trainer = pl.Trainer(
    **cfg.trainer,
    callbacks=callbacks,
    logger=logger,
)</pre>



<p>So switching between FP32, FP16, and BF16 is just a matter of changing <code data-enlighter-language="python" class="EnlighterJSRAW">precision</code> in YAML (no changes to the training loop, no new context managers, and no extra boilerplate).</p>



<h3 class="wp-block-heading">When to Use FSDP (Fully Sharded Data Parallel)</h3>



<p>While DDP works well when the entire model fits on one GPU, <strong>FSDP</strong> is designed for cases where it <em>doesn’t</em>.</p>



<p>Instead of replicating the whole model on each GPU, FSDP <strong>shards</strong> parameters, gradients, and optimizer states across GPUs, allowing you to train models many times larger than a single GPU’s memory.</p>



<p>Lightning makes this just as simple as DDP:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="14">python src/train.py trainer=fsdp</pre>



<p>FSDP uses <strong>BF16 mixed precision</strong> by default (configured in <code data-enlighter-language="python" class="EnlighterJSRAW">configs/trainer/fsdp.yaml</code>), which provides better stability for large-scale Transformer models.</p>



<p>Use FSDP if:</p>



<ul class="wp-block-list">
<li>you are hitting OOM even with small batch sizes</li>



<li>you are experimenting with larger Transformer backbones</li>



<li>you want memory-efficient training across multiple GPUs</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-31.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1022" height="572" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-31.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54979"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-31.png?size=126x71&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-31-300x168.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-31.png?size=378x212&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-31.png?size=504x282&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-31.png?size=630x353&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-31-768x430.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-31.png?lossy=2&strip=1&webp=1 1022w" sizes="(max-width: 1022px) 100vw, 1022px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> Comparison of standard Distributed Data Parallel (DDP), which replicates the full model on every GPU, versus Fully Sharded Data Parallel (FSDP), which shards parameters, gradients, and optimizer states across GPUs, reducing memory usage and enabling larger model training.</figcaption></figure></div>


<h3 class="wp-block-heading">Running AMP on NVIDIA GPUs or Apple Silicon</h3>



<p>Once the configurations are in place, running AMP simply requires choosing the right trainer preset from the CLI.</p>



<p><strong>Single GPU </strong><strong>with</strong><strong> Mixed Precision (NVIDIA </strong><strong>GPU </strong><strong>or Apple Silicon):</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="15"># From the lesson2 directory
python src/train.py trainer=single_gpu</pre>



<p>Because <code data-enlighter-language="python" class="EnlighterJSRAW">trainer=single_gpu</code> already sets:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="16">accelerator: auto
devices: 1
precision: 16-mixed</pre>



<p>Lightning will:</p>



<ul class="wp-block-list">
<li>Use <strong>CUDA</strong> if an NVIDIA GPU is available,</li>



<li>Use <strong>MPS</strong> on a Mac with Apple Silicon,</li>



<li>Fall back to the CPU otherwise (mixed precision provides little benefit there).</li>
</ul>



<p>If you want to be explicit, you can override precision on the command line:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="17">python src/train.py trainer=single_gpu trainer.precision=32        # FP32 baseline
python src/train.py trainer=single_gpu trainer.precision=16-mixed # FP16 AMP</pre>



<p>The same pattern applies to DDP:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="18"># Use all available GPUs with AMP
python src/train.py trainer=ddp

# Or force 2 GPUs with mixed precision
python src/train.py trainer=ddp trainer.devices=2 trainer.precision=16-mixed</pre>



<p>For very large models using FSDP:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="19">python src/train.py trainer=fsdp trainer.precision=bf16-mixed</pre>



<p>In every case, you are only changing Hydra configuration values, while the underlying Lightning code path remains exactly the same.</p>



<h3 class="wp-block-heading">What Speedups Should You Expect?</h3>



<p>The exact speedup depends on your GPU, batch size, and model size, but for Transformer-style models like DistilBERT, you may see:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">1.5-2×</code><strong> faster</strong> iteration times</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">~50%</code><strong> lower</strong> activation memory usage</li>



<li>Similar validation accuracy to FP32</li>
</ul>



<p>If you want to quantify performance in your own environment, you can run a simple timing experiment:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="20"># FP32 (baseline)
time python src/train.py trainer=single_gpu trainer.precision=32 trainer.max_epochs=1

# FP16 mixed precision
time python src/train.py trainer=single_gpu trainer.precision=16-mixed trainer.max_epochs=1</pre>



<p>You can compare the wall-clock time and VRAM usage between the 2 runs. In real MLOps pipelines, this can translate into lower training costs and the ability to run larger experiments on the same hardware.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Distributed-Training-PyTorch-Lightning-DDP-Multi-GPU-Scaling"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Distributed-Training-PyTorch-Lightning-DDP-Multi-GPU-Scaling">Distributed Training with PyTorch Lightning DDP for Multi-GPU Scaling</a></h2>



<p>Scaling your model across multiple GPUs is one of the fastest ways to reduce training time for Transformer architectures. PyTorch Lightning makes this dramatically easier by handling the boilerplate (process spawning, gradient synchronization, device management, and checkpoint coordination). In Lesson 2, we enable <strong>Distributed Data Parallel (DDP)</strong> using a Hydra configuration file and a single command-line override.</p>



<p>Let us walk through how this works and how you can run multi-GPU training without changing a single line of Python code.</p>



<h3 class="wp-block-heading">What Distributed Data Parallel (DDP) Is</h3>



<p>Distributed Data Parallel (DDP) is PyTorch’s recommended approach for training models across multiple GPUs. Each GPU receives:</p>



<ul class="wp-block-list">
<li>a full copy of the model</li>



<li>a shard of the dataset</li>



<li>parallel forward and backward passes</li>
</ul>



<p>After each backward pass, gradients are synchronized across all GPUs so that every model replica remains synchronized.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-25-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="450" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-25-1024x450.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54952"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-25-1024x450.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-25-1024x450.png?size=126x55&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-25-1024x450.png?size=252x111&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-25-1024x450.png?size=378x166&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-25-1024x450.png?size=504x221&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-25-1024x450.png?size=630x277&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> How Distributed Data Parallel (DDP) works: each GPU trains a full model replica in parallel, and gradients are synchronized across workers to maintain a unified model state.</figcaption></figure></div>


<p>The benefit is simple:</p>



<p><strong>More GPUs → Larger global batch size → Faster training</strong></p>



<p>Lightning abstracts the entire DDP workflow, so you do not need to manually write any multiprocessing logic, barrier synchronization, or device placement. All of this is handled by <code data-enlighter-language="python" class="EnlighterJSRAW">Trainer(strategy="ddp")</code>.</p>



<h3 class="wp-block-heading">Hydra and Lightning Configuration (strategy: ddp)</h3>



<p>DDP is enabled entirely through configuration, not code.</p>



<p>Here is the Hydra configuration file you include in Lesson 2:</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">configs/trainer/ddp.yaml</code></p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="21"># Distributed training with DDP
strategy: ddp
accelerator: auto
devices: auto
precision: 16-mixed
max_epochs: 3

# Optional: helps stabilize multi-GPU runs
num_nodes: 1</pre>



<p>This file replaces the default trainer settings when you select it from the CLI.</p>



<p>Lightning reads this configuration and automatically enables:</p>



<ul class="wp-block-list">
<li>automatic GPU detection</li>



<li>multi-process spawning</li>



<li>distributed samplers for the DataModule</li>



<li>synchronized batch normalization</li>



<li>gradient synchronization across devices</li>



<li>safe checkpointing on rank 0</li>
</ul>



<p>You do not need to modify <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">DataModule</code>, or <code data-enlighter-language="python" class="EnlighterJSRAW">LightningModule</code>.</p>



<h3 class="wp-block-heading">Running Multi-GPU Training</h3>



<p>Use the following command to launch DDP:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="22">$ python src/train.py trainer=ddp</pre>



<p>To explicitly choose the number of devices:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="23">$ python src/train.py trainer=ddp trainer.devices=2</pre>



<p>To train on all visible GPUs:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="24">$ python src/train.py trainer=ddp trainer.devices=auto</pre>



<p>You can also combine overrides:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="25">$ python src/train.py trainer=ddp trainer.precision=bf16-mixed data.batch_size=16</pre>



<p>Hydra composes the configurations, Lightning spawns the GPU workers, and DDP executes the training loop.</p>



<p>No additional code is required.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Gradient-Accumulation-Large-Effective-Batch-Sizes"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Gradient-Accumulation-Large-Effective-Batch-Sizes">Gradient Accumulation for Large Effective Batch Sizes</a></h2>



<p>Modern Transformer models often benefit from larger batch sizes because they produce smoother gradients, more stable optimization, and sometimes higher accuracy in fewer iterations. However, large batches require more GPU memory, and even a mid-sized GPU may not be able to process them directly.</p>



<p><strong>Gradient accumulation</strong> solves this by splitting a large batch across multiple smaller forward passes. Lightning collects gradients over several steps before performing one optimizer update, giving you the effect of large-batch training <em>without increasing memory usage</em>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-32.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="924" height="526" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-32.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54982"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-32.png?size=126x72&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-32-300x171.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-32.png?size=378x215&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-32.png?size=504x287&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-32.png?size=630x359&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-32-768x437.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-32.png?lossy=2&strip=1&webp=1 924w" sizes="(max-width: 924px) 100vw, 924px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> Gradient accumulation simulates large-batch training by accumulating gradients across multiple small forward and backward passes before applying a single optimizer update.</figcaption></figure></div>


<h3 class="wp-block-heading">Why Gradient Accumulation Helps</h3>



<p>Instead of running:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">Batch size = 64</code> (requires large GPU memory)</li>
</ul>



<p>…you can simulate it as:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">batch_size = 8</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">accumulate_grad_batches = 8</code></li>
</ul>



<p>Lightning will:</p>



<ul class="wp-block-list">
<li>Run <strong>8 forward</strong><strong> and </strong><strong>backward passes</strong></li>



<li>Accumulate gradients internally</li>



<li>Call <code data-enlighter-language="python" class="EnlighterJSRAW">optimizer.step()</code> <strong>only once</strong></li>
</ul>



<p>This produces <strong>the same gradient update</strong> you would get from a single batch of size 64 while using only the memory required for a batch of 8.</p>



<p>This is especially useful when:</p>



<ul class="wp-block-list">
<li>You hit <strong>CUDA OOM errors</strong> during DDP or FSDP training</li>



<li>You want larger effective batch sizes for stability</li>



<li>You are training with <strong>mixed precision</strong>, which can benefit from larger batches</li>



<li>You are using limited GPU hardware, including Apple Silicon</li>
</ul>



<h3 class="wp-block-heading">Updating Hydra Configurations</h3>



<p>Your Lesson 2 repository supports gradient accumulation through Hydra overrides.</p>



<p>The primary trainer configurations (<code data-enlighter-language="python" class="EnlighterJSRAW">ddp.yaml</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">fsdp.yaml</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">single_gpu.yaml</code>) set:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="26">accumulate_grad_batches: 1</pre>



<p>This means accumulation is <strong>opt-in</strong> and controlled by command-line overrides.</p>



<p>You can also create a dedicated Hydra configuration:</p>



<h3 class="wp-block-heading">configs/trainer/accum.yaml (optional)</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="27">accumulate_grad_batches: 4</pre>



<p>However, this is not required because the command-line interface (CLI) override is often clearer and more flexible.</p>



<h3 class="wp-block-heading">Using Gradient Accumulation via CLI</h3>



<p>Because Hydra merges configurations top-down, you can override accumulation using the following options:</p>



<h3 class="wp-block-heading">Single GPU</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="28">python src/train.py trainer=single_gpu trainer.accumulate_grad_batches=4</pre>



<h3 class="wp-block-heading">DDP (multi-GPU)</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="29">python src/train.py trainer=ddp trainer.accumulate_grad_batches=4</pre>



<h3 class="wp-block-heading">With Mixed Precision</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="30">python src/train.py trainer=ddp trainer.precision=16-mixed trainer.accumulate_grad_batches=8</pre>



<h3 class="wp-block-heading">With Large Batches</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="31">python src/train.py trainer=ddp data.batch_size=4 trainer.accumulate_grad_batches=8</pre>



<p>In this case:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="32">effective_batch_size = batch_size × accumulate_grad_batches × num_gpus</pre>



<p>For example, with:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">batch_size = 4</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">accumulate_grad_batches = 8</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">num_gpus = 2</code></li>
</ul>



<p>the effective batch size is:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="33">4 × 8 × 2 = 64</pre>



<p>This configuration normally requires <code data-enlighter-language="python" class="EnlighterJSRAW">~16-20 GB</code> of GPU memory but is now possible even on a laptop GPU.</p>



<h3 class="wp-block-heading">Batch Size vs Memory Tradeoffs</h3>



<p>Gradient accumulation directly affects:</p>



<ul class="wp-block-list">
<li><strong>Lower Memory Consumption:</strong> Only the small per-step batch must fit in memory.</li>



<li><strong>Equivalent Gradient Updates:</strong> The optimizer receives the same gradient update as full-size training.</li>



<li><strong>Slightly Longer Training Time:</strong> Lightning performs more forward and backward passes before each update. However, this approach is still more efficient than dealing with out-of-memory (OOM) errors or reducing the sequence length or tokenizer settings.</li>
</ul>



<h3 class="wp-block-heading">Lightning Integration (Zero Code Changes)</h3>



<p>Your Lesson 2 <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code> requires <strong>zero</strong> code modification.</p>



<p>Lightning handles:</p>



<ul class="wp-block-list">
<li>gradient scaling</li>



<li>accumulation logic</li>



<li>optimizer stepping</li>



<li>multi-GPU gradient synchronization</li>



<li>mixed-precision scaling</li>
</ul>



<p>You control the behavior through Hydra configurations.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Exporting-PyTorch-Lightning-Transformer-Models-ONNX-TorchScript"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Exporting-PyTorch-Lightning-Transformer-Models-ONNX-TorchScript">Exporting PyTorch Lightning Transformer Models to ONNX and TorchScript</a></h2>



<p>Training a model is only half the story because real-world ML systems need fast, portable, and framework-agnostic inference. In Lesson 2, you extend your training pipeline to automatically export your best checkpoint into two production-ready formats:</p>



<ul class="wp-block-list">
<li><strong>ONNX:</strong> for cross-platform, high-performance inference (ONNX Runtime, TensorRT, OpenVINO)</li>



<li><strong>TorchScript:</strong> for PyTorch-native, C++ or mobile deployment</li>
</ul>



<p>The key design goal is that none of this export logic lives inside the <code data-enlighter-language="python" class="EnlighterJSRAW">LightningModule</code>. Instead, the export code is isolated in <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code> and driven entirely by Hydra configuration. This keeps your model clean and your training loop reusable in both research and production settings.</p>



<h3 class="wp-block-heading">Why Export?</h3>



<p>Exported models provide several benefits:</p>



<p><strong>Fast inference:</strong> ONNX Runtime routinely delivers <strong>2-5× faster CPU inference</strong> than PyTorch eager mode.</p>



<p><strong>Portability:</strong> A single ONNX file runs on:</p>



<ul class="wp-block-list">
<li>Linux, macOS, and Windows</li>



<li>mobile devices</li>



<li>GPU runtimes (e.g., TensorRT)</li>



<li>serverless platforms (AWS Lambda with ONNX Runtime)</li>
</ul>



<p><strong>Reproducible Production Pipelines:</strong> TorchScript provides a stable, serialized version of your model for:</p>



<ul class="wp-block-list">
<li>C++ backends</li>



<li>embedded devices</li>



<li>custom inference servers</li>



<li>TorchServe</li>
</ul>



<p><strong>DVC-ready artifact tracking: </strong>Exports drop cleanly into:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="34">artifacts/models/
artifacts/metrics/</pre>



<p>These artifacts can be versioned and tracked, just like code.</p>



<h3 class="wp-block-heading">Enabling Export via Hydra</h3>



<p>Exports are controlled by your root configuration:</p>



<h4 class="wp-block-heading">configs/config.yaml</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="35"># Export configuration (LESSON 2 feature)
export:
  enabled: true       # Enable model export after training
  onnx: true         # Export to ONNX format
  torchscript: true  # Export to TorchScript format</pre>



<p>You can override these settings at runtime:</p>



<h4 class="wp-block-heading">Export Both Formats</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="36">python src/train.py trainer=ddp export.enabled=true</pre>



<h4 class="wp-block-heading">Export Only ONNX</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="37">python src/train.py export.enabled=true export.torchscript=false</pre>



<h4 class="wp-block-heading">Export Only TorchScript</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="38">python src/train.py export.enabled=true export.onnx=false</pre>



<p>Lightning handles training, <code data-enlighter-language="python" class="EnlighterJSRAW">ModelCheckpoint</code> saves the best checkpoint, and then your script reloads that checkpoint for export.</p>



<h3 class="wp-block-heading">How the Export Pipeline Works</h3>



<p>During export, the script automatically reloads the best checkpoint saved by <code data-enlighter-language="python" class="EnlighterJSRAW">ModelCheckpoint</code>, ensuring the exported ONNX or TorchScript files always correspond to the best validation score.</p>



<p>Your <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code> contains 2 production-grade export utilities:</p>



<h4 class="wp-block-heading">ONNX Export</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="39">export_to_onnx(best_model, onnx_path, cfg.data.max_length)</pre>



<h4 class="wp-block-heading">TorchScript Export</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="40">export_to_torchscript(best_model, ts_path, cfg.data.max_length)</pre>



<p>Both functions:</p>



<ul class="wp-block-list">
<li>load the best checkpoint</li>



<li>switch the model to evaluation mode</li>



<li>move it to the CPU</li>



<li>construct a dummy input of shape <code data-enlighter-language="python" class="EnlighterJSRAW">(1, max_length)</code></li>



<li>save the exported file under <code data-enlighter-language="python" class="EnlighterJSRAW">artifacts/models/</code></li>
</ul>



<p>This separation ensures:</p>



<ul class="wp-block-list">
<li>no modification to <code data-enlighter-language="python" class="EnlighterJSRAW">model_module.py</code></li>



<li>clean Transformer traceability</li>



<li>reproducibility across runs</li>
</ul>



<h3 class="wp-block-heading">ONNX Export Details </h3>



<p>The utility inside <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code> uses:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="41">torch.onnx.export(
    model,
    (dummy_input_ids, dummy_attention_mask),
    str(output_path),
    opset_version=14,
    input_names=["input_ids", "attention_mask"],
    output_names=["logits"],
    dynamic_axes={
        "input_ids": {0: "batch_size", 1: "sequence_length"},
        "attention_mask": {0: "batch_size", 1: "sequence_length"},
        "logits": {0: "batch_size"},
    },
)</pre>



<p>Key features:</p>



<p><strong>Dynamic axes:</strong> Allow variable batch sizes and sequence lengths at inference time.</p>



<p><strong>Opset 14:</strong> Compatible with ONNX Runtime and TensorRT.</p>



<p><strong>Framework-agnostic:</strong> You can deploy the same <code data-enlighter-language="python" class="EnlighterJSRAW">.onnx</code> file using:</p>



<ul class="wp-block-list">
<li>ONNX Runtime</li>



<li>NVIDIA TensorRT</li>



<li>OpenVINO</li>



<li>Triton Inference Server</li>



<li>AWS Lambda (serverless inference)</li>
</ul>



<h3 class="wp-block-heading">ONNX Runtime Inference Example</h3>



<p>Readers can test the exported model using:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="42">import onnxruntime as ort
import numpy as np

session = ort.InferenceSession("artifacts/models/sentiment_classifier.onnx")

input_ids = np.random.randint(0, 1000, (1, 128), dtype=np.int64)
attention_mask = np.ones((1, 128), dtype=np.int64)

outputs = session.run(None, {
    "input_ids": input_ids,
    "attention_mask": attention_mask
})

print("Logits:", outputs[0])</pre>



<p>This should produce logits similar to those from PyTorch inference.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-33.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="982" height="535" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-33.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54985"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-33.png?size=126x69&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-33-300x163.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-33.png?size=378x206&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-33.png?size=504x275&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-33.png?size=630x343&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-33-768x418.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-33.png?lossy=2&strip=1&webp=1 982w" sizes="(max-width: 982px) 100vw, 982px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> ONNX Runtime inference pipeline: input tensors are passed to an ONNX Runtime session, which executes the model and produces outputs through an optimized inference runtime with cross-platform support.</figcaption></figure></div>


<h3 class="wp-block-heading">TorchScript Export Details </h3>



<p>The utility inside <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code> uses:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="43">traced_model = torch.jit.trace(
    model,
    (dummy_input_ids, dummy_attention_mask)
)
traced_model.save(str(output_path))</pre>



<p>TorchScript provides:</p>



<h4 class="wp-block-heading">PyTorch-Native Deployment</h4>



<p>Works with:</p>



<ul class="wp-block-list">
<li>TorchServe</li>



<li>custom C++ services</li>



<li>mobile runtimes</li>



<li>embedded systems</li>
</ul>



<h4 class="wp-block-heading">Stable, Production-Safe Serialization</h4>



<p>Unlike Python pickles, TorchScript provides a stable serialized format for multi-process and multi-node systems.</p>



<h3 class="wp-block-heading">Artifacts Stored in a DVC-Ready Structure</h3>



<p>After export completes, your script creates:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="44">artifacts/
├── models/
│   ├── sentiment_classifier.onnx
│   ├── sentiment_classifier.pt
└── metrics/
    └── metrics.json</pre>



<p>Your script also generates <code data-enlighter-language="python" class="EnlighterJSRAW">artifacts/metrics/metrics.json</code>, which contains the final validation loss and accuracy. Your continuous integration and continuous deployment (CI/CD) pipeline, Data Version Control (DVC), or model registry can consume this file.</p>



<p>This folder is well suited for DVC tracking:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="45">dvc add artifacts/models/
dvc add artifacts/metrics/</pre>



<h3 class="wp-block-heading">End-to-End Export Command</h3>



<p>A typical production run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="46">python src/train.py \
    trainer=ddp \
    trainer.precision=16-mixed \
    export.enabled=true</pre>



<p>produces:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="47">Exported: sentiment_classifier.onnx
Exported: sentiment_classifier.pt
Saved metrics.json</pre>



<p>Both formats are validated, versioned, and ready for deployment.</p>



<h3 class="wp-block-heading">Zero Modifications to Model Code</h3>



<p>Because the <code data-enlighter-language="python" class="EnlighterJSRAW">LightningModule</code> remains unchanged, you maintain:</p>



<ul class="wp-block-list">
<li>readability</li>



<li>testability</li>



<li>modularity</li>



<li>compatibility with future lessons (CI/CD, DVC, deployments)</li>
</ul>



<p>The entire export logic is kept in one place.</p>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you transformed a simple sentiment-classification project into a scalable, production-ready training system using PyTorch Lightning and Hydra. You learned how to enable mixed precision to speed up training, use Distributed Data Parallel (DDP) to leverage multiple GPUs, and apply gradient accumulation to simulate large batch sizes without increasing memory usage. All of these upgrades were achieved without modifying your model or data code, demonstrating the benefits of Lightning’s abstraction and Hydra’s configuration-driven design.</p>



<p>You also implemented a clean and reusable export pipeline, allowing the same trained model to be saved as ONNX or TorchScript and used in lightweight, fast inference systems. With a small export utility built directly into the <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code> script and a clear configuration structure, you now have a workflow that produces consistent, portable artifacts suitable for CI/CD, edge devices, or cloud deployment.</p>



<p>Taken together, these enhancements provide a robust foundation for real-world ML engineering. You have taken the same codebase from Lesson 1 and upgraded it with improved performance, scalability, and deployment-ready outputs. This prepares you for next steps such as deployment, optimization, monitoring, or integrating your models into full MLOps pipelines.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V. </strong>“Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/2xntj" target="_blank" rel="noreferrer noopener">https://pyimg.co/2xntj</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning" data-enlighter-group="48">@incollection{Singh_2026_scaling-optimizing-exporting-transformers-pytorch-lightning,
  author = {Vikram Singh},
  title = {{Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/2xntj},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/10/scaling-optimizing-and-exporting-transformers-with-pytorch-lightning/">Scaling, Optimizing, and Exporting Transformers with PyTorch Lightning</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Training with PyTorch Lightning: Structured MLOps Development</title>
		<link>https://pyimagesearch.com/2026/08/03/training-with-pytorch-lightning-structured-mlops-development/</link>
		
		<dc:creator><![CDATA[Vikram Singh]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Deep Learning]]></category>
		<category><![CDATA[MLOps]]></category>
		<category><![CDATA[Natural Language Processing]]></category>
		<category><![CDATA[PyTorch]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[checkpointing]]></category>
		<category><![CDATA[deep learning]]></category>
		<category><![CDATA[distilbert]]></category>
		<category><![CDATA[huggingface transformers]]></category>
		<category><![CDATA[hydra]]></category>
		<category><![CDATA[mlops]]></category>
		<category><![CDATA[model training]]></category>
		<category><![CDATA[nlp]]></category>
		<category><![CDATA[pytorch lightning]]></category>
		<category><![CDATA[reproducible ml]]></category>
		<category><![CDATA[sentiment analysis]]></category>
		<category><![CDATA[tensorboard]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=54839</guid>

					<description><![CDATA[<p>Table of Contents Training with PyTorch Lightning: Structured MLOps Development Why PyTorch Lightning Improves Reproducible MLOps Training Pipelines Configuring Your Development Environment Project Structure PyTorch LightningDataModule Explained: Building Efficient Data Pipelines PyTorch LightningModule Explained: Building Modular Deep Learning Models Using&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/03/training-with-pytorch-lightning-structured-mlops-development/">Training with PyTorch Lightning: Structured MLOps Development</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Training-PyTorch-Lightning-Structured-MLOps-Development"><a rel="noopener" target="_blank" href="#h1-Training-PyTorch-Lightning-Structured-MLOps-Development">Training with PyTorch Lightning: Structured MLOps Development</a></li>

    <li id="TOC-h2-Why-PyTorch-Lightning-Improves-Reproducible-MLOps-Training-Pipelines"><a rel="noopener" target="_blank" href="#h2-Why-PyTorch-Lightning-Improves-Reproducible-MLOps-Training-Pipelines">Why PyTorch Lightning Improves Reproducible MLOps Training Pipelines</a></li>

    <li id="TOC-h2-Configuring-Development-Environment"><a rel="noopener" target="_blank" href="#h2-Configuring-Development-Environment">Configuring Your Development Environment</a></li>

    <li id="TOC-h2-Project-Structure"><a rel="noopener" target="_blank" href="#h2-Project-Structure">Project Structure</a></li>

    <li id="TOC-h2-PyTorch-LightningDataModule-Explained-Building-Efficient-Data-Pipelines"><a rel="noopener" target="_blank" href="#h2-PyTorch-LightningDataModule-Explained-Building-Efficient-Data-Pipelines">PyTorch LightningDataModule Explained: Building Efficient Data Pipelines</a></li>

    <li id="TOC-h2-PyTorch-LightningModule-Explained-Building-Modular-Deep-Learning-Models"><a rel="noopener" target="_blank" href="#h2-PyTorch-LightningModule-Explained-Building-Modular-Deep-Learning-Models">PyTorch LightningModule Explained: Building Modular Deep Learning Models</a></li>

    <li id="TOC-h2-Using-Hydra-Python-Configuration-Files-Reproducible-ML-Training"><a rel="noopener" target="_blank" href="#h2-Using-Hydra-Python-Configuration-Files-Reproducible-ML-Training">Using Hydra Python Configuration Files for Reproducible ML Training</a></li>

    <li id="TOC-h2-Building-PyTorch-Lightning-Training-Pipeline-train-py"><a rel="noopener" target="_blank" href="#h2-Building-PyTorch-Lightning-Training-Pipeline-train-py">Building a PyTorch Lightning Training Pipeline with train.py</a></li>

    <li id="TOC-h2-Running-Model-Inference-PyTorch-Lightning-DistilBERT"><a rel="noopener" target="_blank" href="#h2-Running-Model-Inference-PyTorch-Lightning-DistilBERT">Running Model Inference with PyTorch Lightning and DistilBERT</a></li>

    <li id="TOC-h2-Training-Evaluating-DistilBERT-Model-PyTorch-Lightning-Hydra"><a rel="noopener" target="_blank" href="#h2-Training-Evaluating-DistilBERT-Model-PyTorch-Lightning-Hydra">Training and Evaluating a DistilBERT Model with PyTorch Lightning and Hydra</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Training-PyTorch-Lightning-Structured-MLOps-Development"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Training-PyTorch-Lightning-Structured-MLOps-Development">Training with PyTorch Lightning: Structured MLOps Development</a></h2>



<p>In this lesson, you will learn how to build a fully modular, reproducible, and production-friendly training pipeline using PyTorch Lightning and Hydra. We will train a sentiment-classification model with a clean MLOps-ready structure that scales as your projects grow.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured.png?lossy=2&strip=1&webp=1" alt="training-w-pytorch-lightning-structured-mlops-development-featured.png" class="wp-image-54855"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/training-w-pytorch-lightning-structured-mlops-development-featured.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p>This lesson is the 1st in a 2-part series on <strong>PyTorch Lightning</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/5fe4l" target="_blank" rel="noreferrer noopener">Training with PyTorch Lightning: Structured MLOps Development</a></strong></em> <strong>(this tutorial)</strong></li>



<li><em>Lesson 2</em></li>
</ol>



<p><strong>To learn how to structure deep learning code for reliable, maintainable training workflows,</strong><em><strong> just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Why-PyTorch-Lightning-Improves-Reproducible-MLOps-Training-Pipelines"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Why-PyTorch-Lightning-Improves-Reproducible-MLOps-Training-Pipelines">Why PyTorch Lightning Improves Reproducible MLOps Training Pipelines</a></h2>



<p>Deep learning projects begin as small experiments (e.g., a single script, a few functions, and a quick training loop). But as your model grows, your dataset expands, and your experimentation increases, that simple script becomes a bottleneck. It collects data loading, model code, training logic, metrics, logging, and CLI arguments all in one place. This makes the code harder to debug, harder to reproduce, and nearly impossible to scale.</p>



<p>From an MLOps perspective, this is a critical issue. Reproducibility, modularity, logging, version control, and collaboration all depend on a clean separation of concerns. When data, modeling, and training logic live inside one file, versioning becomes fragile. Dockerization becomes harder. Running distributed or mixed-precision training becomes messy. And experiment tracking ends up inconsistent.</p>



<p>This is exactly where <strong>PyTorch Lightning</strong> shines. Instead of mixing everything together, Lightning organizes your training workflow into well-defined components:</p>



<ul class="wp-block-list">
<li><strong>LightningModule:</strong> contains your model and training/validation/test logic</li>



<li><strong>LightningDataModule:</strong> manages data downloading, tokenization, and data loaders</li>



<li><strong>Trainer:</strong> handles the engineering: GPUs, mixed precision, checkpointing, logging, and training loops</li>
</ul>



<p>You keep your PyTorch code. Lightning handles the boilerplate.</p>



<p>This separation is not just “clean code.” It is an MLOps superpower. With structure in place, you can:</p>



<ul class="wp-block-list">
<li>Version your data, model code, and configs separately</li>



<li>Run consistent experiments across environments</li>



<li>Scale from CPU → single GPU → multi-GPU → multi-node without changing code</li>



<li>Produce reproducible artifacts for DVC and deployment</li>



<li>Integrate Hydra configs for repeatable pipelines</li>
</ul>



<p>In this lesson, you will see how Lightning transforms a typical sentiment-classification workflow into a modular, reproducible, production-ready training pipeline (i.e., the kind expected in real-world MLOps and LLMOps teams).</p>



<p>To learn how to structure reproducible ML training with PyTorch Lightning, just keep reading.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Would you like immediate access to 3,457 images curated and labeled with hand gestures to train, explore, and experiment with &#8230; for free? Head over to <a href="https://universe.roboflow.com/isl/az-6mqow?ref=pyimagesearch" target="_blank" rel="noreferrer noopener">Roboflow</a> and get a free account to grab these hand gesture images. </p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Configuring-Development-Environment"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Configuring-Development-Environment">Configuring Your Development Environment</a></h2>



<p>Before we begin implementing the training pipeline, let’s configure the development environment. This project uses modern versions of PyTorch, PyTorch Lightning, and the Hugging Face ecosystem, along with Hydra for configuration management. Installing the correct dependencies ensures that your training, data loading, and model export workflows run smoothly.</p>



<p>All the required libraries are pip-installable. Below is the full list of dependencies used in this lesson:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="1"># PyTorch and Lightning
torch>=2.1.0
torchvision>=0.16.0
torchaudio>=2.1.0
pytorch-lightning>=2.1.0

# Hugging Face ecosystem
transformers>=4.35.0
datasets>=2.14.0
tokenizers>=0.15.0

# Configuration management
hydra-core>=1.3.0
omegaconf>=2.3.0

# Metrics and monitoring
torchmetrics>=1.2.0
tensorboard>=2.15.0

# Data processing
numpy>=1.24.0
pandas>=2.0.0

# Utilities
tqdm>=4.66.0
pyyaml>=6.0.0</pre>



<p>You can install everything with a single command:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="2">!pip install -r requirements.txt</pre>



<p>Your project folder includes a <code data-enlighter-language="python" class="EnlighterJSRAW">requirements.txt</code> file containing all of the dependencies above. If you are working inside a clean virtual environment (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">venv</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">Conda</code>), this setup will give you everything needed to train the sentiment-classification model in Lesson 1.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<!-- wp:paragraph -->
<h3>Need Help Configuring Your Development Environment?</h3>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":18137,"sizeSlug":"large","linkDestination":"custom"} -->
<figure class="wp-block-image aligncenter size-large"><a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-18137" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?lossy=2&strip=1&webp=1 500w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=126x84&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=252x168&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2021/01/pyimagesearch_plus_jupyter.png?size=378x253&lossy=2&strip=1&webp=1 378w" sizes="(max-width: 500px) 100vw, 500px" /></a><figcaption>Having trouble configuring your development environment? Want access to pre-configured Jupyter Notebooks running on Google Colab? Be sure to join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">PyImageSearch University</a> — you will be up and running with this tutorial in a matter of minutes. </figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>All that said, are you:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Short on time?</li><li>Learning on your employer’s administratively locked system?</li><li>Wanting to skip the hassle of fighting with the command line, package managers, and virtual environments?</li><li><strong>Ready to run the code immediately on your Windows, macOS, or Linux system?</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Then join <a href="https://pyimagesearch.com/pyimagesearch-university/" target="_blank">PyImageSearch University</a> today!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Gain access to Jupyter Notebooks for this tutorial and other PyImageSearch guides pre-configured to run on Google Colab’s ecosystem right in your web browser!</strong> No installation required.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>And best of all, these Jupyter Notebooks will run on Windows, macOS, and Linux!</p>
<!-- /wp:paragraph -->



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Project-Structure"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Project-Structure">Project Structure</a></h2>



<p>Before we write any code, it is important to understand how a real MLOps-ready training workflow is organized. A clean project structure makes your training pipeline easier to maintain, debug, scale across machines, and integrate later with DVC, MLflow, and CI/CD. PyTorch Lightning encourages this modular layout (e.g., through structured components), and Hydra takes care of configuration management in a clean, reproducible way.</p>



<p>Below is the project structure we will use for Lesson 1:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="3">lesson1/
├── configs/
│   ├── config.yaml
│   ├── model/
│   │   └── distilbert.yaml
│   ├── data/
│   │   └── imdb.yaml
│   └── trainer/
│       └── default.yaml
└── src/
    ├── data_module.py
    ├── model_module.py
    ├── train.py
    └── inference.py</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">src/</code> folder contains all the Python logic:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">data_module.py</code>: handles dataset loading, tokenization, and data loaders</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">model_module.py</code>: defines the DistilBERT-based classifier</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code>: orchestrates the training loop using PyTorch Lightning</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">inference.py</code>: allows you to test the trained model interactively</li>
</ul>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">configs/</code> directory is where we store all configuration files, grouped by purpose (model, data, trainer). This keeps hyperparameters and training settings cleanly separated from code, which is a fundamental principle in MLOps.</p>



<h3 class="wp-block-heading">Hydra in 60 Seconds (All You Need for This Lesson)</h3>



<p>Hydra is a configuration framework from Facebook Research that lets us manage all training settings (e.g., model hyperparameters, data parameters, and trainer options) in clean, reusable YAML files. Instead of hard-coding values inside Python, Hydra loads these configs automatically and lets you override anything from the command line.</p>



<p>This makes your training pipeline more reproducible, easier to maintain, and far more MLOps-friendly.</p>



<p>For example, you can change batch size, learning rate, or precision without editing the code:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="4">python src/train.py data.batch_size=16 model.lr=3e-5 trainer.precision=16-mixed</pre>



<p>Hydra merges the YAML configs at runtime (using <code data-enlighter-language="python" class="EnlighterJSRAW">config.yaml</code> as the root), giving us a clean and scalable way to manage experiments.</p>



<p>Now that we understand how the project is organized and how Hydra helps us control training behavior, let us implement the <code data-enlighter-language="python" class="EnlighterJSRAW">LightningDataModule</code> and build the first part of our training pipeline.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-PyTorch-LightningDataModule-Explained-Building-Efficient-Data-Pipelines"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-PyTorch-LightningDataModule-Explained-Building-Efficient-Data-Pipelines">PyTorch LightningDataModule Explained: Building Efficient Data Pipelines</a></h2>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">LightningDataModule</code> is one of the most important building blocks in PyTorch Lightning. It packages every data-related responsibility (e.g., downloading, preprocessing, tokenizing, and creating data loaders) into a single reusable module. This keeps your training loop clean and preserves a sharp separation between <em>data code</em> and <em>model code</em>, which is essential in any production-ready MLOps workflow.</p>



<p>In sentiment classification, the DataModule encapsulates 3 main tasks:</p>



<ul class="wp-block-list">
<li><strong>loading the IMDB dataset:</strong> from Hugging Face</li>



<li><strong>tokenizing text:</strong> using a pretrained Transformer tokenizer</li>



<li><strong>producing data loaders:</strong> for train, validation, and test splits</li>
</ul>



<p>Let us walk through the implementation step-by-step.</p>



<p>Before we start examining the class, let us begin with the imports that power this component of the pipeline:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="5">import pytorch_lightning as pl
from datasets import load_dataset
from torch.utils.data import DataLoader
from transformers import AutoTokenizer</pre>



<p>These 4 imports define everything the DataModule needs. <code data-enlighter-language="python" class="EnlighterJSRAW">pytorch_lightning</code> gives us the <code data-enlighter-language="python" class="EnlighterJSRAW">LightningDataModule</code> base class, which enforces a structured, reproducible way to manage data. <code data-enlighter-language="python" class="EnlighterJSRAW">load_dataset</code> from Hugging Face handles downloading and preparing the IMDB dataset.</p>



<p>PyTorch’s <code data-enlighter-language="python" class="EnlighterJSRAW">DataLoader</code> will assemble batches and handle multiprocessing, and <code data-enlighter-language="python" class="EnlighterJSRAW">AutoTokenizer</code> loads the correct tokenizer for whichever transformer model we choose in the configuration.</p>



<p>Now that the foundations are clear, let us walk through the class itself.</p>



<h3 class="wp-block-heading">Initializing the DataModule (__init__)</h3>



<p>The constructor sets up every configurable part of the data pipeline.</p>



<p>Here is the code block we are examining:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="6">class SentimentDataModule(pl.LightningDataModule):
    def __init__(
        self,
        model_name: str = "distilbert-base-uncased",
        dataset_name: str = "imdb",
        max_length: int = 128,
        batch_size: int = 8,
        num_workers: int = 4,
        **kwargs
    ):
        super().__init__()
        self.save_hyperparameters()
        
        self.model_name = model_name
        self.dataset_name = dataset_name
        self.max_length = max_length
        self.batch_size = batch_size
        self.num_workers = num_workers
        
        self.tokenizer = None
        self.train_dataset = None
        self.val_dataset = None
        self.test_dataset = None</pre>



<p>The class inherits from <code data-enlighter-language="python" class="EnlighterJSRAW">pl.LightningDataModule</code>, which means Lightning expects this object to implement the standard methods (<code data-enlighter-language="python" class="EnlighterJSRAW">prepare_data</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">setup</code>, and the data loaders). The constructor receives all runtime-configurable parameters (e.g., which model tokenizer to use, which dataset to load, how long sequences should be, and what batch size to apply). These values are stored as instance attributes, but they are also captured automatically through <code data-enlighter-language="python" class="EnlighterJSRAW">self.save_hyperparameters()</code>, enabling 2 important MLOps behaviors: experiment reproducibility and checkpoint metadata tracking.</p>



<p>The module initializes empty placeholders for the tokenizer and each dataset split. Nothing is loaded at this point. This keeps initialization fast, makes the DataModule safe for multiprocessing, and follows Lightning’s recommended pattern where all heavyweight work happens in the next 2 methods: <code data-enlighter-language="python" class="EnlighterJSRAW">prepare_data()</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">setup()</code>.</p>



<h3 class="wp-block-heading">prepare_data(): One-Time Download Step</h3>



<p>Next is the method <code data-enlighter-language="python" class="EnlighterJSRAW">prepare_data()</code> that Lightning calls exactly once on a single process:</p>



<p>The constructor receives all hyperparameters needed to configure data preprocessing. Hydra will pass these values at runtime.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="7">def prepare_data(self):
    load_dataset(self.dataset_name)
    AutoTokenizer.from_pretrained(self.model_name)</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">prepare_data()</code> is responsible only for downloading the dataset and the tokenizer files. It does <strong>not</strong> tokenize or process anything; that comes later. The logic here intentionally has no side effects and does not assign anything to self. Lightning enforces this behavior because, in distributed training, only one GPU should perform downloads, while every GPU should independently run tokenization and data setup.</p>



<p>This separation is crucial for large-scale MLOps workflows because it prevents race conditions, avoids repetitive downloads, and maintains determinism across runs regardless of device or environment.</p>



<h3 class="wp-block-heading">The setup() Method: Tokenization and Dataset Preparation</h3>



<p>After the one-time download step in <code data-enlighter-language="python" class="EnlighterJSRAW">prepare_data()</code>, Lightning calls the <code data-enlighter-language="python" class="EnlighterJSRAW">setup()</code> method on <strong>every GPU</strong> (or CPU worker) participating in training. This is where the real work happens: tokenization, dataset formatting, and creating the train/validation/test splits. Because each device executes this method independently, the logic inside must be deterministic and side-effect-free, which is a core requirement when building reproducible training pipelines in distributed environments.</p>



<p>Here is the method we are explaining:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="8">def setup(self, stage=None):
    self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
    
    if stage == "fit" or stage is None:
        dataset = load_dataset(self.dataset_name)

        self.train_dataset = dataset["train"].map(
            self._tokenize_function,
            batched=True,
            remove_columns=dataset["train"].column_names,
            desc="Tokenizing train set"
        )
        self.train_dataset.set_format("torch")

        self.val_dataset = dataset["test"].map(
            self._tokenize_function,
            batched=True,
            remove_columns=dataset["test"].column_names,
            desc="Tokenizing validation set"
        )
        self.val_dataset.set_format("torch")
    
    if stage == "test" or stage is None:
        dataset = load_dataset(self.dataset_name)
        self.test_dataset = dataset["test"].map(
            self._tokenize_function,
            batched=True,
            remove_columns=dataset["test"].column_names,
            desc="Tokenizing test set"
        )
        self.test_dataset.set_format("torch")</pre>



<p>The first line initializes a fresh tokenizer instance. Lightning may spawn <strong>multiple processes</strong> in Distributed Data Parallel (DDP) or Fully Sharded Data Parallel (FSDP) mode, so each process needs its own tokenizer object. Tokenizers are stateless, lightweight, and safe to re-create, making them ideal for device-local initialization.</p>



<p>Lightning passes a stage identifier depending on what it is doing:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">fit</code>: when calling <code data-enlighter-language="python" class="EnlighterJSRAW">trainer.fit()</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">validate</code>: when calling <code data-enlighter-language="python" class="EnlighterJSRAW">trainer.validate()</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">test</code>: when calling <code data-enlighter-language="python" class="EnlighterJSRAW">trainer.test()</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">predict</code>: when calling <code data-enlighter-language="python" class="EnlighterJSRAW">trainer.predict()</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">None</code>: Lightning did not specify a stage (certain internal flows)</li>
</ul>



<p>Handling the <code data-enlighter-language="python" class="EnlighterJSRAW">None</code> stage ensures the DataModule works correctly in all cases.</p>



<p>Inside the <code data-enlighter-language="python" class="EnlighterJSRAW">fit</code> stage, we load the Hugging Face dataset. IMDB contains only <code data-enlighter-language="python" class="EnlighterJSRAW">train</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">test</code> splits. Since it does not ship with an official validation set, this lesson uses the <code data-enlighter-language="python" class="EnlighterJSRAW">test</code> split as validation. (In a real production MLOps pipeline, you would do a train/validation split yourself. This note will be added in the blog.)</p>



<p>Next comes tokenization. Hugging Face Datasets operates like a high-performance dataframe.</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">.map()</code> applies <code data-enlighter-language="python" class="EnlighterJSRAW">_tokenize_function</code> to <strong>every example or batch</strong>, efficiently processing the dataset:</p>



<ul class="wp-block-list">
<li>processes data in batches automatically</li>



<li>parallelizes when possible</li>



<li>avoids loading everything into memory</li>
</ul>



<p>This is far more efficient and cleaner than writing custom loops.</p>



<p>After tokenization, we call <code data-enlighter-language="python" class="EnlighterJSRAW">self.train_dataset.set_format("torch")</code>. This converts the dataset to output:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">torch.Tensor</code> for inputs</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">torch.Tensor</code> for labels</li>
</ul>



<p>which is exactly what PyTorch Lightning and the data loader expect. A nearly identical process prepares the validation dataset.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">test</code> stage works the same way. Lightning may call <code data-enlighter-language="python" class="EnlighterJSRAW">fit</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">test</code> <strong>separately</strong>, so we load and tokenize test data inside its own conditional block.</p>



<p>The logic inside <code data-enlighter-language="python" class="EnlighterJSRAW">setup()</code> must be safe to run:</p>



<ul class="wp-block-list">
<li>once per GPU</li>



<li>once per CPU worker</li>



<li>once per stage</li>
</ul>



<p>This is why nothing is written to disk or downloaded in this method. Everything is device-local: load → tokenize → return.</p>



<p>This is essential for:</p>



<ul class="wp-block-list">
<li>reproducibility</li>



<li>distributed training</li>



<li>multi-node training</li>



<li>deterministic behavior in automated pipelines</li>
</ul>



<h3 class="wp-block-heading">Tokenization Logic: The Heart of Text Preprocessing</h3>



<p>Tokenization is the most important preprocessing step in any natural language processing (NLP) pipeline. It converts raw text into numerical tensors that a Transformer model like DistilBERT can understand. Unlike image data (where pixel arrays are already numeric), text needs to be transformed from variable-length strings into fixed-length integer sequences.</p>



<p>In our DataModule, all tokenization runs through a single helper method:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="9">def _tokenize_function(self, examples):
    tokenized = self.tokenizer(
        examples["text"],
        truncation=True,
        padding="max_length",
        max_length=self.max_length,
    )
    tokenized["labels"] = examples["label"]
    return tokenized</pre>



<p>Let us break this down step-by-step.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">_tokenize_function</code> handles the core preprocessing step for every text sample in the dataset. Hugging Face Datasets calls this function in batched mode, so <code data-enlighter-language="python" class="EnlighterJSRAW">examples["text"]</code> is a list of movie reviews rather than a single string. The tokenizer processes the entire batch at once, which is significantly faster than looping over rows individually and also plays nicely with multiprocessing when multiple workers are specified in the data loader.</p>



<p>Inside the tokenizer call, we enable truncation and padding to a fixed <code data-enlighter-language="python" class="EnlighterJSRAW">max_length</code>. Truncation ensures that very long reviews do not exceed the model’s maximum input window, keeping GPU memory usage predictable. Padding ensures that shorter reviews still produce fixed-length tensors, which is not only important for creating uniform batches but also becomes essential later when exporting the model to ONNX or TorchScript in Lesson 2 because both formats expect consistent input shapes.</p>



<p>After tokenization, we attach the corresponding labels by copying <code data-enlighter-language="python" class="EnlighterJSRAW">examples["label"]</code> into the labels field. Hugging Face models expect this exact key during the forward pass when computing classification loss. This small remapping keeps the training step clean because the LightningModule can simply read <code data-enlighter-language="python" class="EnlighterJSRAW">batch["labels"]</code> without any additional preprocessing or renaming logic.</p>



<p>The method finally returns a dictionary containing <code data-enlighter-language="python" class="EnlighterJSRAW">input_ids</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">attention_mask</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">labels</code>. Lightning and Hugging Face Datasets take care of converting these into PyTorch tensors later when we call <code data-enlighter-language="python" class="EnlighterJSRAW">.set_format("torch")</code> in the <code data-enlighter-language="python" class="EnlighterJSRAW">setup()</code> method. This tokenization pattern is highly scalable because it supports parallel CPU tokenization, remains deterministic for reproducibility, and works seamlessly in multi-GPU environments like DDP or FSDP. It is the same preprocessing approach used in large-scale NLP pipelines, from academic benchmarks to production systems handling millions of records.</p>



<h3 class="wp-block-heading">Why This Approach Scales Well (MLOps Insight)</h3>



<p>A major advantage of this design is how well it scales in real-world MLOps environments. Because <code data-enlighter-language="python" class="EnlighterJSRAW">_tokenize_function</code> is used together with the Hugging Face <code data-enlighter-language="python" class="EnlighterJSRAW">.map()</code> API, tokenization automatically runs in parallel across CPU workers. This gives you high throughput even when preprocessing millions of text samples. More importantly, the transformation is deterministic (the same input will always produce the same tokenized output), which is critical when you need reproducibility across training runs, machines, or distributed setups.</p>



<p>This pattern also integrates cleanly with multi-GPU training strategies (e.g., DDP and FSDP). Each GPU receives its own shard of the dataset and applies the exact same preprocessing logic, avoiding subtle mismatches between processes. Memory usage stays efficient as well, because batches are generated lazily and only the final tensorized output is kept in memory.</p>



<p>Finally, centralizing all preprocessing logic inside a single helper function makes the pipeline easy to maintain. Whether you’re training on a single GPU, scaling out to multiple nodes, or running the same preprocessing for offline batch inference, this structure remains robust. It’s the same approach used in production NLP systems where datasets can reach tens of millions of records and preprocessing needs to remain both fast and reliable.</p>



<h3 class="wp-block-heading">Data Loaders: Batching Data for Training, Validation, and Testing</h3>



<p>Once the dataset has been downloaded, tokenized, and formatted as tensors, the final component of the DataModule is producing PyTorch data loaders. These data loaders handle shuffling, batching, multiprocessing, and memory pinning, which are all needed to efficiently feed data into the model during training.</p>



<p>Here are the 3 data loader methods in our DataModule:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="10">def train_dataloader(self):
    return DataLoader(
        self.train_dataset,
        batch_size=self.batch_size,
        shuffle=True,
        num_workers=self.num_workers,
        pin_memory=True
    )

def val_dataloader(self):
    return DataLoader(
        self.val_dataset,
        batch_size=self.batch_size,
        shuffle=False,
        num_workers=self.num_workers,
        pin_memory=True
    )

def test_dataloader(self):
    return DataLoader(
        self.test_dataset,
        batch_size=self.batch_size,
        shuffle=False,
        num_workers=self.num_workers,
        pin_memory=True
    )</pre>



<p>Let us walk through these line-by-line.</p>



<p>The final part of the DataModule defines the 3 data loaders Lightning will use for training, validation, and testing. Each method returns a standard PyTorch <code data-enlighter-language="python" class="EnlighterJSRAW">DataLoader</code>, but Lightning wires them together automatically inside <code data-enlighter-language="python" class="EnlighterJSRAW">trainer.fit()</code>, so you do not need to manually pass datasets around.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">train_dataloader()</code> creates batches from the tokenized training set and enables <code data-enlighter-language="python" class="EnlighterJSRAW">shuffle=True</code>. Shuffling is essential for training stability because it prevents the model from seeing samples in the same order every epoch, which helps it generalize better. The batch size and number of workers come directly from the DataModule’s configuration, allowing Hydra to control them externally.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">val_dataloader()</code> follows the same structure, except it disables shuffling. Validation should always process data in a fixed order because we want deterministic metrics that reflect model performance, not random input order.</p>



<p>Finally, the <code data-enlighter-language="python" class="EnlighterJSRAW">test_dataloader()</code> mirrors the validation setup (no shuffling and the same batching logic), ensuring that evaluation on the test set is consistent and repeatable.</p>



<p>All 3 data loaders also set <code data-enlighter-language="python" class="EnlighterJSRAW">pin_memory=True</code>, a small but meaningful performance optimization. When using GPUs, pinned memory allows faster host-to-device transfers, reducing input bottlenecks during training. Lightning will automatically handle device placement of data, so the data loaders simply provide ready-to-consume batches.</p>



<p>Overall, these 3 methods complete the DataModule’s lifecycle: downloading, tokenizing, splitting, and packaging the data into efficient data loaders. With these in place, the Trainer can run end-to-end training without any additional data plumbing from your side, which provides exactly the modularity and cleanliness we want in an MLOps-focused codebase.</p>



<h3 class="wp-block-heading">Why This Loader Setup Works Well for MLOps</h3>



<p>This pattern brings several production advantages:</p>



<ul class="wp-block-list">
<li><strong>Works seamlessly with DDP/FSDP across multiple GPUs:</strong> Lightning automatically replicates data loaders across processes</li>



<li><strong>Fully deterministic:</strong> Regenerating the same batches across runs supports experiment reproducibility</li>



<li><strong>Fast I/O due to multiprocessing + pinned memory:</strong> Critical for high-speed training workloads</li>



<li><strong>Clear boundaries for debugging:</strong> If a batch is wrong, you know the issue is in the DataModule, not the model</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-PyTorch-LightningModule-Explained-Building-Modular-Deep-Learning-Models"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-PyTorch-LightningModule-Explained-Building-Modular-Deep-Learning-Models">PyTorch LightningModule Explained: Building Modular Deep Learning Models</a></h2>



<p>The LightningModule is the heart of the training pipeline.</p>



<p>While the DataModule organizes <em>data</em>, the LightningModule organizes <em>learning</em> (the model architecture, forward pass, loss computation, metrics, and optimization strategy).</p>



<p>In plain terms, the LightningModule is where “what the model is” and “how the model learns” are defined.</p>



<p>For sentiment classification, the module wraps a pretrained DistilBERT encoder, adds a small classification head, and implements the training/validation/test logic needed for stable and reproducible NLP experiments.</p>



<h3 class="wp-block-heading">Initialization: Building the Model Architecture</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="11">class SentimentClassifier(pl.LightningModule):
    def __init__(
        self,
        model_name: str = "distilbert-base-uncased",
        num_labels: int = 2,
        dropout: float = 0.1,
        lr: float = 2e-5,
        weight_decay: float = 0.01,
        **kwargs
    ):
        super().__init__()
        self.save_hyperparameters()

        config = AutoConfig.from_pretrained(model_name)
        self.encoder = AutoModel.from_pretrained(model_name, config=config)

        self.dropout = nn.Dropout(dropout)
        self.classifier = nn.Linear(config.hidden_size, num_labels)

        self.criterion = nn.CrossEntropyLoss()

        self.train_acc = Accuracy(task="multiclass", num_classes=num_labels)
        self.val_acc = Accuracy(task="multiclass", num_classes=num_labels)
        self.val_f1 = F1Score(task="multiclass", num_classes=num_labels, average="macro")</pre>



<p>The constructor defines the core architecture and all hyperparameters needed for training. The use of <code data-enlighter-language="python" class="EnlighterJSRAW">self.save_hyperparameters()</code> ensures that every setting (from model name to learning rate) is automatically captured in Lightning’s checkpoint files. This is extremely valuable in MLOps workflows because it makes every experiment self-describing and fully reproducible.</p>



<p>A DistilBERT encoder is loaded using the Hugging Face <code data-enlighter-language="python" class="EnlighterJSRAW">AutoModel</code>, which instantly gives the model robust language understanding without any manual feature engineering. On top of that encoder, the model adds a simple dropout layer for regularization and a linear classifier to convert the encoded representation into sentiment logits. This small, clean architecture keeps training fast while still benefiting from the power of modern transformers.</p>



<p>Loss and metrics are also initialized here. Cross-entropy is the standard loss function for classification, while Accuracy and F1Score track performance during training and validation. Storing metrics inside the LightningModule keeps the entire training logic encapsulated cleanly in one place.</p>



<h3 class="wp-block-heading">Forward Pass: How the Model Produces Predictions</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="12">def forward(self, input_ids, attention_mask):
    outputs = self.encoder(
        input_ids=input_ids,
        attention_mask=attention_mask
    )
    pooled = outputs.last_hidden_state[:, 0]
    logits = self.classifier(self.dropout(pooled))
    return logits</pre>



<p>The forward method defines how a batch of tokenized text moves through the model. DistilBERT returns hidden states for all tokens, but for classification tasks it is sufficient to use the representation of the first token (the CLS token). The dropout + linear layer converts this pooled representation into raw class logits. Because we return logits directly, the LightningModule maintains full flexibility for computing loss and metrics in the training, validation, and test loops.</p>



<h3 class="wp-block-heading">Training Step: Computing Loss and Logging Metrics</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="13">def training_step(self, batch, batch_idx):
    logits = self(batch["input_ids"], batch["attention_mask"])
    loss = self.criterion(logits, batch["labels"])
    preds = torch.argmax(logits, dim=1)
    acc = self.train_acc(preds, batch["labels"])

    self.log("train_loss", loss, on_step=True, on_epoch=True, prog_bar=True)
    self.log("train_acc", acc, on_step=True, on_epoch=True, prog_bar=True)
    return loss</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">training_step</code> defines exactly how the model learns from each batch. It runs a forward pass, computes the loss against the ground-truth labels, and updates accuracy. Lightning’s <code data-enlighter-language="python" class="EnlighterJSRAW">self.log</code> handles aggregation across GPUs and ensures clean output in both the console and TensorBoard. Because Lightning abstracts away the optimizer step and gradient handling, this method focuses purely on the model’s learning logic rather than the boilerplate.</p>



<h3 class="wp-block-heading">Validation Step: Tracking Generalization</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="14">def validation_step(self, batch, batch_idx):
    logits = self(batch["input_ids"], batch["attention_mask"])
    loss = self.criterion(logits, batch["labels"])

    preds = torch.argmax(logits, dim=1)
    acc = self.val_acc(preds, batch["labels"])
    f1 = self.val_f1(preds, batch["labels"])

    self.log("val_loss", loss, on_epoch=True, prog_bar=True)
    self.log("val_acc", acc, on_epoch=True, prog_bar=True)
    self.log("val_f1", f1, on_epoch=True)
    return loss</pre>



<p>The validation step mirrors the training step but excludes gradient updates. It focuses entirely on monitoring how well the model generalizes. The addition of F1Score is especially useful in sentiment classification, where class imbalance is common and accuracy alone may hide poor performance on minority classes. Lightning automatically runs this method at the end of each epoch.</p>



<h3 class="wp-block-heading">Test Step: Final Evaluation</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="15">def test_step(self, batch, batch_idx):
    logits = self(batch["input_ids"], batch["attention_mask"])
    loss = self.criterion(logits, batch["labels"])

    preds = torch.argmax(logits, dim=1)
    acc = Accuracy(task="multiclass", num_classes=self.num_labels).to(self.device)
    test_acc = acc(preds, batch["labels"])

    self.log("test_loss", loss, on_epoch=True)
    self.log("test_acc", test_acc, on_epoch=True)
    return loss</pre>



<p>The test step follows the same pattern but uses dedicated metrics. This design keeps test evaluation separate from training and validation behaviors, which is useful when training and testing happen in different environments (e.g., offline batch scoring in production).</p>



<h3 class="wp-block-heading">Optimizer Configuration: How the Model Learns</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="16">def configure_optimizers(self):
    no_decay = ["bias", "LayerNorm.weight"]
    optimizer_grouped_parameters = [
        {
            "params": [p for n, p in self.named_parameters()
                       if not any(nd in n for nd in no_decay)],
            "weight_decay": self.weight_decay,
        },
        {
            "params": [p for n, p in self.named_parameters()
                       if any(nd in n for nd in no_decay)],
            "weight_decay": 0.0,
        },
    ]
    return torch.optim.AdamW(optimizer_grouped_parameters, lr=self.lr)</pre>



<p>This method defines how parameters update during training. Transformer models benefit from <code data-enlighter-language="python" class="EnlighterJSRAW">AdamW</code>, but they also require careful separation of weights that should and should not receive weight decay. Lightning lets you return the optimizer directly and handles everything else (e.g., multi-GPU synchronization, gradient scaling, and checkpointing). This keeps optimization logic concise while still following the best practices used in modern NLP training.</p>



<h3 class="wp-block-heading">Why This Optimizer Pattern Matters in MLOps</h3>



<p>This optimizer setup does more than just “train the model.”</p>



<p>By grouping parameters with and without weight decay, you ensure that every training run behaves consistently (regardless of hardware, environment, or number of GPUs). This is important for reproducibility because small optimizer differences can lead to diverging results in downstream evaluations.</p>



<p>Lightning also tracks the optimizer state inside checkpoints, which means that if training is resumed (perhaps in a new environment or after a failure), the run picks up with the exact same momentum buffers and learning dynamics. This is essential for building reliable training pipelines in real MLOps workflows.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Using-Hydra-Python-Configuration-Files-Reproducible-ML-Training"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Using-Hydra-Python-Configuration-Files-Reproducible-ML-Training">Using Hydra Python Configuration Files for Reproducible ML Training</a></h2>



<p>Hydra is the backbone of the training workflow in Lesson 1. Instead of hard-coding hyperparameters inside Python scripts, all settings for the model, data pipeline, and Trainer are stored cleanly inside YAML files. This keeps the codebase flexible, reproducible, and highly MLOps-friendly.</p>



<p>Hydra loads and composes these files automatically through the decorator in <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code>, and you can override any value at runtime from the command line.</p>



<p>Let’s walk through each configuration file used in Lesson 1.</p>



<h3 class="wp-block-heading">Root Configuration: configs/config.yaml</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="17">defaults:
  - model: distilbert
  - data: imdb
  - trainer: default
  - _self_

seed: 42</pre>



<p>This is the entry point Hydra reads first.</p>



<p>The defaults block instructs Hydra to compose the final configuration from:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">configs/model/distilbert.yaml</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">configs/data/imdb.yaml</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">configs/trainer/default.yaml</code></li>
</ul>



<p>The final line, <code data-enlighter-language="python" class="EnlighterJSRAW">seed: 42</code>, becomes <code data-enlighter-language="python" class="EnlighterJSRAW">cfg.seed</code> inside <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code>, and controls reproducibility through <code data-enlighter-language="python" class="EnlighterJSRAW">pl.seed_everything()</code>.</p>



<p>This root file defines <strong>what experiment you’re running</strong>, while the nested configs define <strong>how each component behaves</strong>.</p>



<h3 class="wp-block-heading">Model Configuration: configs/model/distilbert.yaml</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="18">model_name: distilbert-base-uncased
num_labels: 2
learning_rate: 2e-5
weight_decay: 0.01
dropout: 0.1</pre>



<p>These values are passed directly into:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="19">model = SentimentClassifier(**cfg.model)</pre>



<p>Meaning:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">model_name</code>: selects the Hugging Face backbone.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">num_labels</code>: controls the classification head.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">learning_rate</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">weight_decay</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">dropout</code>: feed into the optimizer and architecture.</li>
</ul>



<p>No values are hard-coded in Python, which is a major MLOps best practice that simplifies experiment tracking and hyperparameter sweeps.</p>



<h3 class="wp-block-heading">Data Configuration: configs/data/imdb.yaml</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="20">batch_size: 8
max_length: 128
num_workers: 4</pre>



<p>These values are injected directly into the DataModule:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="21">datamodule = SentimentDataModule(
    model_name=cfg.model.model_name,
    **cfg.data
)</pre>



<p>They control:</p>



<ul class="wp-block-list">
<li>how the data loader batches samples</li>



<li>how long each sequence can be after tokenization</li>



<li>how many CPU workers are used for preprocessing</li>
</ul>



<p>This separation keeps the data pipeline reusable across models and experiments.</p>



<h3 class="wp-block-heading">Trainer Configuration: configs/trainer/default.yaml</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="22">max_epochs: 3
devices: 1
accelerator: auto
precision: 32</pre>



<p>These settings define how Lightning’s Trainer behaves:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="23">trainer = pl.Trainer(**cfg.trainer, ...)</pre>



<p>The values specify:</p>



<ul class="wp-block-list">
<li>number of training epochs</li>



<li>how many devices (CPUs/GPUs) to use</li>



<li>mixed precision strategy (FP32 for Lesson 1)</li>



<li>automatic device selection (auto chooses CPU or GPU as available)</li>
</ul>



<p>In Lesson 2, this same configuration pattern will allow you to enable DDP, FSDP, and mixed precision with <strong>zero Python code changes</strong>, requiring only new YAML overrides.</p>



<h3 class="wp-block-heading">Why This Configuration System Is Critical for MLOps</h3>



<p>Hydra gives you:</p>



<ul class="wp-block-list">
<li>clean separation of concerns</li>



<li>reproducible experiments (config + code define a run)</li>



<li>easy command-line overrides</li>



<li>composable, hierarchical configurations</li>



<li>effortless scaling for future lessons (distributed training, mixed precision, exports)</li>
</ul>



<p>This structure is what makes the whole training pipeline “industrial-ready” instead of being a one-off script.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Building-PyTorch-Lightning-Training-Pipeline-train-py"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Building-PyTorch-Lightning-Training-Pipeline-train-py">Building a PyTorch Lightning Training Pipeline with train.py</a></h2>



<p>The training script is the glue that brings the entire pipeline together. Up to this point, you built a clean DataModule for handling data, a LightningModule for modeling logic, and a Hydra configuration system for reproducibility. Now, <code data-enlighter-language="python" class="EnlighterJSRAW">train.py</code> uses all three to create a fully configurable training workflow without requiring manual training loops.</p>



<p>This script intentionally contains <strong>no data processing, no model logic, and no hard-coded hyperparameters</strong>. Instead, everything is driven by Hydra configs and Lightning abstractions, making the training pipeline reproducible, maintainable, and MLOps-friendly.</p>



<p>Let us walk through each component.</p>



<h3 class="wp-block-heading">Hydra Configuration Loading</h3>



<p>The script begins with Hydra’s decorator:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="24">@hydra.main(config_path="../configs", config_name="config", version_base=None)
def main(cfg: DictConfig):</pre>



<p>This single line gives the entire training pipeline a powerful configuration system. Hydra automatically:</p>



<ul class="wp-block-list">
<li>loads all YAML files under <code data-enlighter-language="python" class="EnlighterJSRAW">configs/</code></li>



<li>composes them into a single cfg object</li>



<li>injects trainer, data, and model settings dynamically</li>



<li>allows overrides at runtime:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="25">python src/train.py trainer.max_epochs=5 model.lr=3e-5</pre>



<p>This configuration-first approach is a hallmark of modern MLOps workflows: the training script stays stable while configs change between runs.</p>



<h3 class="wp-block-heading">Ensuring Reproducibility</h3>



<p>Before building any components, you fix the training seed:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="26">pl.seed_everything(cfg.seed, workers=True)</pre>



<p>This sets random seeds across PyTorch, CUDA, NumPy, and Python itself. With <code data-enlighter-language="python" class="EnlighterJSRAW">workers=True</code>, Lightning synchronizes DataLoader workers as well.</p>



<p>In MLOps pipelines, deterministic behavior is essential for:</p>



<ul class="wp-block-list">
<li>debugging</li>



<li>regression testing</li>



<li>comparing experiments fairly</li>



<li>tracking drift in downstream metrics</li>
</ul>



<p>Seeding makes every run repeatable.</p>



<h3 class="wp-block-heading">Initializing the DataModule and Model</h3>



<p>Next, the script constructs the full data pipeline:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="27">datamodule = SentimentDataModule(
    model_name=cfg.model.model_name,
    **cfg.data
)</pre>



<p>Hydra provides the dataset name, batch size, sequence length, tokenizer model, and <code data-enlighter-language="python" class="EnlighterJSRAW">num_workers</code>.</p>



<p>Because all preprocessing logic is encapsulated inside the DataModule, the training script remains clean and focused.</p>



<p>Then the model is initialized:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="28">model = SentimentClassifier(**cfg.model)</pre>



<p>Every hyperparameter (e.g., learning rate, dropout, and number of labels) flows directly from the configuration. This separation between code and configuration makes the training pipeline:</p>



<ul class="wp-block-list">
<li>easier to maintain</li>



<li>safer to scale</li>



<li>more reproducible across teams and environments</li>
</ul>



<h3 class="wp-block-heading">Configuring Callbacks (Checkpointing + LR Monitoring)</h3>



<p>Callbacks enable essential training behaviors without cluttering the model code.</p>



<h4 class="wp-block-heading">ModelCheckpoint</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="29">checkpoint_callback = ModelCheckpoint(
    dirpath="checkpoints",
    filename="best-{epoch:02d}-{val_acc:.4f}",
    monitor="val_acc",
    mode="max",
    save_top_k=1,
    save_last=True,
    verbose=True
)</pre>



<p>This callback:</p>



<ul class="wp-block-list">
<li>saves the best model based on validation accuracy</li>



<li>keeps the last checkpoint for safety</li>



<li>stores everything in a structured directory (checkpoints/)</li>



<li>generates readable filenames</li>
</ul>



<p>This makes it trivial to reload the best model for evaluation or deployment.</p>



<h4 class="wp-block-heading">LearningRateMonitor</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="30">lr_monitor = LearningRateMonitor(logging_interval="step")</pre>



<p>This tracks how the learning rate changes during training. LR patterns are important indicators of:</p>



<ul class="wp-block-list">
<li>instability</li>



<li>vanishing gradients</li>



<li>misconfigured schedulers</li>



<li>early plateauing</li>
</ul>



<p>Both callbacks are added to the Trainer:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="31">callbacks = [checkpoint_callback, lr_monitor]</pre>



<h3 class="wp-block-heading">Logging with TensorBoard</h3>



<p>Lightning integrates cleanly with TensorBoard through:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="32">logger = TensorBoardLogger(
    save_dir="logs",
    name="sentiment_classifier"
)</pre>



<p>The logger records:</p>



<ul class="wp-block-list">
<li>loss curves</li>



<li>accuracy</li>



<li>learning rate</li>



<li>hyperparameters (optional)</li>
</ul>



<p>You can visualize results with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="33">tensorboard --logdir logs/</pre>



<p>In Lesson 2 and later modules, this logging layer leads naturally into MLflow, W&amp;B, or Langfuse for real MLOps observability.</p>



<h3 class="wp-block-heading">Constructing the Trainer</h3>



<p>Lightning’s Trainer consolidates all runtime behavior into one object:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="34">trainer = pl.Trainer(
    **cfg.trainer,
    callbacks=callbacks,
    logger=logger,
    enable_progress_bar=True
)</pre>



<p>Hydra injects:</p>



<ul class="wp-block-list">
<li>accelerator (cpu/gpu)</li>



<li>devices</li>



<li>precision</li>



<li>max epochs</li>



<li>gradient clipping</li>



<li>logging frequency</li>
</ul>



<p>This makes the script hardware-agnostic. In Lesson 2, you will enable DDP, FSDP, and mixed precision simply by changing the trainer config without modifying the Python code.</p>



<h3 class="wp-block-heading">Launching Training</h3>



<p>Finally, the entire pipeline is executed with:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="35">trainer.fit(model, datamodule)</pre>



<p>This one method call triggers:</p>



<ul class="wp-block-list">
<li>dataset downloading (<code data-enlighter-language="python" class="EnlighterJSRAW">prepare_data</code>)</li>



<li>tokenization (<code data-enlighter-language="python" class="EnlighterJSRAW">setup</code>)</li>



<li>data loader creation</li>



<li>training and validation loops</li>



<li>checkpointing</li>



<li>logging</li>
</ul>



<p>After training completes, the script prints useful summary information:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="36">best_checkpoint = checkpoint_callback.best_model_path
val_acc = trainer.callback_metrics.get("val_acc", 0)
val_loss = trainer.callback_metrics.get("val_loss", 0)</pre>



<p>This delivers the key metrics needed for evaluation or comparison with future runs.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-Model-Inference-PyTorch-Lightning-DistilBERT"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-Model-Inference-PyTorch-Lightning-DistilBERT">Running Model Inference with PyTorch Lightning and DistilBERT</a></h2>



<p>Once training is complete, the next step in any ML or MLOps workflow is deploying the model for inference. Lesson 1 focuses on offline inference (i.e., running predictions locally using the saved checkpoints). Your inference script is flexible and production-ready, supporting 4 different usage modes:</p>



<ul class="wp-block-list">
<li><strong>Single-text prediction</strong></li>



<li><strong>Batch prediction from a file</strong></li>



<li><strong>Interactive command-line mode</strong></li>



<li><strong>Demo mode with predefined reviews</strong></li>
</ul>



<p>This section walks through every component of the script and explains how it all fits together.</p>



<p>Here is the full header and imports:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="37">import argparse
from pathlib import Path
import torch
from transformers import AutoTokenizer
from model_module import SentimentClassifier</pre>



<p>The imports bring in <code data-enlighter-language="python" class="EnlighterJSRAW">argparse</code> for command-line parsing, torch for tensor and device management, <code data-enlighter-language="python" class="EnlighterJSRAW">AutoTokenizer</code> for text preprocessing, and your trained LightningModule class <code data-enlighter-language="python" class="EnlighterJSRAW">SentimentClassifier</code>. <code data-enlighter-language="python" class="EnlighterJSRAW">Path</code> is imported but not strictly required in this version; that’s fine and easy to reuse later.</p>



<h3 class="wp-block-heading">Loading the Trained Model</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="38">def load_model(checkpoint_path: str):
    """Load trained model from checkpoint."""
    print(f"Loading model from {checkpoint_path}...")
    # Load to CPU first to avoid device mismatch issues
    # Works for checkpoints saved on any device (CPU/CUDA/MPS/multi-GPU)
    model = SentimentClassifier.load_from_checkpoint(
        checkpoint_path,
        map_location="cpu"
    )
    model.eval()
    model.freeze()
    print("✅ Model loaded successfully!\n")
    return model</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">load_model()</code> encapsulates everything needed to restore the model from disk. It prints which checkpoint is being loaded for transparency, then uses Lightning’s <code data-enlighter-language="python" class="EnlighterJSRAW">load_from_checkpoint()</code> to rebuild the full <code data-enlighter-language="python" class="EnlighterJSRAW">SentimentClassifier</code> with weights and hyperparameters. </p>



<p>Setting <code data-enlighter-language="python" class="EnlighterJSRAW">map_location="cpu"</code> guarantees that the checkpoint can be loaded no matter where it was trained (single GPU, multi-GPU, CPU, MPS). Calling <code data-enlighter-language="python" class="EnlighterJSRAW">eval()</code> puts the model into inference mode, turning off dropout and other training-only behaviors, and <code data-enlighter-language="python" class="EnlighterJSRAW">freeze()</code> disables gradients so the model becomes a pure forward-pass computation graph. Finally, it prints a success message and returns the ready-to-use model.</p>



<h3 class="wp-block-heading">Predicting Sentiment for a Single Text</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="39">def predict_sentiment(model, tokenizer, text: str, device: str = "auto"):
    """Predict sentiment for a single text."""
    # Tokenize input
    encoding = tokenizer(
        text,
        max_length=128,
        padding="max_length",
        truncation=True,
        return_tensors="pt"
    )
    
    # Move to device
    if device == "auto":
        device = "cuda" if torch.cuda.is_available() else "cpu"
    
    input_ids = encoding["input_ids"].to(device)
    attention_mask = encoding["attention_mask"].to(device)
    model = model.to(device)
    
    # Get prediction
    with torch.no_grad():
        logits = model(input_ids, attention_mask)
        probs = torch.softmax(logits, dim=1)
        pred_class = torch.argmax(probs, dim=1).item()
        confidence = probs[0, pred_class].item()
    
    sentiment = "Positive 😊" if pred_class == 1 else "Negative 😞"
    
    return {
        "text": text,
        "sentiment": sentiment,
        "confidence": confidence,
        "label": pred_class
    }</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">predict_sentiment()</code> is the core prediction helper. It starts by tokenizing the input text using the same tokenizer settings as training: <code data-enlighter-language="python" class="EnlighterJSRAW">max_length=128</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">truncation=True</code>, and <code data-enlighter-language="python" class="EnlighterJSRAW">padding="max_length"</code>. This ensures the input tensor shape is fixed and consistent with what DistilBERT expects, which is also helpful later when exporting to ONNX/TorchScript.</p>



<p>The device argument defaults to <code data-enlighter-language="python" class="EnlighterJSRAW">"auto"</code>, which makes the function portable. If <code data-enlighter-language="python" class="EnlighterJSRAW">"auto"</code> is requested, it checks whether CUDA is available and chooses GPU when possible, otherwise falls back to CPU. Both the <code data-enlighter-language="python" class="EnlighterJSRAW">input_ids</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">attention_masktensors</code> are moved onto that device, and the model itself is also transferred using <code data-enlighter-language="python" class="EnlighterJSRAW">model.to(device)</code>. This guarantees that all tensors and the model live on the same device, avoiding runtime errors.</p>



<p>Inside a <code data-enlighter-language="python" class="EnlighterJSRAW">torch.no_grad()</code> block, the function performs a forward pass: it feeds <code data-enlighter-language="python" class="EnlighterJSRAW">input_ids</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">attention_mask</code> into the model, gets raw logits, converts them into probabilities with <code data-enlighter-language="python" class="EnlighterJSRAW">torch.softmax</code>, and then finds the predicted class with <code data-enlighter-language="python" class="EnlighterJSRAW">torch.argmax</code>. The <code data-enlighter-language="python" class="EnlighterJSRAW">.item()</code> calls convert these tiny tensors into regular Python scalars. The sentiment string maps class 1 to positive and everything else to negative, adding emoji for an immediately readable result. Finally, the function returns a dictionary containing the original text, the sentiment label, confidence score, and numeric class. This output is easy to print, log, or send through an API.</p>



<h3 class="wp-block-heading">Interactive Inference Mode</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="40">def interactive_mode(model, tokenizer):
    """Interactive inference mode."""
    print("=" * 70)
    print("Interactive Sentiment Analysis")
    print("=" * 70)
    print("\nType your movie review and press Enter.")
    print("Type 'quit' or 'exit' to stop.\n")
    
    while True:
        text = input(" Review: ").strip()
        
        if text.lower() in ['quit', 'exit', 'q']:
            print("\n Goodbye!")
            break
        
        if not text:
            continue
        
        result = predict_sentiment(model, tokenizer, text)
        
        print(f"\n{'='*70}")
        print(f"Sentiment: {result['sentiment']}")
        print(f"Confidence: {result['confidence']:.2%}")
        print(f"{'='*70}\n")</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">interactive_mode()</code> turns the script into a small command-line app. It prints a header and basic instructions, then enters an infinite loop. Each iteration reads a line from <code data-enlighter-language="python" class="EnlighterJSRAW">input()</code>, trims whitespace, and checks for exit commands (<code data-enlighter-language="python" class="EnlighterJSRAW">quit</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">exit</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">q</code>). Empty lines are ignored so the user doesn’t accidentally trigger computation.</p>



<p>For valid text, it calls <code data-enlighter-language="python" class="EnlighterJSRAW">predict_sentiment()</code> with the provided model and tokenizer. The results are printed in a nicely formatted block, showing the sentiment label and confidence percentage. This mode is perfect for quickly sanity-checking the model, exploring its behavior, and giving students an immediate feel for how the classifier responds to different reviews.</p>



<h3 class="wp-block-heading">Batch Inference from a Text File</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="41">def batch_inference(model, tokenizer, input_file: str):
    """Run inference on multiple texts from file."""
    print(f"Reading texts from {input_file}...")
    
    with open(input_file, 'r') as f:
        texts = [line.strip() for line in f if line.strip()]
    
    print(f"Running inference on {len(texts)} samples...\n")
    print("=" * 70)
    
    results = []
    for i, text in enumerate(texts, 1):
        result = predict_sentiment(model, tokenizer, text)
        results.append(result)
        
        print(f"\n{i}. {text[:60]}{'...' if len(text) > 60 else ''}")
        print(f"   → {result['sentiment']} ({result['confidence']:.2%})")
    
    print("\n" + "=" * 70)
    print(f"\n Processed {len(results)} samples")
    
    # Summary statistics
    positive = sum(1 for r in results if r['label'] == 1)
    negative = len(results) - positive
    avg_confidence = sum(r['confidence'] for r in results) / len(results)
    
    print(f"\n Summary:")
    print(f"   Positive: {positive} ({positive/len(results):.1%})")
    print(f"   Negative: {negative} ({negative/len(results):.1%})")
    print(f"   Avg Confidence: {avg_confidence:.2%}")</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">batch_inference()</code> is designed for real-world workflows where you want to score multiple reviews at once. It accepts a path to a file, reads all non-empty lines into a list, and reports how many samples it found. For each line, it calls <code data-enlighter-language="python" class="EnlighterJSRAW">predict_sentiment()</code> and prints out a truncated version of the text plus its predicted sentiment and confidence. Results are collected into a list so you can compute summary statistics afterward.</p>



<p>At the end, it computes how many predictions were positive vs negative and the average confidence across all samples. Those aggregate metrics are useful when running offline evaluations, batch scoring jobs, or quick experiments. This pattern also generalizes well to production pipelines where you might later replace the file input with a database, message queue, or data warehouse.</p>



<h3 class="wp-block-heading">Demo Mode with Sample Reviews</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="42">def demo_mode(model, tokenizer):
    """Run demo with sample reviews."""
    print("=" * 70)
    print("Demo: Sample Movie Reviews")
    print("=" * 70)
    
    sample_reviews = [
        "This movie was absolutely fantastic! Best film I've seen this year.",
        "Terrible movie, complete waste of time. Boring and poorly acted.",
        "The plot was confusing and the ending made no sense."
    ]
    
    print(f"\nTesting {len(sample_reviews)} sample reviews...\n")
    
    for i, text in enumerate(sample_reviews, 1):
        result = predict_sentiment(model, tokenizer, text)
        print(f"{i}. {text}")
        print(f"   → {result['sentiment']} ({result['confidence']:.2%})\n")
    
    print("=" * 70)</pre>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">demo_mode()</code> is a curated, zero-configuration way to showcase the model. It defines a fixed list of sample movie reviews that cover positive, negative, and neutral-ish cases. It prints a header, shows how many samples will be tested, and then iterates through each review, calling <code data-enlighter-language="python" class="EnlighterJSRAW">predict_sentiment()</code> under the hood. The output is a neat numbered list with the original sentence and the corresponding sentiment and confidence.</p>



<p>This is ideal for teaching, demos, and quick regression checks after changing code or dependencies. If the demo suddenly behaves strangely, you know something is wrong.</p>



<h3 class="wp-block-heading">Command-Line Interface and Mode Routing</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="43">def main():
    parser = argparse.ArgumentParser(description="Sentiment Classification Inference")
    parser.add_argument(
        "--checkpoint",
        type=str,
        required=True,
        help="Path to model checkpoint (e.g., checkpoints/best-*.ckpt)"
    )
    parser.add_argument(
        "--text",
        type=str,
        help="Single text to classify"
    )
    parser.add_argument(
        "--input-file",
        type=str,
        help="File with texts to classify (one per line)"
    )
    parser.add_argument(
        "--demo",
        action="store_true",
        help="Run demo with sample reviews"
    )
    parser.add_argument(
        "--device",
        type=str,
        default="auto",
        choices=["auto", "cpu", "cuda", "mps"],
        help="Device to run inference on"
    )
    
    args = parser.parse_args()
    
    # Load model
    model = load_model(args.checkpoint)
    
    # Load tokenizer
    print("Loading tokenizer...")
    tokenizer = AutoTokenizer.from_pretrained(model.hparams.model_name)
    print("Tokenizer loaded\n")
    
    # Run appropriate mode
    if args.demo:
        demo_mode(model, tokenizer)
    elif args.text:
        result = predict_sentiment(model, tokenizer, args.text, args.device)
        print("=" * 70)
        print(f"Text: {result['text']}")
        print(f"Sentiment: {result['sentiment']}")
        print(f"Confidence: {result['confidence']:.2%}")
        print("=" * 70)
    elif args.input_file:
        batch_inference(model, tokenizer, args.input_file)
    else:
        interactive_mode(model, tokenizer)


if __name__ == "__main__":
    main()</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">main()</code> function wires everything into a clean CLI interface. argparse defines all supported flags:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">--checkpoint</code> (required): tells the script which <code data-enlighter-language="python" class="EnlighterJSRAW">.ckpt</code> file to load</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--text</code>: enables single-text prediction</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--input-file</code>: enables batch inference from a file</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--demo</code>: triggers the curated demo mode</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--device</code>: lets you override the device choice (auto, cpu, cuda, mps)</li>
</ul>



<p>After parsing arguments, <code data-enlighter-language="python" class="EnlighterJSRAW">main()</code> calls <code data-enlighter-language="python" class="EnlighterJSRAW">load_model()</code> to restore the trained Lightning model. It then loads the tokenizer using <code data-enlighter-language="python" class="EnlighterJSRAW">AutoTokenizer.from_pretrained(model.hparams.model_name)</code>, which reads the model name stored in the checkpoint hyperparameters. This ensures inference uses the exact same tokenizer that training used.</p>



<p>Finally, it chooses which mode to run based on the arguments:</p>



<ul class="wp-block-list">
<li>If <code data-enlighter-language="python" class="EnlighterJSRAW">--demo</code> is set: run <code data-enlighter-language="python" class="EnlighterJSRAW">demo_mode()</code></li>



<li>Else if <code data-enlighter-language="python" class="EnlighterJSRAW">--text</code> is provided: run <code data-enlighter-language="python" class="EnlighterJSRAW">predict_sentiment()</code> once and print the result</li>



<li>Else if <code data-enlighter-language="python" class="EnlighterJSRAW">--input-file</code> is provided: run <code data-enlighter-language="python" class="EnlighterJSRAW">batch_inference()</code></li>



<li>Else: fall back to <code data-enlighter-language="python" class="EnlighterJSRAW">interactive_mode()</code></li>
</ul>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">if __name__ == "__main__":</code> guard makes this file executable as a script and keeps it import-safe if you ever reuse these helpers in a larger app or API.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Training-Evaluating-DistilBERT-Model-PyTorch-Lightning-Hydra"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Training-Evaluating-DistilBERT-Model-PyTorch-Lightning-Hydra">Training and Evaluating a DistilBERT Model with PyTorch Lightning and Hydra</a></h2>



<p>Now that we have built our DataModule, LightningModule, and training script, it is time to run everything end-to-end.</p>



<p>This section walks you through:</p>



<ul class="wp-block-list">
<li>launching training with Hydra</li>



<li>visualizing metrics in TensorBoard</li>



<li>running inference in multiple modes</li>



<li>understanding how Lightning automatically selects the best available device (CUDA, MPS, or CPU)</li>
</ul>



<p>To learn how to run and evaluate your sentiment classifier, just keep reading.</p>



<h3 class="wp-block-heading">Running Training from the Command Line</h3>



<p>You can start a basic training run using:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="44">$ python src/train.py</pre>



<p>Lightning will automatically download the IMDB dataset, initialize DistilBERT, set seeds, and begin training.
</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="45">$ python src/train.py
================================================================================
LESSON 1: PyTorch Lightning Fundamentals with Hydra
================================================================================
📋 Configuration:
model:
  model_name: distilbert-base-uncased
  num_labels: 2
  dropout: 0.1
  lr: 2.0e-05
  weight_decay: 0.01
data:
  dataset_name: imdb
  max_length: 128
  batch_size: 8
  num_workers: 4
trainer:
  max_epochs: 3
  accelerator: auto
  devices: 1
  precision: 32
  log_every_n_steps: 10
  check_val_every_n_epoch: 1
  deterministic: false
seed: 42
Seed set to 42
🌱 Seed set to: 42
   → Ensures reproducible results across runs
📊 Initializing DataModule...
   Dataset: imdb
   Batch size: 8
   Max sequence length: 128
🤖 Initializing Model...
   Model: distilbert-base-uncased
   Learning rate: 2e-05
   Number of labels: 2
config.json: 100%|██████████████████████████████| 483/483 [00:00&lt;00:00, 682kB/s]
model.safetensors:   0%|                             | 0.00/268M [00:00&lt;?, ?B/s]</pre>



<p>This screenshot typically includes:</p>



<ul class="wp-block-list">
<li>Hydra-composed config</li>



<li>“Seed set to …”</li>



<li>DataModule initialization logs</li>



<li>Model initialization</li>



<li>Trainer configuration summary</li>
</ul>



<p>It gives readers immediate confidence that the training pipeline is well-structured and reproducible.</p>



<h3 class="wp-block-heading">Overriding Hyperparameters with Hydra</h3>



<p>One of Hydra’s biggest strengths is that it allows you to change any configuration value without modifying a single line of code.</p>



<p>For example, you can increase the number of training epochs:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="46">$ python src/train.py trainer.max_epochs=5</pre>



<p>Or explore a different learning rate:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="47">$ python src/train.py model.lr=3e-5</pre>



<p>Or change the batch size:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="48">$ python src/train.py data.batch_size=16</pre>



<p>Hydra composes the full configuration at runtime and applies the CLI override (<code data-enlighter-language="python" class="EnlighterJSRAW">data.batch_size=16</code>). The screenshot shows the merged YAML, seed setup, DataModule initialization, model initialization, and callback configuration, all without changing a single line of code. This illustrates the core MLOps benefit of configuration-driven training.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-7.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="584" height="641" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54859"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7.png?size=126x138&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7-273x300.png?lossy=2&strip=1&webp=1 273w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7.png?size=378x415&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7.png?size=504x553&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-7.png?lossy=2&strip=1&webp=1 584w" sizes="(max-width: 584px) 100vw, 584px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> Hydra Override in Action</figcaption></figure></div>


<p>This is a major <strong>MLOps advantage</strong> because configuration-driven training allows experiments to be repeated, compared, and automated easily.</p>



<h3 class="wp-block-heading">Viewing Logs and Metrics in TensorBoard</h3>



<p>All training metrics are logged to:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="49">logs/sentiment_classifier/</pre>



<p>You can launch TensorBoard using:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="50">$ tensorboard --logdir logs/</pre>



<p>This screenshot shows how TensorBoard automatically tracks metrics (e.g., <code data-enlighter-language="python" class="EnlighterJSRAW">val_acc</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">val_loss</code>) for each Hydra-versioned training run. Because the entire pipeline is configuration-driven, you can compare experiments side-by-side by smoothing curves, inspecting run histories, and validating that model performance trends remain consistent across seeds or hyperparameter overrides.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-8-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="531" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-8-1024x531.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54860"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-8-1024x531.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-8-1024x531.png?size=126x65&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-8-1024x531.png?size=252x131&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-8-1024x531.png?size=378x196&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-8-1024x531.png?size=504x261&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-8-1024x531.png?size=630x327&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> TensorBoard Visualization of Validation Accuracy and Loss Across Multiple Runs</figcaption></figure></div>


<p>A typical figure here would show:</p>



<ul class="wp-block-list">
<li>Training vs. validation loss</li>



<li>Validation accuracy</li>



<li>Learning rate</li>



<li>Iteration timeline</li>
</ul>



<p>This screenshot reinforces that Lightning + TensorBoard gives you well-structured experiment tracking out of the box.</p>



<h3 class="wp-block-heading">Where Checkpoints Are Saved</h3>



<p>Lightning automatically saves your best checkpoint based on validation accuracy:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="51">checkpoints/best-epoch=XX-val_acc=YY.ckpt</pre>



<p>It also saves a <code data-enlighter-language="python" class="EnlighterJSRAW">last.ckpt</code> for safety.</p>



<p>Lightning’s ModelCheckpoint callback saves the best-performing model (based on <code data-enlighter-language="python" class="EnlighterJSRAW">val_acc</code>) and the final “<code data-enlighter-language="python" class="EnlighterJSRAW">last.ckpt</code>” snapshot for safety. Because training is fully configuration-driven, each Hydra-run creates its own versioned checkpoint set, making it easy to resume, compare, or deploy models in a reproducible MLOps workflow.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-9-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="115" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-9-1024x115.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54862"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-9-1024x115.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-9-1024x115.png?size=126x14&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-9-1024x115.png?size=252x28&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-9-1024x115.png?size=378x42&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-9-1024x115.png?size=504x57&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-9-1024x115.png?size=630x71&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> Checkpoints Generated Automatically by PyTorch Lightning</figcaption></figure></div>


<p>This is often the moment readers see how cleanly Lightning organizes artifacts, which ties directly into the next DVC module.</p>



<h3 class="wp-block-heading">Running Inference (Multiple Modes)</h3>



<p>Once training completes, you can use the inference script in 3 different modes.</p>



<h4 class="wp-block-heading">A) Single-Text Prediction</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="52">$ python src/inference.py --checkpoint "checkpoints/best-epoch=00-val_acc=0.8797.ckpt" --text "This movie was amazing!"</pre>



<p>The screenshot shows the model being loaded from the selected checkpoint, the tokenizer initialization, and the final prediction with confidence for the given input text.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-10-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="357" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-10-1024x357.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54865"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-10-1024x357.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-10-1024x357.png?size=126x44&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-10-1024x357.png?size=252x88&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-10-1024x357.png?size=378x132&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-10-1024x357.png?size=504x176&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-10-1024x357.png?size=630x220&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> Command-line inference using the trained Lightning model</figcaption></figure></div>


<p>Lightning auto-loads the checkpoint, loads the tokenizer, moves tensors to the correct device, and returns a structured prediction.</p>



<h4 class="wp-block-heading">B) Batch Inference from File</h4>



<p>Create a file such as the following:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="53">samples.txt
This movie was incredible.
The film was a disappointment.
I loved every character!</pre>



<p>Then run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="54">$ python src/inference.py \
    --checkpoint checkpoints/best-epoch=*.ckpt \
    --input-file samples.txt</pre>



<p>The screenshot shows the model loading, tokenizer initialization, per-review predictions with confidence scores, and a final summary of positive and negative counts. This demonstrates how the inference pipeline scales cleanly from single inputs to larger batches, which is a key requirement in practical MLOps workflows.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-11-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="939" height="1024" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11-939x1024.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54867"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11-939x1024.png?lossy=2&strip=1&webp=1 939w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11-939x1024.png?size=126x137&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11-939x1024.png?size=252x275&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11-939x1024.png?size=378x412&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11-939x1024.png?size=504x550&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-11-939x1024.png?size=630x687&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 939px) 100vw, 939px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> Batch inference on a text file using the trained sentiment classifier</figcaption></figure></div>


<p>The script also prints a small summary (positive %, negative %, average confidence).</p>



<h4 class="wp-block-heading">C) Interactive Mode</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="55">$ python src/inference.py \
    --checkpoint checkpoints/best-epoch=*.ckpt</pre>



<p>You can then type reviews manually.</p>



<p><strong>Figure 6</strong> shows the interactive prompt, a sample user review (“The movie was good — just not great.”), and the model’s corresponding prediction with confidence. This mode is ideal for quick testing and showcasing the end-to-end inference flow after training your Lightning model.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-12-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="642" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12-1024x642.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54870"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12-1024x642.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12-1024x642.png?size=126x79&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12-1024x642.png?size=252x158&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12-1024x642.png?size=378x237&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12-1024x642.png?size=504x316&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-12-1024x642.png?size=630x395&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> Real-time interactive sentiment analysis from the terminal</figcaption></figure></div>


<p>This mode is great for demos, lightweight testing, or API prototyping.</p>



<h3 class="wp-block-heading">Device Auto-Selection (CPU, GPU, and MPS)</h3>



<p>Your inference script includes this logic:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="56">if device == "auto":
    device = "cuda" if torch.cuda.is_available() else "cpu"</pre>



<p>When <code data-enlighter-language="python" class="EnlighterJSRAW">device</code> is set to <code data-enlighter-language="python" class="EnlighterJSRAW">"auto"</code>, the script selects a CUDA-enabled graphics processing unit (GPU) if one is available. Otherwise, it selects the central processing unit (CPU). On Macs with Apple Silicon, you can also choose Metal Performance Shaders (MPS) explicitly by setting the device to <code data-enlighter-language="python" class="EnlighterJSRAW">"mps"</code>.</p>



<p>Lightning also handles device selection on the training side:</p>



<ul class="wp-block-list">
<li>If a GPU is available and the trainer is configured to use it, Lightning uses the GPU.</li>



<li>If multiple GPUs are configured, Lightning uses DDP (Lesson 2).</li>



<li>If no supported GPU is available, Lightning falls back to the CPU.</li>
</ul>



<p><strong>Figure 7</strong> confirms that Lightning detected an MPS-capable GPU on macOS and will use it for training. It also reports that no Tensor Processing Unit (TPU) cores are available. This automation allows the training code to run across different hardware configurations without modification.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/08/image-13.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1022" height="408" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-13.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54873"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-13.png?size=126x50&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-13-300x120.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-13.png?size=378x151&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-13.png?size=504x201&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-13.png?size=630x252&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-13-768x307.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/08/image-13.png?lossy=2&strip=1&webp=1 1022w" sizes="(max-width: 1022px) 100vw, 1022px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> Trainer initialization showing automatic MPS GPU detection</figcaption></figure></div>


<p>This flexibility dramatically improves <strong>developer experience (DX)</strong> because your code can run on any machine.</p>



<h3 class="wp-block-heading">Summary: Why This Section Matters for MLOps</h3>



<p>This “Run and Evaluate” section ties the entire workflow together:</p>



<ul class="wp-block-list">
<li>Hydra makes experiments repeatable and configurable.</li>



<li>Lightning makes training structured and supports deterministic execution when configured appropriately.</li>



<li>TensorBoard provides observability.</li>



<li>Checkpoints preserve model state for downstream deployment.</li>



<li>The inference script demonstrates end-to-end functionality.</li>
</ul>



<p>This is precisely the level of workflow maturity expected in modern MLOps and LLMOps environments.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, you learned how PyTorch Lightning and Hydra work together to bring structure, reproducibility, and MLOps-friendly practices to a modern NLP training pipeline. Instead of writing long, error-prone training loops, Lightning allowed you to organize your project into clean, isolated components: the <code data-enlighter-language="python" class="EnlighterJSRAW">LightningDataModule</code> for handling all data operations and the <code data-enlighter-language="python" class="EnlighterJSRAW">LightningModule</code> for model logic, optimization, and metrics. Hydra completed the picture by giving you a powerful configuration system that let you change hyperparameters, batch sizes, model choices, or even hardware strategies from the command line without touching a single line of Python.</p>



<p>You trained a sentiment classifier on the IMDB dataset using a fully modular workflow, ran experiments with different overrides, visualized metrics in TensorBoard, and saved reproducible checkpoints automatically. Most importantly, you saw how clean architecture and configuration-driven design make your training pipeline easier to extend, debug, monitor, and deploy. These capabilities are exactly what real-world MLOps demands.</p>



<p>In the next lesson, we will take this foundation and push it into production territory. You will learn how to scale the same codebase with distributed training (DDP and FSDP), mixed precision, and model export to ONNX and TorchScript, all while keeping the project structure identical. If you want to turn this into a robust, deployment-ready training workflow, just keep reading.</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Singh, V</strong><strong>. </strong>“Training with PyTorch Lightning: Structured MLOps Development,” <em>PyImageSearch</em>, S. Huot, A. Sharma, and P. Thakur, eds., 2026, <a href="https://pyimg.co/5fe4l" target="_blank" rel="noreferrer noopener">https://pyimg.co/5fe4l</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Training with PyTorch Lightning: Structured MLOps Development" data-enlighter-group="57">@incollection{Singh_2026_training-w-pytorch-lightning-structured-mlops-development,
  author = {Vikram Singh},
  title = {{Training with PyTorch Lightning: Structured MLOps Development}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Aditya Sharma and Piyush Thakur},
  year = {2026},
  url = {https://pyimg.co/5fe4l},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/08/03/training-with-pytorch-lightning-structured-mlops-development/">Training with PyTorch Lightning: Structured MLOps Development</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Running Gemma 4 in the Browser with Transformers.js and WebGPU</title>
		<link>https://pyimagesearch.com/2026/07/27/running-gemma-4-in-the-browser-with-transformers-js-and-webgpu/</link>
		
		<dc:creator><![CDATA[Piyush Thakur]]></dc:creator>
		<pubDate>Mon, 27 Jul 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Google Gemma]]></category>
		<category><![CDATA[Hugging Face]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[ai deployment]]></category>
		<category><![CDATA[browser ai]]></category>
		<category><![CDATA[client-side ai]]></category>
		<category><![CDATA[gemma 4]]></category>
		<category><![CDATA[hugging face]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[local inference]]></category>
		<category><![CDATA[multimodal ai]]></category>
		<category><![CDATA[multimodal inference]]></category>
		<category><![CDATA[onnx]]></category>
		<category><![CDATA[onnx runtime web]]></category>
		<category><![CDATA[transformers.js]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[web llm]]></category>
		<category><![CDATA[webgpu]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=54773</guid>

					<description><![CDATA[<p>Table of Contents Running Gemma 4 in the Browser with Transformers.js and WebGPU Building a Browser-Based Gemma 4 AI Application Creating the HTML Document and Styling the Interface Creating the Application Layout Loading Transformers.js for Browser-Based Gemma 4 Inference Checking&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/07/27/running-gemma-4-in-the-browser-with-transformers-js-and-webgpu/">Running Gemma 4 in the Browser with Transformers.js and WebGPU</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" id="TOC"/>


<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<script src="https://fast.wistia.com/embed/medias/xpdhvg0nfc.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_xpdhvg0nfc seo=true videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/xpdhvg0nfc/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>



<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Running-Gemma-4-Browser-Transformers-js-WebGPU"><a rel="noopener" target="_blank" href="#h1-Running-Gemma-4-Browser-Transformers-js-WebGPU">Running Gemma 4 in the Browser with Transformers.js and WebGPU</a></li>

    <li id="TOC-h2-Building-Browser-Based-Gemma-4-AI-Application"><a rel="noopener" target="_blank" href="#h2-Building-Browser-Based-Gemma-4-AI-Application">Building a Browser-Based Gemma 4 AI Application</a></li>

    <li id="TOC-h2-Creating-HTML-Document-Styling-Interface"><a rel="noopener" target="_blank" href="#h2-Creating-HTML-Document-Styling-Interface">Creating the HTML Document and Styling the Interface</a></li>

    <li id="TOC-h2-Creating-Application-Layout"><a rel="noopener" target="_blank" href="#h2-Creating-Application-Layout">Creating the Application Layout</a></li>

    <li id="TOC-h2-Loading-Transformers-js-Browser-Based-Gemma-4-Inference"><a rel="noopener" target="_blank" href="#h2-Loading-Transformers-js-Browser-Based-Gemma-4-Inference">Loading Transformers.js for Browser-Based Gemma 4 Inference</a></li>

    <li id="TOC-h2-Checking-WebGPU-Support"><a rel="noopener" target="_blank" href="#h2-Checking-WebGPU-Support">Checking for WebGPU Support</a></li>

    <li id="TOC-h2-Referencing-User-Interface"><a rel="noopener" target="_blank" href="#h2-Referencing-User-Interface">Referencing the User Interface</a></li>

    <li id="TOC-h2-Creating-Helper-Functions"><a rel="noopener" target="_blank" href="#h2-Creating-Helper-Functions">Creating Helper Functions</a></li>

    <li id="TOC-h2-Previewing-Images-URL"><a rel="noopener" target="_blank" href="#h2-Previewing-Images-URL">Previewing Images from a URL</a></li>

    <li id="TOC-h2-Processing-Local-Image-Audio-Inputs-Gemma-4-Multimodal-AI"><a rel="noopener" target="_blank" href="#h2-Processing-Local-Image-Audio-Inputs-Gemma-4-Multimodal-AI">Processing Local Image and Audio Inputs for Gemma 4 Multimodal AI</a></li>

    <li id="TOC-h2-Processing-Audio-Inputs-Browser-Web-Audio-API"><a rel="noopener" target="_blank" href="#h2-Processing-Audio-Inputs-Browser-Web-Audio-API">Processing Audio Inputs in the Browser with Web Audio API</a></li>

    <li id="TOC-h2-Loading-Processor-Gemma-4-Model"><a rel="noopener" target="_blank" href="#h2-Loading-Processor-Gemma-4-Model">Loading the Processor and Gemma 4 Model</a></li>
    <ul>
        <li id="TOC-h3-Initializing-Gemma-4-Multimodal-Processor"><a rel="noopener" target="_blank" href="#h3-Initializing-Gemma-4-Multimodal-Processor">Initializing the Gemma 4 Multimodal Processor</a></li>
        <li id="TOC-h3-Loading-Gemma-4-ONNX-Model-WebGPU-Inference"><a rel="noopener" target="_blank" href="#h3-Loading-Gemma-4-ONNX-Model-WebGPU-Inference">Loading the Gemma 4 ONNX Model for WebGPU Inference</a></li>
        <li id="TOC-h3-Updating-Browser-Interface-Gemma-4-Model-Loading"><a rel="noopener" target="_blank" href="#h3-Updating-Browser-Interface-Gemma-4-Model-Loading">Updating the Browser Interface During Gemma 4 Model Loading</a></li>
    </ul>

    <li id="TOC-h2-Running-Gemma-4-Multimodal-Inference-Directly-Browser"><a rel="noopener" target="_blank" href="#h2-Running-Gemma-4-Multimodal-Inference-Directly-Browser">Running Gemma 4 Multimodal Inference Directly in the Browser</a></li>
    <ul>
        <li id="TOC-h3-Building-Prompt"><a rel="noopener" target="_blank" href="#h3-Building-Prompt">Building the Prompt</a></li>
        <li id="TOC-h3-Processing-Image-Audio-Inputs-Transformers-js"><a rel="noopener" target="_blank" href="#h3-Processing-Image-Audio-Inputs-Transformers-js">Processing Image and Audio Inputs with Transformers.js</a></li>
        <li id="TOC-h3-Generating-Response"><a rel="noopener" target="_blank" href="#h3-Generating-Response">Generating the Response</a></li>
    </ul>

    <li id="TOC-h2-Output"><a rel="noopener" target="_blank" href="#h2-Output">Output</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
    <ul>
        <li id="TOC-h3-Citation-Information"><a rel="noopener" target="_blank" href="#h3-Citation-Information">Citation Information</a></li>
    </ul>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Running-Gemma-4-Browser-Transformers-js-WebGPU"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Running-Gemma-4-Browser-Transformers-js-WebGPU">Running Gemma 4 in the Browser with Transformers.js and WebGPU</a></h2>



<p>In the first part of this series, we explored the architecture behind Gemma 4, including its multimodal design, Mixture-of-Experts routing, long-context capabilities, and the innovations that make it one of Google&#8217;s most capable open-weight model families.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured.png?lossy=2&strip=1&webp=1" alt="running-gemma-4-in-browser-transformers-js-webgpu-featured.png" class="wp-image-54793"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-in-browser-transformers-js-webgpu-featured.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p>In the second part, we put those capabilities into practice using Hugging Face Transformers, building multimodal applications that processed images, videos, audio, and structured outputs from a unified Python interface.</p>



<p>Next, we shifted our focus to deployment. In the previous tutorial, we explored multiple ways to run Gemma 4 locally using <strong>Ollama</strong>, <strong>llama.cpp</strong>, <strong>MLX</strong>, <strong>LM Studio</strong>, and <strong>Transformers.js</strong>. Along the way, we learned how to download models, perform local multimodal inference, and expose OpenAI-compatible APIs without relying on cloud-hosted services.</p>



<p>But there is one final step that takes local AI even further:</p>



<p>Can we run Gemma 4 entirely inside a web browser?</p>



<p>Until recently, running multimodal large language models required native applications, Python environments, or dedicated inference servers. Modern browser technologies, however, have changed that. With <strong>Transformers.js</strong>, ONNX Runtime, and hardware acceleration through <strong>WebGPU</strong> and <strong>WebAssembly</strong>, we can now execute state-of-the-art models directly inside modern web browsers without sending data to external servers.</p>



<p>Running models entirely in the browser offers several advantages. It improves user privacy by keeping data on the client device, reduces infrastructure costs by eliminating backend inference servers, enables offline AI experiences, and simplifies deployment by allowing applications to run anywhere a modern browser is available.</p>



<p>In this tutorial, we will build a fully client-side multimodal application using <strong>Transformers.js</strong>. We will learn how to load an ONNX version of Gemma 4 inside the browser, process image inputs, perform local inference using browser hardware acceleration, and build an interactive web application that runs entirely on the user&#8217;s machine.</p>



<p>By the end of this guide, you will understand how to deploy Gemma 4 as a browser-native AI application without Python, without a backend inference server, and without sacrificing the multimodal capabilities we&#8217;ve explored throughout this series.</p>



<p>This lesson is the 4th in a 5-part series on <strong>Google DeepMind&#8217;s Gemma 4</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/uqxzw" target="_blank" rel="noreferrer noopener">Google DeepMind’s Gemma 4: MoE, Efficiency Tricks, and Benchmarks</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/09dks" target="_blank" rel="noreferrer noopener">Building Multimodal AI Applications with Gemma 4 and Transformers</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/1rpad" target="_blank" rel="noreferrer noopener">Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/gx0fr" target="_blank" rel="noreferrer noopener">Running Gemma 4 in the Browser with Transformers.js and WebGPU</a></strong></em><strong> (this tutorial)</strong></li>



<li><em>Lesson 5</em></li>
</ol>



<p><strong>To learn how to </strong><strong>build fully client-side multimodal AI applications with Gemma 4</strong><strong>, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Building-Browser-Based-Gemma-4-AI-Application"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Building-Browser-Based-Gemma-4-AI-Application">Building a Browser-Based Gemma 4 AI Application</a></h2>



<p>Unlike the previous tutorial, where we interacted with Gemma 4 through the terminal, we will build a browser-based application that allows users to provide multimodal inputs and run inference directly from a web page.</p>



<p>The application consists of 3 primary components:</p>



<ul class="wp-block-list">
<li><strong>An input panel</strong> for entering image and audio URLs, uploading local files, and specifying the text prompt.</li>



<li><strong>A status panel</strong> that displays the current stage of the inference pipeline, download progress, and elapsed execution time.</li>



<li><strong>An output panel</strong> where Gemma 4 streams its response as it is generated.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Creating-HTML-Document-Styling-Interface"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Creating-HTML-Document-Styling-Interface">Creating the HTML Document and Styling the Interface</a></h2>



<p>We begin by creating a standard HTML5 document that serves as the foundation for our browser application.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="1">&lt;!DOCTYPE html>
&lt;html lang="en">
&lt;head>
&lt;meta charset="UTF-8"/>
&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"/>
&lt;title>Gemma 4 · WebGPU&lt;/title>
&lt;style>/* CSS styling */&lt;/style>
&lt;/head>
&lt;body>

&lt;header>
 &lt;h1>Gemma 4 · E2B&lt;/h1>
 &lt;span class="badge">WebGPU&lt;/span>
&lt;/header>
</pre>



<p>Here, <code data-enlighter-language="python" class="EnlighterJSRAW">&lt;!DOCTYPE html&gt;</code> declares an HTML5 document, while the <code data-enlighter-language="python" class="EnlighterJSRAW">&lt;html&gt;</code> element serves as the root of the page. The <code data-enlighter-language="python" class="EnlighterJSRAW">&lt;head&gt;</code> section contains metadata such as the character encoding, viewport configuration, page title, and CSS styles that define the application&#8217;s appearance. The <code data-enlighter-language="python" class="EnlighterJSRAW">&lt;body&gt;</code> section contains the visible content displayed in the browser, and the <code data-enlighter-language="python" class="EnlighterJSRAW">&lt;header&gt;</code> provides a simple title and badge identifying the application.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">&lt;style&gt;</code> section defines the application&#8217;s layout, typography, color palette, progress indicators, animations, and responsive behavior. Since the focus of this tutorial is browser-based AI inference rather than frontend development, we won&#8217;t discuss the styling in detail. Instead, we will concentrate on the JavaScript code that loads Gemma 4 and performs multimodal inference directly in the browser.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Creating-Application-Layout"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Creating-Application-Layout">Creating the Application Layout</a></h2>



<p>Next, we create the main layout of our browser application.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="html" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="15" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="2">&lt;div class="main">...&lt;/div>
</pre>



<p>The interface is divided into 3 primary sections that guide users through the entire inference workflow:</p>



<ul class="wp-block-list">
<li><strong>Input Panel:</strong> Allows users to provide an image and audio either by entering a URL or uploading local files. It also includes a text area for entering the prompt and a button for loading the model and starting inference.</li>



<li><strong>Status Panel:</strong> Displays the progress of the inference pipeline, including processor initialization, model loading, input preprocessing, token generation, and download progress.</li>



<li><strong>Output Panel:</strong> Displays the response generated by Gemma 4 as it is streamed to the browser in real time.</li>
</ul>



<p>This layout separates user inputs, model status, and generated outputs into dedicated sections, making the application easier to navigate while providing clear feedback throughout the inference process.</p>



<p>With the user interface in place, we can now implement the JavaScript code responsible for loading Gemma 4 and running multimodal inference directly inside the browser.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Loading-Transformers-js-Browser-Based-Gemma-4-Inference"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Loading-Transformers-js-Browser-Based-Gemma-4-Inference">Loading Transformers.js for Browser-Based Gemma 4 Inference</a></h2>



<p>With the user interface in place, we can begin implementing the browser-based inference pipeline. We start by importing the components required from <strong>Transformers.js</strong> and specifying the Gemma 4 model that will be loaded throughout the application.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="16" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="3">&lt;script type="module">
import {
 AutoProcessor,
 Gemma4ForConditionalGeneration,
 TextStreamer,
 load_image,
} from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4/dist/transformers.min.js";

const MODEL_ID = "onnx-community/gemma-4-E2B-it-ONNX";
</pre>



<p>Unlike the previous tutorial, where we installed Transformers.js locally using <strong>npm</strong>, the browser version imports the library directly from the <strong>jsDelivr CDN</strong>. This allows the application to download the required JavaScript modules automatically when the page loads, eliminating the need for a local Node.js environment or build process.</p>



<p>The imported classes serve the same purpose as before:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">AutoProcessor</code>: loads the preprocessing pipeline for Gemma 4, preparing text, images, and audio before they are passed to the model.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Gemma4ForConditionalGeneration</code>: loads the ONNX version of Gemma 4 and provides the API used for multimodal generation.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">TextStreamer</code>: streams generated tokens to the browser as they are produced, enabling real-time output instead of waiting for the entire response.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">load_image</code>: downloads and preprocesses images from either URLs or user-uploaded files.</li>
</ul>



<p>Finally, we define the <code data-enlighter-language="python" class="EnlighterJSRAW">MODEL_ID</code>, which points to the <strong>Gemma 4 E2B Instruct ONNX</strong> model hosted on the Hugging Face Hub. This is the same model used in the previous tutorial, ensuring consistent behavior across both the Node.js and browser implementations.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Checking-WebGPU-Support"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Checking-WebGPU-Support">Checking for WebGPU Support</a></h2>



<p>Before loading Gemma 4, we first verify that the user&#8217;s browser supports <strong>WebGPU</strong>. Unlike the Node.js implementation, which executes the model on the CPU, browser-based inference relies on WebGPU to accelerate model execution using the client&#8217;s GPU.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="25" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="4">// ── WebGPU check ──
if (!navigator.gpu) {
 document.getElementById("error-msg").textContent = "WebGPU is not supported in this browser. Please use Chrome 113+ or Edge 113+.";
 document.getElementById("error-msg").classList.add("visible");
 document.getElementById("run-btn").disabled = true;
 document.getElementById("run-btn").textContent = "WebGPU not available";
}
</pre>



<p>The browser exposes WebGPU support through the <code data-enlighter-language="python" class="EnlighterJSRAW">navigator.gpu</code> property. If this property is unavailable, the application assumes that the browser or hardware does not support GPU-accelerated inference.</p>



<p>Rather than allowing the application to continue and fail later when loading the model, we perform this check upfront. If WebGPU is unavailable, the application:</p>



<ul class="wp-block-list">
<li>displays an informative error message</li>



<li>disables the <strong>Run Inference</strong> button</li>



<li>prevents the user from attempting to load Gemma 4 on an unsupported browser</li>
</ul>



<p>This early validation provides a better user experience by clearly communicating the requirements for running the application.</p>



<p><em><strong>Note:</strong></em><em> At the time of writing, WebGPU is supported by modern Chromium-based browsers such as </em><em><strong>Google Chrome</strong></em><em> and </em><em><strong>Microsoft Edge</strong></em><em>. If your browser does not support WebGPU, consider updating to the latest version or enabling the appropriate experimental features.</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Referencing-User-Interface"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Referencing-User-Interface">Referencing the User Interface</a></h2>



<p>Next, we create references to the HTML elements that we will interact with throughout the application.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="32" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="5">// ── UI refs ──
const runBtn       = document.getElementById("run-btn");
const outputEl     = document.getElementById("output-text");
const errorEl      = document.getElementById("error-msg");
const progressWrap = document.getElementById("progress-wrap");
const progressFill = document.getElementById("progress-fill");
const progressPct  = document.getElementById("progress-pct");
const progressLbl  = document.getElementById("progress-label-text");
const elapsedEl    = document.getElementById("elapsed-text");
const imgPreview   = document.getElementById("img-preview");
const imgUrlInput  = document.getElementById("image-url");
</pre>



<p>These variables store references to the application&#8217;s user interface elements, including the <strong>Run Inference</strong> button, output panel, progress bar, status indicators, image preview, and input fields.</p>



<p>By retrieving these elements once at the beginning of the script, we can efficiently update the interface during model loading and inference without repeatedly querying the Document Object Model (DOM). This keeps the code cleaner, improves readability, and avoids unnecessary DOM lookups throughout the application.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Creating-Helper-Functions"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Creating-Helper-Functions">Creating Helper Functions</a></h2>



<p>Next, we define a few helper functions that simplify updating the application&#8217;s user interface during model loading and inference.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="43" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="6">// ── Step helpers ──
function setStep(n, state) { // state: 'active' | 'done' | ''
 const el = document.getElementById(`step-${n}`);
 el.className = "step" + (state ? ` ${state}` : "");
}
function setProgress(pct, label) {
 progressWrap.classList.add("visible");
 progressFill.style.width = `${pct}%`;
 progressPct.textContent = `${Math.round(pct)}%`;
 if (label) progressLbl.textContent = label;
}
function showError(msg) {
 errorEl.textContent = msg;
 errorEl.classList.add("visible");
}
function clearError() { errorEl.classList.remove("visible"); }
</pre>



<p>Each helper is responsible for a specific aspect of the interface:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">setStep()</code>: updates the status indicator for each stage of the inference pipeline, allowing users to see whether a step is currently running or has completed</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">setProgress()</code>: updates the model download progress bar and percentage value while the ONNX model is being downloaded and initialized</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">showError()</code>: displays error messages whenever model loading or inference fails</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">clearError()</code>: removes any previously displayed error messages before a new operation begins</li>
</ul>



<p>Although these functions are not directly involved in running Gemma 4, they provide real-time feedback throughout the application&#8217;s execution, making it easier for users to monitor model loading and inference progress.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Previewing-Images-URL"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Previewing-Images-URL">Previewing Images from a URL</a></h2>



<p>To provide immediate visual feedback, we update the image preview whenever the user enters a new image URL.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="59" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="7">// ── Image preview from URL ──
imgUrlInput.addEventListener("input", () => {
 const url = imgUrlInput.value.trim();
 if (url) { imgPreview.src = url; imgPreview.classList.add("visible"); }
 else imgPreview.classList.remove("visible");
});
// show default on load
imgPreview.src = imgUrlInput.value;
imgPreview.classList.add("visible");
</pre>



<p>This event listener monitors changes to the <strong>Image URL</strong> input field. Whenever the user enters a valid URL, the application updates the preview image so that the selected input can be verified before running inference. If the input is cleared, the preview is automatically hidden.</p>



<p>When the page first loads, the script also displays the default sample image specified in the input field, allowing users to run the application immediately without providing their own image.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Processing-Local-Image-Audio-Inputs-Gemma-4-Multimodal-AI"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Processing-Local-Image-Audio-Inputs-Gemma-4-Multimodal-AI">Processing Local Image and Audio Inputs for Gemma 4 Multimodal AI</a></h2>



<p>In addition to accepting image and audio URLs, our application also allows users to upload local files directly from their computer. This provides greater flexibility by supporting both online resources and locally stored media.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="68" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="8">// ── File upload handling ──
let uploadedImageData = null; // base64 data URL
let uploadedAudioBuffer = null; // Float32Array

document.getElementById("img-file").addEventListener("change", (e) => {
 const file = e.target.files[0];
 if (!file) return;
 const reader = new FileReader();
 reader.onload = (ev) => {
   uploadedImageData = ev.target.result;
   imgPreview.src = uploadedImageData;
   imgPreview.classList.add("visible");
   imgUrlInput.value = "";
 };
 reader.readAsDataURL(file);
});
</pre>



<p>When an image is uploaded, the browser reads the selected file using the <strong>FileReader API</strong> and converts it into a Base64-encoded data URL. The resulting image is stored in <code data-enlighter-language="python" class="EnlighterJSRAW">uploadedImageData</code>, displayed in the preview panel, and later passed to Transformers.js for inference.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="84" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="9">document.getElementById("audio-file").addEventListener("change", async (e) => {
 const file = e.target.files[0];
 if (!file) return;
 const arrayBuffer = await file.arrayBuffer();
 const audioCtx = new AudioContext({ sampleRate: 16000 });
 const decoded = await audioCtx.decodeAudioData(arrayBuffer);
 uploadedAudioBuffer = decoded.getChannelData(0);
 document.getElementById("audio-url").value = "";
});
</pre>



<p>For audio files, the preprocessing pipeline is slightly different.</p>



<p>Instead of using <code data-enlighter-language="python" class="EnlighterJSRAW">FileReader</code>, the browser loads the audio into an <code data-enlighter-language="python" class="EnlighterJSRAW">ArrayBuffer</code> and decodes it using the <strong>Web Audio API</strong> (<code data-enlighter-language="python" class="EnlighterJSRAW">AudioContext</code>). The decoded waveform is then extracted as a <code data-enlighter-language="python" class="EnlighterJSRAW">Float32Array</code> using <code data-enlighter-language="python" class="EnlighterJSRAW">getChannelData(0)</code> and stored in <code data-enlighter-language="python" class="EnlighterJSRAW">uploadedAudioBuffer</code>.</p>



<p>This preprocessing step converts the uploaded audio into the numerical waveform representation expected by Gemma 4. By handling image and audio uploads within the browser, users can perform multimodal inference on their own files without relying on external servers or additional preprocessing tools.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Processing-Audio-Inputs-Browser-Web-Audio-API"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Processing-Audio-Inputs-Browser-Web-Audio-API">Processing Audio Inputs in the Browser with Web Audio API</a></h2>



<p>Earlier, we implemented a helper function for loading audio in a Node.js environment. In the browser, however, we can leverage the <strong>Web Audio API</strong>, which provides native support for decoding and processing audio files.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="93" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="10">// ── Load audio via AudioContext (browser-native) ──
async function loadAudioBrowser(url) {
 const res = await fetch(url);
 const arrayBuffer = await res.arrayBuffer();
 const audioCtx = new AudioContext({ sampleRate: 16000 });
 const decoded = await audioCtx.decodeAudioData(arrayBuffer);
 return decoded.getChannelData(0); // Float32Array @ 16kHz
}
</pre>



<p>This helper first downloads the audio file using the Fetch API and stores it as an <code data-enlighter-language="python" class="EnlighterJSRAW">ArrayBuffer</code>. The browser&#8217;s <code data-enlighter-language="python" class="EnlighterJSRAW">AudioContext</code> then decodes the audio and resamples it to <strong>16 kHz</strong>, matching the sampling rate expected by Gemma 4. Finally, <code data-enlighter-language="python" class="EnlighterJSRAW">getChannelData(0)</code> extracts the waveform as a <code data-enlighter-language="python" class="EnlighterJSRAW">Float32Array</code>, which is passed directly to the processor during inference.</p>



<p>Using the browser&#8217;s native audio processing capabilities eliminates the need for external libraries while providing the model with audio in the format required for multimodal inference.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Loading-Processor-Gemma-4-Model"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Loading-Processor-Gemma-4-Model">Loading the Processor and Gemma 4 Model</a></h2>



<p>With the user interface and helper functions in place, we can now load the processor and Gemma 4 model. Since downloading and initializing a large multimodal model can take some time, the application also displays a progress bar and status updates throughout the loading process.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="11">// State
let processor, model;
let modelLoaded = false;

async function loadModel() {
    ...
}
</pre>



<p>We begin by defining three variables that manage the model&#8217;s state. The <code data-enlighter-language="python" class="EnlighterJSRAW">processor</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">model</code> variables store the initialized processor and Gemma 4 model, while the <code data-enlighter-language="python" class="EnlighterJSRAW">modelLoaded</code> flag tracks whether the model has already been loaded. This prevents the application from downloading and initializing the model multiple times during a single browser session.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">loadModel()</code> function is responsible for downloading, initializing, and preparing Gemma 4 for inference.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Initializing-Gemma-4-Multimodal-Processor"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Initializing-Gemma-4-Multimodal-Processor">Initializing the Gemma 4 Multimodal Processor</a></h3>



<p>The first step is loading the processor.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="12">setStep(1, "active");

processor = await AutoProcessor.from_pretrained(MODEL_ID);

setStep(1, "done");
</pre>



<p>As in the previous tutorial, <code data-enlighter-language="python" class="EnlighterJSRAW">AutoProcessor.from_pretrained()</code> downloads the preprocessing pipeline associated with Gemma 4. The processor prepares text, images, and audio before they are passed to the model, ensuring that all three modalities are converted into the format expected during inference.</p>



<p>The status panel is updated before and after loading so users can track the application&#8217;s progress.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Loading-Gemma-4-ONNX-Model-WebGPU-Inference"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Loading-Gemma-4-ONNX-Model-WebGPU-Inference">Loading the Gemma 4 ONNX Model for WebGPU Inference</a></h3>



<p>Once the processor is ready, we initialize the Gemma 4 model.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="13">model = await Gemma4ForConditionalGeneration.from_pretrained(
    MODEL_ID,
    {
        dtype: "q4f16",
        device: "webgpu",
        progress_callback: (info) => {
            ...
        },
    }
);
</pre>



<p>Here, <code data-enlighter-language="python" class="EnlighterJSRAW">Gemma4ForConditionalGeneration.from_pretrained()</code> downloads the ONNX model and initializes it for browser-based inference.</p>



<p>Several important parameters are specified:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">dtype: "q4f16"</code>: loads a <strong>4-bit </strong><strong>floating-point </strong><strong>quantized</strong> version of Gemma 4, reducing memory consumption while maintaining strong inference performance.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">device: "webgpu"</code>: instructs Transformers.js to execute the model using the browser&#8217;s <strong>WebGPU</strong> backend. Instead of performing inference on the CPU, computations are offloaded to the client&#8217;s GPU, resulting in significantly faster execution on supported hardware.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">progress_callback</code>: receives download progress updates as the model files are fetched from the Hugging Face Hub. We use these updates to drive the application&#8217;s progress bar, allowing users to monitor the download in real time.</li>
</ul>



<p>Since Gemma 4 consists of multiple ONNX weight files, the first execution may take several minutes depending on the user&#8217;s internet connection. Once downloaded, however, the browser caches these files, allowing subsequent executions to start much more quickly.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Updating-Browser-Interface-Gemma-4-Model-Loading"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Updating-Browser-Interface-Gemma-4-Model-Loading">Updating the Browser Interface During Gemma 4 Model Loading</a></h3>



<p>While the model is loading, the application continuously updates the elapsed time and download progress, giving users immediate feedback throughout the initialization process.</p>



<p>Once the download completes successfully, the progress indicator is updated, the <strong>Run Inference</strong> button is enabled, and the <code data-enlighter-language="python" class="EnlighterJSRAW">modelLoaded</code> flag is set to <code data-enlighter-language="python" class="EnlighterJSRAW">true</code>, indicating that Gemma 4 is ready to process user inputs.</p>



<p>If any step fails (e.g., due to a network issue or an unsupported browser), the exception is caught, an informative error message is displayed, and the application is reset so that the user can safely retry loading the model.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-Gemma-4-Multimodal-Inference-Directly-Browser"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-Gemma-4-Multimodal-Inference-Directly-Browser">Running Gemma 4 Multimodal Inference Directly in the Browser</a></h2>



<p>With the processor and model loaded, we are ready to perform multimodal inference. The application waits for the user to click the <strong>Run Inference</strong> button before preparing the inputs, generating a response, and streaming the output back to the browser.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="14">// Run inference
runBtn.addEventListener("click", async () => {
    ...
});
</pre>



<p>The event listener serves as the entry point for the inference pipeline. Before performing any computation, it first checks whether Gemma 4 has already been loaded. If the model has not been initialized, the application automatically downloads and loads it. Otherwise, it immediately begins processing the user&#8217;s inputs.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Building-Prompt"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Building-Prompt">Building the Prompt</a></h3>



<p>The first stage constructs the multimodal conversation that will be passed to Gemma 4.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="15">const prompt_text = document.getElementById("prompt-input").value.trim();

const messages = [{
    role: "user",
    content: [
        { type: "image" },
        { type: "audio" },
        { type: "text", text: prompt_text },
    ],
}];

const prompt = processor.apply_chat_template(
    messages,
    {
        enable_thinking: false,
        add_generation_prompt: true,
    }
);
</pre>



<p>As in the previous tutorial, we represent the input as a chat conversation. The user message contains placeholders for an image and an audio clip, along with the text instruction entered through the interface.</p>



<p>The conversation is then passed to <code data-enlighter-language="python" class="EnlighterJSRAW">apply_chat_template()</code>, which formats the prompt according to Gemma 4&#8217;s expected conversational template. Setting <code data-enlighter-language="python" class="EnlighterJSRAW">enable_thinking</code> to <code data-enlighter-language="python" class="EnlighterJSRAW">false</code> disables thinking mode, while <code data-enlighter-language="python" class="EnlighterJSRAW">add_generation_prompt</code> appends the appropriate generation token, indicating where the model should begin producing its response.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Processing-Image-Audio-Inputs-Transformers-js"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Processing-Image-Audio-Inputs-Transformers-js">Processing Image and Audio Inputs with Transformers.js</a></h3>



<p>Next, the application prepares the multimodal inputs.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="16">// Load image
...

// Load audio
...

const inputs = await processor(
    prompt,
    image,
    audio,
    {
        add_special_tokens: false,
    }
);
</pre>



<p>The application supports both <strong>URLs</strong> and <strong>locally uploaded files</strong>. If the user has uploaded an image or audio clip, those inputs are used directly. Otherwise, the application downloads the resources from the URLs provided in the input fields.</p>



<p>Once the image, audio, and prompt are available, the processor converts them into the tensor representations expected by Gemma 4. This preprocessing step combines all three modalities into a unified set of model inputs, allowing the model to reason over text, images, and audio simultaneously.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Generating-Response"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Generating-Response">Generating the Response</a></h3>



<p>With the inputs prepared, we can finally generate a response.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="17">const outputs = await model.generate({
    ...inputs,
    max_new_tokens: 512,
    do_sample: false,
    streamer: new TextStreamer(
        processor.tokenizer,
        {
            skip_prompt: true,
            skip_special_tokens: true,
            callback_function: (text) => {
                ...
            },
        }
    ),
});
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">generate()</code> method performs autoregressive text generation using the processed multimodal inputs.</p>



<p>Several parameters control the generation process:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">max_new_tokens: 512</code>: limits the maximum number of tokens that Gemma 4 can generate.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">do_sample: false</code>: disables stochastic sampling, producing deterministic responses for identical inputs.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">TextStreamer</code>: streams generated tokens to the browser as soon as they are produced instead of waiting for the entire response to complete.</li>
</ul>



<p>Inside the streamer&#8217;s callback function, each newly generated token is appended to the output panel, allowing users to watch the response appear in real time. This creates a significantly more interactive experience, particularly when generating longer responses.</p>



<p>Once generation completes, the application removes the animated cursor, updates the elapsed execution time, reports the total number of generated tokens, and re-enables the <strong>Run Inference</strong> button for the next request.</p>



<p>If an error occurs at any point during preprocessing or generation, the exception is caught, an informative error message is displayed, and the application is reset so that inference can be attempted again.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Output"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Output">Output</a></h2>



<p>After opening the HTML file in a WebGPU-enabled browser, you will see an interface similar to the one shown in <strong>Figure 1</strong>. The left panel allows you to provide an image and audio either by entering their URLs or by uploading local files. You can also customize the prompt that will be sent to Gemma 4.</p>



<figure style="text-align: center; max-width: 700px; margin: auto;">
<iframe width="700" height="267" src="https://www.youtube.com/embed/oDmgXzo7-uk" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  <figcaption style="align: center; margin-top: 8px;">
    <strong>Figure 1:</strong> Browser-Based Gemma 4 Application with Transformers.js and WebGPU (source: author).
  </figcaption>
</figure>



<p>Click <strong>Load Model</strong> to download the ONNX version of Gemma 4 into the browser. During the first run, the application displays the download progress along with the status of each stage in the inference pipeline. Once the model has been loaded, the button changes to <strong>Run Inference</strong>.</p>



<p>Clicking <strong>Run Inference</strong> processes the image, audio, and prompt entirely within the browser using WebGPU acceleration. As Gemma 4 generates its response, the output is streamed token by token into the output panel, allowing you to see the answer appear in real time rather than waiting for the entire generation to finish.</p>



<p>The first execution may take a few minutes because the model weights must be downloaded and initialized. Subsequent runs are significantly faster since the model is already cached by the browser, requiring only the preprocessing and inference steps to complete.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this tutorial, we explored how to run <strong>Gemma 4 entirely inside a web browser</strong> using <strong>Transformers.js</strong> and <strong>WebGPU</strong>, without relying on Python, Node.js, or any external inference server. Starting from a simple HTML page, we built a complete multimodal application capable of loading an ONNX version of Gemma 4, accepting image and audio inputs, tracking model download progress, and streaming responses directly in the browser.</p>



<p>Along the way, we learned how to initialize the model with <code data-enlighter-language="python" class="EnlighterJSRAW">AutoProcessor</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">Gemma4ForConditionalGeneration</code>, verify WebGPU support, preprocess multimodal inputs, construct prompts using the chat template, and perform real-time token streaming with <code data-enlighter-language="python" class="EnlighterJSRAW">TextStreamer</code>. By leveraging ONNX Runtime Web and the browser&#8217;s GPU, we were able to execute the entire inference pipeline locally while keeping all data on the user&#8217;s device.</p>



<p>With this tutorial, we have now covered the complete Gemma 4 deployment landscape, from Python applications using Hugging Face Transformers to local inference with <strong>Ollama</strong>, <strong>llama.cpp</strong>, <strong>MLX</strong>, <strong>LM Studio</strong>, <strong>Transformers.js for Node.js</strong>, and finally <strong>Transformers.js running directly in the browser</strong>. Together, these tutorials demonstrate how the same Gemma 4 model can be deployed across a wide range of environments, from research notebooks and desktop applications to fully client-side web experiences.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h3-Citation-Information"/>



<h3 class="wp-block-heading"><a href="#TOC-h3-Citation-Information">Citation Information</a></h3>



<p><strong>Thakur, P</strong><strong>. </strong>“Running Gemma 4 in the Browser with Transformers.js and WebGPU,” <em>PyImageSearch</em>, S. Huot, G. Kudriavtsev, and A. Sharma, eds., 2026, <a href="https://pyimg.co/gx0fr" target="_blank" rel="noreferrer noopener">https://pyimg.co/gx0fr</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 in the Browser with Transformers.js and WebGPU" data-enlighter-group="18">@incollection{Thakur_2026_running-gemma-4-in-browser-transformers-js-webgpu,
  author = {Piyush Thakur},
  title = {{Running Gemma 4 in the Browser with Transformers.js and WebGPU}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Georgii Kudriavtsev and Aditya Sharma},
  year = {2026},
  url = {https://pyimg.co/gx0fr},
}
</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/07/27/running-gemma-4-in-the-browser-with-transformers-js-and-webgpu/">Running Gemma 4 in the Browser with Transformers.js and WebGPU</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More</title>
		<link>https://pyimagesearch.com/2026/07/20/running-gemma-4-locally-ollama-llama-cpp-mlx-and-more/</link>
		
		<dc:creator><![CDATA[Piyush Thakur]]></dc:creator>
		<pubDate>Mon, 20 Jul 2026 12:45:00 +0000</pubDate>
				<category><![CDATA[Artificial Intelligence]]></category>
		<category><![CDATA[Deployment]]></category>
		<category><![CDATA[Google Gemma]]></category>
		<category><![CDATA[Large Language Models]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[ai deployment]]></category>
		<category><![CDATA[apple silicon]]></category>
		<category><![CDATA[gemma 4]]></category>
		<category><![CDATA[gguf]]></category>
		<category><![CDATA[google gemma]]></category>
		<category><![CDATA[llama.cpp]]></category>
		<category><![CDATA[lm studio]]></category>
		<category><![CDATA[local inference]]></category>
		<category><![CDATA[local llm]]></category>
		<category><![CDATA[mlx]]></category>
		<category><![CDATA[mlx-vlm]]></category>
		<category><![CDATA[multimodal ai]]></category>
		<category><![CDATA[ollama]]></category>
		<category><![CDATA[on-device ai]]></category>
		<category><![CDATA[openai compatible api]]></category>
		<category><![CDATA[transformers.js]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[vision language model]]></category>
		<guid isPermaLink="false">https://pyimagesearch.com/?p=54541</guid>

					<description><![CDATA[<p>Table of Contents Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More Running Gemma 4 with Ollama for Local AI Inference Running Gemma 4 with llama.cpp and GGUF Models Running Gemma 4 with MLX on Apple Silicon Macs Running Gemma&#8230;</p>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/07/20/running-gemma-4-locally-ollama-llama-cpp-mlx-and-more/">Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="yoast-breadcrumbs"><span><span><a href="https://pyimagesearch.com/">Home</a></span></div>


<script src="https://fast.wistia.com/embed/medias/srgcawrsso.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_srgcawrsso seo=true videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/srgcawrsso/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>



<div class="toc">
<hr class="TOC"/>
<p class="has-large-font-size"><strong>Table of Contents</strong></p>
<ul>
    <li id="TOC-h1-Running-Gemma-4-Locally-Ollama-llama-cpp-MLX-More"><a rel="noopener" target="_blank" href="#h1-Running-Gemma-4-Locally-Ollama-llama-cpp-MLX-More">Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More</a></li>

    <li id="TOC-h2-Running-Gemma-4-Ollama-Local-AI-Inference"><a rel="noopener" target="_blank" href="#h2-Running-Gemma-4-Ollama-Local-AI-Inference">Running Gemma 4 with Ollama for Local AI Inference</a></li>

    <li id="TOC-h2-Running-Gemma-4-llama-cpp-GGUF-Models"><a rel="noopener" target="_blank" href="#h2-Running-Gemma-4-llama-cpp-GGUF-Models">Running Gemma 4 with llama.cpp and GGUF Models</a></li>

    <li id="TOC-h2-Running-Gemma-4-MLX-Apple-Silicon-Macs"><a rel="noopener" target="_blank" href="#h2-Running-Gemma-4-MLX-Apple-Silicon-Macs">Running Gemma 4 with MLX on Apple Silicon Macs</a></li>

    <li id="TOC-h2-Running-Gemma-4-LM-Studio-Desktop-App"><a rel="noopener" target="_blank" href="#h2-Running-Gemma-4-LM-Studio-Desktop-App">Running Gemma 4 with LM Studio&#8217;s Desktop App</a></li>

    <li id="TOC-h2-Running-Gemma-4-Locally-Transformers-js"><a rel="noopener" target="_blank" href="#h2-Running-Gemma-4-Locally-Transformers-js">Running Gemma 4 Locally with Transformers.js</a></li>

    <li id="TOC-h2-Choosing-Right-Runtime-Running-Gemma-4-Locally"><a rel="noopener" target="_blank" href="#h2-Choosing-Right-Runtime-Running-Gemma-4-Locally">Choosing the Right Runtime for Running Gemma 4 Locally</a></li>

    <li id="TOC-h2-Summary"><a rel="noopener" target="_blank" href="#h2-Summary">Summary</a></li>
</ul>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h1-Running-Gemma-4-Locally-Ollama-llama-cpp-MLX-More"/>



<h2 class="wp-block-heading"><a href="#TOC-h1-Running-Gemma-4-Locally-Ollama-llama-cpp-MLX-More">Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More</a></h2>



<p>In the first part of this series, we explored the architecture behind Gemma 4, including its multimodal design, long-context capabilities, Mixture-of-Experts routing, Per-Layer Embeddings, and the engineering decisions that allow the model family to scale from lightweight on-device deployments to large production systems. We also examined the different Gemma 4 variants, their hardware requirements, and the reasoning capabilities that make the model family one of the most compelling open-weight releases available today.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="940" height="780" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54649"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1.png?size=126x105&lossy=2&strip=1&webp=1 126w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1-300x249.png?lossy=2&strip=1&webp=1 300w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1.png?size=378x314&lossy=2&strip=1&webp=1 378w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1.png?size=504x418&lossy=2&strip=1&webp=1 504w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1.png?size=630x523&lossy=2&strip=1&webp=1 630w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1-768x637.png?lossy=2&strip=1&webp=1 768w, https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/running-gemma-4-locally-ollama-llama-cpp-mlx-featured-1.png?lossy=2&strip=1&webp=1 940w" sizes="(max-width: 940px) 100vw, 940px" /></a></figure></div>


<p>In the second part, we moved from theory to practice. Using <code data-enlighter-language="python" class="EnlighterJSRAW">Hugging Face Transformers</code>, we built multimodal applications capable of processing images, videos, audio, and structured outputs. Along the way, we saw how Gemma 4 handles tasks such as screenshot-to-code generation, object detection, image captioning, multimodal function calling, and audio understanding from a single unified interface.</p>



<p>But there is still an important question left unanswered:</p>



<p><strong>How do we actually run Gemma 4 outside of a Python notebook?</strong></p>



<p>For many developers, deploying and interacting with a model is just as important as understanding its architecture or capabilities. A model that performs well inside a notebook is useful, but a model that can run locally on a laptop, power desktop applications, expose APIs, or operate entirely offline opens up an entirely different set of possibilities.</p>



<p>Fortunately, Gemma 4 has quickly gained support across the local AI ecosystem. Whether you prefer a simple one-command setup, a graphical desktop interface, a highly optimized C++ inference engine, a runtime specifically designed for Apple Silicon, or even JavaScript-based local inference, there is already a mature deployment path available.</p>



<p>In this lesson, we will explore five of the most popular ways to run Gemma 4 locally:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code>: fast setup and developer-friendly APIs</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code>: a graphical desktop experience</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code>: efficient GGUF (GPT-Generated Unified Format)-based inference and maximum control</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code>: optimized performance on Apple Silicon devices</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>: running Gemma 4 directly from <code data-enlighter-language="python" class="EnlighterJSRAW">Node.js</code> using ONNX models</li>
</ul>



<p>By the end of this guide, you will know how to download Gemma 4, perform local text and multimodal inference, expose OpenAI-compatible APIs, and choose the deployment runtime that best fits your workflow.</p>



<p>In the next lesson, we will build upon the <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> foundation introduced here and take things one step further by running Gemma 4 <strong>entirely inside the browser</strong>, enabling fully client-side AI applications without requiring a Python backend or native inference runtime.</p>



<p>This lesson is the 3rd in a 5-part series on <strong>Google DeepMind&#8217;s Gemma 4</strong>:</p>



<ol class="wp-block-list">
<li><em><strong><a href="https://pyimg.co/uqxzw" target="_blank" rel="noreferrer noopener">Google DeepMind’s Gemma 4: MoE, Efficiency Tricks, and Benchmarks</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/09dks" target="_blank" rel="noreferrer noopener">Building Multimodal AI Applications with Gemma 4 and Transformers</a></strong></em></li>



<li><em><strong><a href="https://pyimg.co/1rpad" target="_blank" rel="noreferrer noopener">Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More</a></strong></em><strong> (this tutorial)</strong></li>



<li><em>Lesson 4</em></li>



<li><em>Lesson 5</em></li>
</ol>



<p><strong>To learn how to </strong><strong>run Gemma 4 entirely on your own hardware</strong><strong>, </strong><em><strong>just keep reading.</strong></em></p>



<div id="pyi-source-code-block" class="source-code-wrap"><div class="gpd-source-code">
    <div class="gpd-source-code-content">
        <img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/source-code-icon.png?lossy=2&strip=1&webp=1" alt="">
        <h4>Looking for the source code to this post?</h4>
                    <a href="#download-the-code" class="pyis-cta-modal-open-modal">Jump Right To The Downloads Section <svg class="svg-icon arrow-right" width="12" height="12" aria-hidden="true" role="img" focusable="false" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.8125 0.1875C6.875 0.125 6.96875 0.09375 7.09375 0.09375C7.1875 0.09375 7.28125 0.125 7.34375 0.1875L13.875 6.75C13.9375 6.8125 14 6.90625 14 7C14 7.125 13.9375 7.1875 13.875 7.25L7.34375 13.8125C7.28125 13.875 7.1875 13.9062 7.09375 13.9062C6.96875 13.9062 6.875 13.875 6.8125 13.8125L6.1875 13.1875C6.125 13.125 6.09375 13.0625 6.09375 12.9375C6.09375 12.8438 6.125 12.75 6.1875 12.6562L11.0312 7.8125H0.375C0.25 7.8125 0.15625 7.78125 0.09375 7.71875C0.03125 7.65625 0 7.5625 0 7.4375V6.5625C0 6.46875 0.03125 6.375 0.09375 6.3125C0.15625 6.25 0.25 6.1875 0.375 6.1875H11.0312L6.1875 1.34375C6.125 1.28125 6.09375 1.1875 6.09375 1.0625C6.09375 0.96875 6.125 0.875 6.1875 0.8125L6.8125 0.1875Z" fill="#169FE6"></path></svg></a>
            </div>
</div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-Gemma-4-Ollama-Local-AI-Inference"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-Gemma-4-Ollama-Local-AI-Inference">Running Gemma 4 with Ollama for Local AI Inference</a></h2>



<p>Among all the local inference solutions available today, <strong><a href="https://ollama.com" target="_blank" rel="noreferrer noopener">Ollama</a></strong> is arguably the fastest and easiest way to get started with Gemma 4. With just a few terminal commands, it can download the model, manage different versions, launch a local inference server, and expose an OpenAI-compatible API (Application Programming Interface), all without requiring any manual configuration.</p>



<p>Instead of downloading model weights, setting up inference backends, or configuring server endpoints yourself, <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> automates the entire deployment process behind a simple command-line interface. This makes it an excellent choice for developers who want to start experimenting with Gemma 4 immediately while still having access to production-friendly APIs.</p>



<p>In this section, we will install <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code>, download the <code data-enlighter-language="python" class="EnlighterJSRAW">Gemma 4</code> model, perform both text and vision inference from the terminal, and interact with the model through its built-in <code data-enlighter-language="python" class="EnlighterJSRAW">REST API</code>.</p>



<h3 class="wp-block-heading">Installing Ollama</h3>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-38-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="893" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-38-1024x893.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54652"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-38-1024x893.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-38-1024x893.png?size=126x110&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-38-1024x893.png?size=252x220&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-38-1024x893.png?size=378x330&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-38-1024x893.png?size=504x440&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-38-1024x893.png?size=630x549&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 1:</strong> <code>Ollama</code> Download Page (source: <a href="https://ollama.com/download" target="_blank" rel="noreferrer noopener">Ollama website</a>)</figcaption></figure></div>


<p>The first step is installing <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> on your machine.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="1">curl -fsSL https://ollama.com/install.sh | sh
</pre>



<p>This command downloads the official installation script directly from the <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> website and executes it locally.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-39-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="150" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-39-1024x150.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54655"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-39-1024x150.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-39-1024x150.png?size=126x18&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-39-1024x150.png?size=252x37&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-39-1024x150.png?size=378x55&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-39-1024x150.png?size=504x74&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-39-1024x150.png?size=630x92&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 2:</strong> Terminal Command to Download <code>Ollama</code> (source: author)</figcaption></figure></div>


<p>After the installation completes, we can verify that <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> was installed successfully:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="2">ollama --version
</pre>



<p>If the command prints a version number, the installation is working correctly and we are ready to download Gemma 4.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-40-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="50" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-40-1024x50.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54658"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-40-1024x50.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-40-1024x50.png?size=126x6&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-40-1024x50.png?size=252x12&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-40-1024x50.png?size=378x18&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-40-1024x50.png?size=504x25&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-40-1024x50.png?size=630x31&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 3:</strong> Verifying <code>Ollama</code> version (source: author)</figcaption></figure></div>


<h3 class="wp-block-heading">Downloading the Gemma 4 Model</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> stores models locally and downloads them on demand.</p>



<p>To pull the Gemma 4 E2B model, run:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="3">ollama pull gemma4:e2b
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">pull</code> command downloads the model weights and stores them inside Ollama&#8217;s local model registry.</p>



<p>Here:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">gemma4</code>: specifies the model family</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">e2b</code>: refers to the Gemma 4 E2B variant</li>
</ul>



<p>Once the download finishes, the model becomes available for local inference without requiring any additional configuration.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-41-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="228" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-41-1024x228.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54662"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-41-1024x228.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-41-1024x228.png?size=126x28&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-41-1024x228.png?size=252x56&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-41-1024x228.png?size=378x84&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-41-1024x228.png?size=504x112&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-41-1024x228.png?size=630x140&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 4:</strong> Command to Download Gemma 4 E2B model (source: author)</figcaption></figure></div>


<h3 class="wp-block-heading">Running Gemma 4 Interactively</h3>



<p>With the model downloaded, we can launch an interactive chat session:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="4">ollama run gemma4:e2b
</pre>



<p>This command starts Gemma 4 and opens a terminal-based conversation interface.</p>



<p>You can now enter prompts directly into the terminal and receive responses from the model in real time.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="101">>>> explain the mixture-of-experts architecture.
Thinking...
Here's a thinking process that leads to the suggested explanation:
1.  **Deconstruct the Request:** The user wants an explanation of the "Mixture-of-Experts (MoE) architecture."
2.  **Determine the Core Context (Where does MoE live?):** MoE is primarily a model architecture, most famously applied to Large Language Models (LLMs) like those from Google (GLaM/PaLM/Gemini) 
and Meta.
3.  **Establish the Problem MoE Solves (Why MoE?):** Standard dense models (like a traditional Transformer) are computationally expensive. They require massive parameter counts and massive 
computations for *every* input, even for simple tasks.
    *   *Goal:* Make models larger (more capable) without increasing the computational cost per inference/training step proportionally.
….
….</pre>



<p>To exit the session, type:</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">/bye</code></p>



<p>or press:</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">Ctrl + D</code></p>



<p>This interactive mode is useful when experimenting with prompts, testing reasoning capabilities, or quickly validating model behavior before integrating it into an application.</p>



<h3 class="wp-block-heading">Image Understanding with Gemma 4</h3>



<p>Gemma 4 is a multimodal model, meaning it can process both text and images.</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> exposes this capability through a simple command:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="6">ollama run gemma4:e2b "caption this image /Users/cosmo3769/Desktop/venice.jpg"
</pre>



<p>In this example, Gemma 4 receives an image and generates a textual description of its contents.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-23-scaled.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="674" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-23-1024x674.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-54667"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-23-1024x674.jpeg?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-23-1024x674.jpeg?size=126x83&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-23-1024x674.jpeg?size=252x166&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-23-1024x674.jpeg?size=378x249&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-23-1024x674.jpeg?size=504x332&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-23-1024x674.jpeg?size=630x415&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 5:</strong> Input Image (source: <a href="https://huggingface.co/datasets/merve/vlm_test_images/blob/main/venice.jpg" target="_blank" rel="noreferrer noopener">Hugging Face</a>)</figcaption></figure></div>


<p>Behind the scenes, <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> loads the image, converts it into visual embeddings using Gemma 4&#8217;s vision encoder, and passes those embeddings into the language model alongside the text prompt.</p>



<p>From the user&#8217;s perspective, image understanding works exactly like a standard text prompt.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="102">Added image '/Users/cosmo3769/Desktop/venice.jpg'
Thinking...
Here's a thinking process for generating the caption:
1.  **Analyze the Image:**
    *   **Subject:** A large, classical-style building (a temple/fascia) situated over water.
    *   **Setting:** A canal/river scene. There are wooden pilings in the water, suggesting a waterfront or mooring area.
    *   **Architecture:** The building is ornate, stone, and features columns and pediments. It looks like a palazzo or a significant public structure.
    *   **Context/Location Clues (Inferred):** The architecture style strongly suggests Italy, likely Venice or a similar canal city.
    *   **Foreground/Background:** Wooden waterboats/barges are visible. There are old European buildings lining the far bank. A bridge/structure is visible on the right. The light suggests a 
sunny day.
2.  **Identify Key Themes &amp; Mood:**
    *   History/Antiquity
    *   Waterways/Canals
    *   European Charm
    *   Architecture/Grandeur
3.  **Determine the Goal:** Create a compelling caption for this scenic, historical photo.
4.  **Draft Caption Options (Categorized):**
    *   *Descriptive/Literal:* Focus on what is seen.
    *   *Evocative/Poetic:* Focus on the feeling and atmosphere.
    *   *Location-Specific (If known):* If we assume Venice/Italy.
    *   *Travel/Wanderlust Focused:* Encouraging travel.
5.  **Refine and Select the Best Options (Adding specific details):**
    *   *Focus on the contrast:* The grandeur of the building vs. the water.
    *   *Focus on the atmosphere:* Serene, timeless.
    *   *Focus on the setting:* Venetian/Italian canals.
6.  **Final Polish (Generating a variety of styles):** (This leads to the suggested captions below.)
...done thinking.
Here are several options for captioning this beautiful image, depending on the tone you want to convey:
**1. Descriptive &amp; Historical:**
*   "A glimpse of timeless elegance and history along the canals. The grandeur of the architecture reflected in the calm waters."
*   "Ancient beauty meets the waterways. Exploring the historic facades and hidden corners of this European city."
*   "The stunning architecture overlooking the tranquil waters. A true masterpiece of history."
**2. Evocative &amp; Poetic:**
*   "Where history flows on water. Serenity and splendor in every frame."
*   "Lost in the charm of the canals, where old stones whisper tales of the past."
*   "Golden light on ancient waters. A perfect moment of European serenity."
**3. Travel &amp; Wanderlust Focused:**
*   "Dreaming of Italian canals and timeless beauty. 🇮🇹"
*   "Wanderlust activated! Finding magic in the waterways of Europe."
*   "Exploring the hidden gems of a historic city by the water."
**4. Short &amp; Punchy (Good for Instagram):**
*   "Canal views and classical charm."
*   "Waterways of wonder."
*   "Historic beauty afloat."
*   "Venetian dreams."
***
**✨ Pro-Tip: Add Relevant Hashtags**
#Venice #Italy #Canals #History #Architecture #Travel #Europe #HistoricCity #WaterViews #TravelGram
</pre>



<h3 class="wp-block-heading">Using the OpenAI-Compatible API</h3>



<p>One of Ollama&#8217;s most useful features is its OpenAI-compatible API.</p>



<p>When <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> is running, it automatically exposes an HTTP endpoint that mimics the OpenAI Chat Completions API. This means existing applications built for OpenAI often require only a single URL change to work with local models.</p>



<p>We can send a request using cURL:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="8">curl http://localhost:11434/v1/chat/completions \
 -H "Content-Type: application/json" \
 -d '{
   "model": "gemma4:e2b",
   "messages": [
     {"role": "user", "content": "Explain how transformers work in two sentences."}
   ]
 }'
</pre>



<p>Let us break down the request:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">model</code>: specifies which local model should handle the request</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">messages</code>: contains the conversation history</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">role</code>: identifies whether a message comes from the user, assistant, or system</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">content</code>: contains the actual prompt</li>
</ul>



<p>The response format mirrors OpenAI&#8217;s API structure, making it straightforward to integrate <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> into existing AI applications.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-42-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="191" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-42-1024x191.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54674"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-42-1024x191.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-42-1024x191.png?size=126x24&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-42-1024x191.png?size=252x47&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-42-1024x191.png?size=378x71&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-42-1024x191.png?size=504x94&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-42-1024x191.png?size=630x118&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 6:</strong> OpenAI-Compatible API script to run inference (source: author)</figcaption></figure></div>


<h3 class="wp-block-heading">Sending Images via the Ollama API</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> also supports multimodal requests through its native API.</p>



<p>The following example sends an image directly to Gemma 4:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-24-scaled.jpeg" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="674" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-24-1024x674.jpeg?lossy=2&strip=1&webp=1" alt="" class="wp-image-54677"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-24-1024x674.jpeg?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-24-1024x674.jpeg?size=126x83&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-24-1024x674.jpeg?size=252x166&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-24-1024x674.jpeg?size=378x249&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-24-1024x674.jpeg?size=504x332&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-24-1024x674.jpeg?size=630x415&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 7:</strong> Input Image (source: <a href="https://huggingface.co/datasets/merve/vlm_test_images/blob/main/venice.jpg" target="_blank" rel="noreferrer noopener">Hugging Face</a>)</figcaption></figure></div>


<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="9">curl http://localhost:11434/api/generate -d "{
 \"model\": \"gemma4:e2b\",
 \"prompt\": \"Describe what you see in this image\",
 \"images\": [\"$(base64 -i /Users/cosmo3769/Desktop/venice.jpg)\"],
 \"stream\": false
}"
</pre>



<p>Unlike the OpenAI-compatible endpoint, this API accepts image data directly.</p>



<p>Notice the use of:</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">base64 -i image.jpg</code></p>



<p>Images cannot be transmitted directly inside JSON payloads, so we first encode the image as a Base64 string. <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> then decodes the image, processes it through Gemma 4&#8217;s vision encoder, and generates a textual response describing what it sees.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">"stream": false</code> parameter tells <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> to return the complete response in a single payload instead of streaming tokens incrementally.</p>



<p>At this point, we have already covered the majority of day-to-day Gemma 4 workflows:</p>



<ul class="wp-block-list">
<li>Interactive chat</li>



<li>Text generation</li>



<li>Multimodal image understanding</li>



<li>OpenAI-compatible APIs</li>



<li>Programmatic image inference</li>
</ul>



<p>For many developers, <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> is all that is needed to begin building local Gemma 4 applications.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-43.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="417" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-43-1024x417.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54679"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-43-1024x417.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-43-1024x417.png?size=126x51&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-43-1024x417.png?size=252x103&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-43-1024x417.png?size=378x154&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-43-1024x417.png?size=504x205&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-43-1024x417.png?size=630x257&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 8:</strong> Sending Images via the <code>Ollama</code> API (source: author)</figcaption></figure></div>


<p>To learn more about <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code>, you can refer to this <a href="https://pyimg.co/16eu7" target="_blank" rel="noreferrer noopener">blog from PyImageSearch</a>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-Gemma-4-llama-cpp-GGUF-Models"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-Gemma-4-llama-cpp-GGUF-Models">Running Gemma 4 with llama.cpp and GGUF Models</a></h2>



<p>While <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> prioritizes simplicity and ease of use, <strong><a href="https://llama-cpp.com" target="_blank" rel="noreferrer noopener">llama.cpp</a></strong> is built for performance, flexibility, and fine-grained control. It is one of the most popular inference engines for running large language models locally, offering efficient CPU execution, hardware acceleration, and native support for <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> models.</p>



<p>Unlike <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code>, which abstracts away much of the deployment process, <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> gives us direct control over every aspect of inference. We explicitly choose the model file, configure hardware acceleration, launch inference servers, and customize runtime parameters to suit our hardware and workload. This level of control makes <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> an excellent choice for developers who want to optimize performance or integrate local models into custom applications.</p>



<p>In this section, we will build <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> from source, download a Gemma 4 <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> model, run inference from the command line, launch an OpenAI-compatible API server, and finally enable multimodal image understanding using Gemma 4&#8217;s <strong>MMProj</strong> (multimodal projector) model.</p>



<h3 class="wp-block-heading">Installing Build Dependencies</h3>



<p>Before building <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code>, we first need to install the required development tools.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="10">xcode-select --install
clang --version
</pre>



<p>The first command installs Apple&#8217;s Command Line Tools, which include the Clang compiler and other utilities required for compiling C++ projects.</p>



<p>After the installation completes, we can verify that Clang is available by checking its version.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-44-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="126" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-44-1024x126.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54681"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-44-1024x126.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-44-1024x126.png?size=126x16&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-44-1024x126.png?size=252x31&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-44-1024x126.png?size=378x47&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-44-1024x126.png?size=504x62&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-44-1024x126.png?size=630x78&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 9:</strong> Verifying Clang version (source: author)</figcaption></figure></div>


<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="11">brew install git cmake
git --version
cmake --version
</pre>



<p>Next, install Git and CMake using Homebrew.</p>



<p>Here:</p>



<ul class="wp-block-list">
<li><strong>Git:</strong> is used to clone the <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> source code.</li>



<li><strong>CMake:</strong> generates platform-specific build files and manages the compilation process.</li>
</ul>



<p>Running the version commands confirms that both tools were installed successfully.</p>



<h3 class="wp-block-heading">Cloning the Repository</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="12">git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
</pre>



<p>Once the required tools are installed, we can download the <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> source code directly from GitHub.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">git clone</code> command downloads the latest version of the repository to your local machine.</p>



<p>We then change into the project directory, where all remaining build and inference commands will be executed.</p>



<h3 class="wp-block-heading">Building llama.cpp</h3>



<p>Next, we compile <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> from source.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="13">mkdir build
cd build
cmake ..
</pre>



<p>Let us briefly examine these commands.</p>



<p>First, we create a separate <code data-enlighter-language="python" class="EnlighterJSRAW">build</code> directory. Keeping compiled files separate from the source code makes it easier to rebuild or clean the project later.</p>



<p>The command: <code data-enlighter-language="python" class="EnlighterJSRAW">cmake ..</code> configures the project and generates the appropriate build files for your system.</p>



<p>After configuration completes, we compile the project using: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="14">cmake --build . --config Release
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">Release</code> configuration enables compiler optimizations, resulting in faster inference performance than a debug build.</p>



<p>Once the compilation finishes, the generated executables can be found inside the <code data-enlighter-language="python" class="EnlighterJSRAW">build/bin</code> directory.</p>



<p>This directory contains utilities such as:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">llama-cli</code></li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">llama-server</code></li>



<li>model conversion tools</li>



<li>quantization utilities</li>
</ul>



<p>These binaries form the core of the <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> inference ecosystem.</p>



<h3 class="wp-block-heading">Enabling Apple Metal Acceleration</h3>



<p>If you are using an Apple Silicon Mac, <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> can offload model computation to the GPU through Apple&#8217;s Metal framework.</p>



<p>To enable <code data-enlighter-language="python" class="EnlighterJSRAW">Metal</code> support, rebuild <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> with the following configuration:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="15">cmake -B build -DGGML_METAL=ON
cmake --build build --config Release
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">GGML_METAL=ON</code> flag enables GPU acceleration during compilation.</p>



<p>Compared to CPU-only inference, <code data-enlighter-language="python" class="EnlighterJSRAW">Metal</code> acceleration can significantly improve generation speed, particularly for larger Gemma 4 models.</p>



<h3 class="wp-block-heading">Verifying the Installation</h3>



<p>Before downloading a model, it is a good idea to verify that the build completed successfully.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="16">./build/bin/llama-cli --help
</pre>



<p>If the installation is successful, <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> prints the available command-line options for <code data-enlighter-language="python" class="EnlighterJSRAW">llama-cli</code>.</p>



<p>This confirms that the executable was built correctly and is ready to load Gemma 4.</p>



<h3 class="wp-block-heading">Downloading and Locating a Gemma GGUF Model</h3>



<p>Unlike <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> works directly with <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> model files.</p>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> is a binary model format optimized for fast local inference and supports multiple quantization levels, allowing models to run efficiently on CPUs and consumer GPUs while reducing memory usage.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="17">python3 -m venv .venv
source .venv/bin/activate
</pre>



<p>We first create a Python virtual environment:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="18">./build/bin/llama-server -hf ggml-org/gemma-4-E2B-it-GGUF
</pre>



<p>Next, download the Gemma 4 <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> model directly from Hugging Face:</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">-hf</code> option downloads the specified repository automatically and stores the model locally.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="19">find ~ -name "*gemma*.gguf" 2>/dev/null
</pre>



<p>If you already have the model on your machine, you can locate it using:</p>



<p>This searches your home directory for all <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> files containing &#8220;gemma&#8221; in their filename.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-45-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="78" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-45-1024x78.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54683"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-45-1024x78.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-45-1024x78.png?size=126x10&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-45-1024x78.png?size=252x19&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-45-1024x78.png?size=378x29&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-45-1024x78.png?size=504x38&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-45-1024x78.png?size=630x48&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 10:</strong> Locating a Gemma <code>GGUF</code> model in local system (source: author)</figcaption></figure></div>


<h3 class="wp-block-heading">Running Gemma 4 from the Command Line</h3>



<p>Once the model has been downloaded, we can perform inference directly from the terminal.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="20">./build/bin/llama-cli \
 -m /Users/cosmo3769/.lmstudio/models/lmstudio-community/gemma-4-E2B-it-GGUF/gemma-4-E2B-it-Q4_K_M.gguf \
 -p "Explain the Mixture of Experts architecture" \
 -n 512 \
 -ngl 99
</pre>



<p>Here:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">-m</code>: specifies the path to the <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> model.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">-p</code>: provides the prompt sent to Gemma 4.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">-n 512</code>: allows the model to generate up to 512 new tokens.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">-ngl 99</code>: offloads supported transformer layers to the GPU for faster inference. The exact value depends on your available GPU memory and hardware configuration.</li>
</ul>



<p><em><strong>NOTE: </strong></em><em>We are using the GGUF model from </em><em>LM S</em><em>tudio which we downloaded earlier. We can also use it from hf but it will be the same.</em></p>



<p>After executing the command, Gemma 4 generates a response directly inside the terminal without requiring a separate inference server.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-46-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="591" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-46-1024x591.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54684"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-46-1024x591.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-46-1024x591.png?size=126x73&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-46-1024x591.png?size=252x145&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-46-1024x591.png?size=378x218&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-46-1024x591.png?size=504x291&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-46-1024x591.png?size=630x364&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 11:</strong> Running Gemma 4 from the command line (source: author)</figcaption></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-47.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="378" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-47-1024x378.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54685"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-47-1024x378.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-47-1024x378.png?size=126x47&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-47-1024x378.png?size=252x93&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-47-1024x378.png?size=378x140&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-47-1024x378.png?size=504x186&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-47-1024x378.png?size=630x233&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 12:</strong> Generated response by Gemma 4 inside terminal (source: author)</figcaption></figure></div>


<h3 class="wp-block-heading">Starting a Local API Server</h3>



<p>Instead of running one prompt at a time, we can expose Gemma 4 through a local HTTP server.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="21">./build/bin/llama-server \
 -m /Users/cosmo3769/.lmstudio/models/lmstudio-community/gemma-4-E2B-it-GGUF/gemma-4-E2B-it-Q4_K_M.gguf \
 --host 0.0.0.0 \
 --port 8080 \
 -n 512
</pre>



<p>Here:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">--host 0.0.0.0</code>: makes the server accessible on all network interfaces</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--port 8080</code>: specifies the port used for incoming requests</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">-n 512</code>: sets the default maximum number of generated tokens</li>
</ul>



<p>Once the server starts successfully, it can be accessed at: <code data-enlighter-language="python" class="EnlighterJSRAW">http://127.0.0.1:8080</code>.</p>



<p>This allows other applications, scripts, or web interfaces to communicate with Gemma 4 through a local API instead of launching a new inference process for every request.</p>



<figure style="text-align: center; max-width: 700px; margin: auto;">
<iframe width="700" height="394" src="https://www.youtube.com/embed/qtt_OdsZsS0" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  <figcaption style="align: center; margin-top: 8px;">    <strong>Figure 13:</strong> Running Gemma 4 Locally Using the <code>llama.cpp</code> Web Interface (source: author)
  </figcaption>
</figure>



<h3 class="wp-block-heading">Enabling Vision Support with MMProj</h3>



<p>Although Gemma 4 is a multimodal model, image understanding requires an additional <strong>MMProj</strong> model when running through <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code>.</p>



<p>The MMProj model converts visual features produced by the vision encoder into the embedding space expected by the language model, allowing Gemma 4 to reason jointly over images and text.</p>



<p>To enable vision support, launch the server with the additional <code data-enlighter-language="python" class="EnlighterJSRAW">--mmproj</code> argument.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="22">./build/bin/llama-server \
 -m /Users/cosmo3769/.lmstudio/models/lmstudio-community/gemma-4-E2B-it-GGUF/gemma-4-E2B-it-Q4_K_M.gguf \
 --mmproj /Users/cosmo3769/.lmstudio/models/lmstudio-community/gemma-4-E2B-it-GGUF/mmproj-gemma-4-E2B-it-BF16.gguf \
 --host 0.0.0.0 \
 --port 8080 \
 -n 512
</pre>



<p>Once the MMProj model is loaded, the server can process multimodal requests containing both text and images, enabling tasks such as image captioning, visual question answering, and general image understanding while continuing to use the same GGUF language model.</p>



<p>To learn more about <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code>, you can refer to this <a href="https://pyimagesearch.com/2024/08/26/llama-cpp-the-ultimate-guide-to-efficient-llm-inference-and-applications/" target="_blank" rel="noreferrer noopener">blog from PyImageSearch</a>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-Gemma-4-MLX-Apple-Silicon-Macs"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-Gemma-4-MLX-Apple-Silicon-Macs">Running Gemma 4 with MLX on Apple Silicon Macs</a></h2>



<p>If you are using an Apple Silicon Mac, <strong><a href="https://github.com/ml-explore/mlx" target="_blank" rel="noreferrer noopener">MLX</a></strong> is one of the most efficient ways to run Gemma 4 locally. Developed by Apple, <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code> is a machine learning framework built specifically for Apple Silicon, enabling models to take full advantage of the unified memory architecture and GPU acceleration available on M-series chips.</p>



<p>Building on top of <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code>, <strong><a href="https://github.com/Blaizzy/mlx-vlm" target="_blank" rel="noreferrer noopener">MLX-VLM</a></strong> extends the framework with support for vision-language models, making it straightforward to run multimodal models such as Gemma 4 with just a few commands. It also provides an OpenAI-compatible server, allowing locally hosted models to integrate seamlessly with existing applications and tools that already support the OpenAI API.</p>



<p>In this section, we will install <code data-enlighter-language="python" class="EnlighterJSRAW">MLX-VLM</code>, perform multimodal inference from the command line, launch a local OpenAI-compatible server, and interact with Gemma 4 using both cURL and the official OpenAI Python SDK (Software Development Kit).</p>



<h3 class="wp-block-heading">Creating a Python Environment</h3>



<p>We begin by creating a dedicated Python virtual environment.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="23">python3 -m venv .venv
source .venv/bin/activate
</pre>



<p>The first command creates an isolated Python environment named <code data-enlighter-language="python" class="EnlighterJSRAW">.venv</code>, while the second activates it.</p>



<p>Using a virtual environment keeps project dependencies separate from the system-wide Python installation and helps avoid version conflicts between different machine learning projects.</p>



<h3 class="wp-block-heading">Installing MLX-VLM</h3>



<p>Next, install the <code data-enlighter-language="python" class="EnlighterJSRAW">MLX-VLM</code> package.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="24">pip install mlx-vlm
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">mlx-vlm</code> package includes everything needed to download, load, and run multimodal vision-language models on Apple Silicon devices.</p>



<p>Once installed, we can immediately begin running Gemma 4 without manually downloading model files or compiling inference engines.</p>



<h3 class="wp-block-heading">Running Single-Image Inference</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">MLX-VLM</code> provides a convenient command-line interface for performing multimodal inference.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="25">mlx_vlm.generate \
 --model mlx-community/gemma-4-e2b-it-4bit \
 --image https://huggingface.co/datasets/huggingface/documentation-images/resolve/0052a70beed5bf71b92610a43a52df6d286cd5f3/diffusers/rabbit.jpg \
 --prompt "Describe this image in detail"
</pre>



<p>Let us examine the command:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">--model</code>: specifies the Gemma 4 checkpoint to load</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--image</code>: points to the image that will be analyzed</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">--prompt</code>: provides the accompanying text instruction for the model</li>
</ul>



<p>If the model is not already available locally, <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code> automatically downloads it from Hugging Face before performing inference.</p>



<p>After loading the image and prompt, Gemma 4 processes both inputs together and generates a detailed textual description of the image.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-48.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="447" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-48-1024x447.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54694"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-48-1024x447.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-48-1024x447.png?size=126x55&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-48-1024x447.png?size=252x110&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-48-1024x447.png?size=378x165&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-48-1024x447.png?size=504x220&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-48-1024x447.png?size=630x275&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 14:</strong> Single-Image Inference using <code>MLX-VLM</code> (source: author)</figcaption></figure></div>


<h3 class="wp-block-heading">Launching an OpenAI-Compatible Server</h3>



<p>Instead of running one inference at a time, we can launch a local server that exposes an OpenAI-compatible API.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="26">mlx_vlm.server \
 --model mlx-community/gemma-4-e2b-it-4bit \
 --port 8080
</pre>



<p>This command loads the model into memory and starts a local inference server listening on port <strong>8080</strong>.</p>



<p>Once running, any application capable of communicating with the OpenAI Chat Completions API can send requests directly to the local server without requiring changes to the overall application logic.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-49-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="171" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-49-1024x171.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54696"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-49-1024x171.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-49-1024x171.png?size=126x21&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-49-1024x171.png?size=252x42&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-49-1024x171.png?size=378x63&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-49-1024x171.png?size=504x84&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-49-1024x171.png?size=630x105&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 15:</strong> Launching an OpenAI-Compatible Server (source: author)</figcaption></figure></div>


<h3 class="wp-block-heading">Querying the Server with cURL</h3>



<p>With the server running, we can send requests using standard HTTP calls.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="27">curl http://localhost:8080/v1/chat/completions \
 -H "Content-Type: application/json" \
 -d '{
   "model": "mlx-community/gemma-4-e2b-it-4bit",
   "messages": [
     {
       "role": "user",
       "content": [
         {"type": "text", "text": "Describe this image in detail"},
         {"type": "image_url", "image_url": {"url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/0052a70beed5bf71b92610a43a52df6d286cd5f3/diffusers/rabbit.jpg"}}
       ]
     }
   ],
   "max_tokens": 500
 }'
</pre>



<p>The request closely mirrors the OpenAI Chat Completions API.</p>



<p>Unlike text-only requests, the <code data-enlighter-language="python" class="EnlighterJSRAW">content</code> field now contains a list consisting of both:</p>



<ul class="wp-block-list">
<li>a text instruction</li>



<li>an image URL</li>
</ul>



<p>Gemma 4 processes the image and prompt together before generating a multimodal response.</p>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">max_tokens</code> parameter specifies the maximum number of tokens the model may generate.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-50.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="298" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-50-1024x298.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54698"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-50-1024x298.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-50-1024x298.png?size=126x37&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-50-1024x298.png?size=252x73&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-50-1024x298.png?size=378x110&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-50-1024x298.png?size=504x147&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-50-1024x298.png?size=630x183&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 16:</strong> Querying the Server with cURL (source: author)</figcaption></figure></div>


<h3 class="wp-block-heading">Querying the Server with Python and OpenAI SDK</h3>



<p>Since <code data-enlighter-language="python" class="EnlighterJSRAW">MLX-VLM</code> exposes an OpenAI-compatible endpoint, we can also interact with it using the official OpenAI Python SDK.</p>



<p>First, install the SDK:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="28">pip install openai --break-system-packages
</pre>



<p>Next, create an OpenAI client that points to the locally running server.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="29">from openai import OpenAI

client = OpenAI(base_url="http://localhost:8080/v1", api_key="fake")
</pre>



<p>Notice that the API key is simply a placeholder. Authentication is not required because the server is running locally on our machine.</p>



<p>Once the client has been initialized, sending multimodal requests is almost identical to interacting with OpenAI-hosted models.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="30">response = client.chat.completions.create(
   model="mlx-community/gemma-4-e2b-it-4bit",
   messages=[
       {
           "role": "user",
           "content": [
               {"type": "text", "text": "Describe this image"},
               {"type": "image_url", "image_url": {"url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/0052a70beed5bf71b92610a43a52df6d286cd5f3/diffusers/rabbit.jpg
"}}
           ]
       }
   ],
   max_tokens=500
)
</pre>



<p>The request contains both text and image inputs inside the <code data-enlighter-language="python" class="EnlighterJSRAW">messages</code> field, following the same structure used by the OpenAI Vision API.</p>



<p>After the request is sent, Gemma 4 analyzes the image, combines it with the accompanying prompt, and returns a textual response, which we print using:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="31">print(response.choices[0].message.content)
</pre>



<p>One of the biggest advantages of <code data-enlighter-language="python" class="EnlighterJSRAW">MLX-VLM</code> is that developers already using the OpenAI SDK can switch to a fully local Gemma 4 deployment with minimal code changes. In many cases, updating the <code data-enlighter-language="python" class="EnlighterJSRAW">base_url</code> to point to the local server is all that is required, making it straightforward to move between cloud-hosted and on-device inference while keeping the rest of the application unchanged.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-51-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="378" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-51-1024x378.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54701"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-51-1024x378.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-51-1024x378.png?size=126x47&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-51-1024x378.png?size=252x93&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-51-1024x378.png?size=378x140&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-51-1024x378.png?size=504x186&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-51-1024x378.png?size=630x233&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 17:</strong> Processing Images Through the OpenAI SDK (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-Gemma-4-LM-Studio-Desktop-App"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-Gemma-4-LM-Studio-Desktop-App">Running Gemma 4 with LM Studio&#8217;s Desktop App</a></h2>



<p>If you are looking for the easiest way to run Gemma 4 locally without using the command line, <strong><a href="https://lmstudio.ai" target="_blank" rel="noreferrer noopener">LM Studio</a></strong> is an excellent choice. It provides an intuitive graphical interface for downloading, managing, and interacting with local language models, while handling much of the underlying configuration automatically.</p>



<p>Behind the scenes, <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> uses <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> as its inference engine for <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> models, allowing you to benefit from efficient CPU and GPU inference without manually compiling the runtime or configuring model files. This makes it an ideal option for developers who want a streamlined setup while still taking advantage of the performance offered by <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code>.</p>



<p>In this section, we will install <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code>, download a Gemma 4 model, configure the local runtime, and begin interacting with Gemma 4 through its built-in chat interface.</p>



<h3 class="wp-block-heading">Installing LM Studio</h3>



<p>Begin by downloading and installing <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> from its official website. Once the installation is complete, launch the application.</p>



<p>On the first launch, you will be greeted with the welcome screen shown in <strong>Figure </strong><strong>18</strong>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-52-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="659" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-52-1024x659.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54703"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-52-1024x659.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-52-1024x659.png?size=126x81&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-52-1024x659.png?size=252x162&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-52-1024x659.png?size=378x243&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-52-1024x659.png?size=504x324&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-52-1024x659.png?size=630x405&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 18:</strong> Welcome screen displayed when launching <code>LM Studio</code> for the first time (source: LM Studio Application)</figcaption></figure></div>


<p>Click <strong>Get Started</strong> to begin the setup process.</p>



<h3 class="wp-block-heading">Downloading Your First Model</h3>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> guides you through downloading your first local model.</p>



<p>As shown in <strong>Figure </strong><strong>19</strong>, it recommends downloading a Gemma 4 model directly from within the application.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-53-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="659" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-53-1024x659.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54705"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-53-1024x659.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-53-1024x659.png?size=126x81&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-53-1024x659.png?size=252x162&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-53-1024x659.png?size=378x243&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-53-1024x659.png?size=504x324&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-53-1024x659.png?size=630x405&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 19:</strong> <code>LM Studio</code> recommends downloading a Gemma 4 model during the initial setup (source: LM Studio Application)</figcaption></figure></div>


<p>Click <strong>Download gemma-4-e4b</strong> to begin downloading the model. During the download, <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> displays the current progress while allowing you to continue using the application.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-54-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="659" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-54-1024x659.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54707"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-54-1024x659.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-54-1024x659.png?size=126x81&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-54-1024x659.png?size=252x162&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-54-1024x659.png?size=378x243&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-54-1024x659.png?size=504x324&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-54-1024x659.png?size=630x405&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 20:</strong> Download progress for the selected Gemma 4 model (source: LM Studio Application)</figcaption></figure></div>


<p>Once the download finishes, you will see a confirmation screen indicating that the model is ready for use.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-55-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="659" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-55-1024x659.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54708"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-55-1024x659.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-55-1024x659.png?size=126x81&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-55-1024x659.png?size=252x162&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-55-1024x659.png?size=378x243&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-55-1024x659.png?size=504x324&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-55-1024x659.png?size=630x405&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 21:</strong> Model download completed successfully (source: LM Studio Application)</figcaption></figure></div>


<p>Click <strong>Continue</strong> to proceed to the final setup step.</p>



<h3 class="wp-block-heading">Enabling Developer Mode</h3>



<p>Before entering the main interface, <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> presents several optional developer settings.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-56-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="659" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-56-1024x659.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54709"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-56-1024x659.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-56-1024x659.png?size=126x81&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-56-1024x659.png?size=252x162&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-56-1024x659.png?size=378x243&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-56-1024x659.png?size=504x324&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-56-1024x659.png?size=630x405&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 22:</strong> Advanced settings available during the initial configuration (source: LM Studio Application)</figcaption></figure></div>


<p>For this lesson, enable both options:</p>



<ul class="wp-block-list">
<li><strong>Turn on Developer Mode</strong></li>



<li><strong>Start local LLM service on login</strong></li>
</ul>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">Developer Mode</code> exposes additional features useful for developers, while automatically starting the local server makes it easier to access your models from external applications through an OpenAI-compatible API.</p>



<p>After enabling these options, click <strong>Continue to LM Studio</strong>.</p>



<h3 class="wp-block-heading">Loading Gemma 4</h3>



<p>Once <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> opens, you will see an empty workspace similar to <strong>Figure </strong><strong>23</strong>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-57-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="644" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-57-1024x644.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54710"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-57-1024x644.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-57-1024x644.png?size=126x79&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-57-1024x644.png?size=252x158&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-57-1024x644.png?size=378x238&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-57-1024x644.png?size=504x317&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-57-1024x644.png?size=630x396&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 23:</strong> Main <code>LM Studio</code> interface after completing the setup (source: LM Studio Application)</figcaption></figure></div>


<p>To begin chatting with Gemma 4, create a new conversation by clicking <strong>New Chat</strong>.</p>



<p>Next, click <strong>Select a model to load</strong> at the top of the window.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-58-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="644" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-58-1024x644.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54712"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-58-1024x644.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-58-1024x644.png?size=126x79&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-58-1024x644.png?size=252x158&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-58-1024x644.png?size=378x238&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-58-1024x644.png?size=504x317&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-58-1024x644.png?size=630x396&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 24:</strong> Creating a new chat before selecting a model (source: LM Studio Application)</figcaption></figure></div>


<p>If you have already downloaded Gemma 4 during setup, you can simply select it from the available models.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-59-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="644" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-59-1024x644.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54714"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-59-1024x644.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-59-1024x644.png?size=126x79&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-59-1024x644.png?size=252x158&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-59-1024x644.png?size=378x238&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-59-1024x644.png?size=504x317&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-59-1024x644.png?size=630x396&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 25: </strong>Downloaded model selected (source: LM Studio Application)</figcaption></figure></div>


<p>Otherwise, use the search bar to search for <strong>gemma-4-e2b</strong>. <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> displays several compatible model variants, including <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code> versions.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-60-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="644" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-60-1024x644.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54716"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-60-1024x644.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-60-1024x644.png?size=126x79&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-60-1024x644.png?size=252x158&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-60-1024x644.png?size=378x238&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-60-1024x644.png?size=504x317&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-60-1024x644.png?size=630x396&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 26:</strong> Searching for Gemma 4 models inside <code>LM Studio</code> (source: LM Studio Application)</figcaption></figure></div>


<p>For this lesson, select the <strong>gemma-4-E2B-it-GGUF</strong> model and click <strong>Download</strong>. <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> automatically downloads the model and stores it in its local model library.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-61-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="644" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-61-1024x644.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54718"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-61-1024x644.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-61-1024x644.png?size=126x79&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-61-1024x644.png?size=252x158&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-61-1024x644.png?size=378x238&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-61-1024x644.png?size=504x317&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-61-1024x644.png?size=630x396&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 27: </strong>Model Downloading (source: LM Studio Application)</figcaption></figure></div>


<p>After the download completes, load the model into memory by selecting it from the model picker.</p>



<p>Once loaded successfully, the model name appears at the top of the interface, and the chat window becomes active.</p>



<h3 class="wp-block-heading">Running Inference</h3>



<p>You can now interact with Gemma 4 just like any other chat assistant. Simply enter your prompt into the message box and press <strong>Enter</strong> to generate a response.</p>



<p>For example, asking:</p>



<p><code data-enlighter-language="raw" class="EnlighterJSRAW">What is an LLM?</code></p>



<p>produces the response shown in <strong>Figure </strong><strong>28</strong>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-62-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="571" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-62-1024x571.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54719"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-62-1024x571.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-62-1024x571.png?size=126x70&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-62-1024x571.png?size=252x141&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-62-1024x571.png?size=378x211&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-62-1024x571.png?size=504x281&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-62-1024x571.png?size=630x351&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 28:</strong> Gemma 4 loaded and ready for inference (source: LM Studio Application)</figcaption></figure></div>


<p>In <strong>Figure </strong><strong>29</strong>, we can also see that we can input an image and prompt together to generate a response. </p>



<figure style="text-align: center; max-width: 700px; margin: auto;">
<iframe width="700" height="394" src="https://www.youtube.com/embed/7X5DFxGPsjg" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  <figcaption style="align: center; margin-top: 8px;">
    <strong>Figure 29:</strong> Running inference locally with Gemma 4 inside <code>LM Studio</code> (source: LM Studio Application)
  </figcaption>
</figure>



<p>At this point, we have a fully functional local Gemma 4 environment capable of running entirely on our own machine, without relying on external APIs or cloud services.</p>



<p>To learn more about <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code>, you can refer to this <a href="https://pyimagesearch.com/2024/06/24/integrating-local-llm-frameworks-a-deep-dive-into-lm-studio-and-anythingllm/" target="_blank" rel="noreferrer noopener">blog from PyImageSearch</a>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Running-Gemma-4-Locally-Transformers-js"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Running-Gemma-4-Locally-Transformers-js">Running Gemma 4 Locally with Transformers.js</a></h2>



<p>Unlike the deployment options we have explored so far, <strong><a href="https://github.com/huggingface/transformers.js" target="_blank" rel="noreferrer noopener">Transformers.js</a></strong> allows us to run Gemma 4 entirely from JavaScript. Instead of relying on Python or a native inference engine, it enables developers to load and run Hugging Face models directly within <code data-enlighter-language="python" class="EnlighterJSRAW">Node.js</code> applications using familiar JavaScript APIs.</p>



<p>Under the hood, <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> executes <strong>ONNX</strong> versions of Hugging Face models through ONNX Runtime, making it possible to perform local inference without leaving the JavaScript ecosystem. This makes it an excellent choice for JavaScript developers who want to build AI-powered applications without introducing a separate Python backend or external inference service.</p>



<p>In this section, we will install <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>, download an ONNX version of Gemma 4, load the model locally, process both image and audio inputs, and perform multimodal inference entirely from JavaScript.</p>



<h3 class="wp-block-heading">Setting Up the Project</h3>



<p>We begin by creating a new <code data-enlighter-language="python" class="EnlighterJSRAW">Node.js</code> project.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="32">npm init -y
</pre>



<p>This command initializes a new <code data-enlighter-language="python" class="EnlighterJSRAW">Node.js</code> project and creates a <code data-enlighter-language="python" class="EnlighterJSRAW">package.json</code> file containing the project&#8217;s metadata, dependencies, and configuration.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="33">npm install @huggingface/transformers wavefile
</pre>



<p>Next, install the required packages.</p>



<p>This installs 2 packages that our application depends on:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">@huggingface/transformers</code>: provides the <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> library for downloading, loading, and running Hugging Face models directly from JavaScript.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">wavefile</code>: is a lightweight library for reading and preprocessing WAV audio files before they are passed to Gemma 4.</li>
</ul>



<p>Once the installation completes, create a file named <code data-enlighter-language="python" class="EnlighterJSRAW">index.mjs</code>. This file will contain our entire multimodal inference pipeline, including model loading, input preprocessing, and response generation.</p>



<h3 class="wp-block-heading">Importing the Required Libraries</h3>



<p>We begin by importing the libraries needed throughout the application.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="34">import {
 AutoProcessor,
 Gemma4ForConditionalGeneration,
 TextStreamer,
 load_image,
} from "@huggingface/transformers";
import pkg from "wavefile";
const { WaveFile } = pkg;
import { readFileSync, existsSync, createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import { tmpdir } from "os";
import { join } from "path";
</pre>



<p>Let us understand the purpose of each import.</p>



<p>The first group of imports comes from <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>.</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">AutoProcessor</code>: loads the preprocessing pipeline associated with Gemma 4. It prepares text, images, and audio before they are passed to the model.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">Gemma4ForConditionalGeneration</code>: loads the Gemma 4 ONNX model and provides the generation API that we will use for multimodal inference.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">TextStreamer</code>: streams generated tokens to the terminal as they are produced instead of waiting for the entire response to finish. This creates a more interactive experience, especially for longer generations.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">load_image</code>: simplifies loading images from either local files or remote URLs.</li>
</ul>



<p>Next, we import the <code data-enlighter-language="python" class="EnlighterJSRAW">wavefile</code> package.</p>



<p>Unlike images, audio inputs require additional preprocessing before they can be passed to Gemma 4. The <code data-enlighter-language="python" class="EnlighterJSRAW">WaveFile</code> class allows us to read WAV (Waveform Audio File Format) files, convert them to the required bit depth, resample them to the appropriate sampling rate, and extract the raw audio samples expected by the model.</p>



<p>Finally, we import several built-in <code data-enlighter-language="python" class="EnlighterJSRAW">Node.js</code> modules. These utilities help us manage files and temporary resources during inference.</p>



<p>Here:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">fs</code>: provides functions for reading files from disk and creating temporary files</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">pipeline</code>: enables efficient streaming of downloaded data, such as audio files, directly into a file</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">tmpdir</code>: returns the operating system&#8217;s temporary directory, which we use for storing downloaded files during preprocessing</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">join</code>: constructs platform-independent file paths, ensuring the script works consistently across different operating systems</li>
</ul>



<p>Together, these libraries provide everything needed to load Gemma 4, preprocess multimodal inputs, and perform inference entirely from a JavaScript application.</p>



<h3 class="wp-block-heading">Creating Helper Functions</h3>



<p>Before loading the model, let us define a few helper functions that make our script easier to read and improve the overall user experience.</p>



<p>These functions are not directly involved in running Gemma 4. Instead, they provide utilities for formatting terminal output, displaying download progress, measuring execution time, and preprocessing audio before it is passed to the model.</p>



<h4 class="wp-block-heading">Formatting Console Output</h4>



<p>We begin by defining several ANSI (American National Standards Institute) escape codes.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="13" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="35">const RESET  = "\x1b[0m";
const BOLD   = "\x1b[1m";
const GREEN  = "\x1b[32m";
const CYAN   = "\x1b[36m";
const YELLOW = "\x1b[33m";
const DIM    = "\x1b[2m";
</pre>



<p>ANSI escape codes allow us to style text printed to the terminal. Throughout the script, we use them to distinguish different types of messages by applying colors and formatting.</p>



<p>For example:</p>



<ul class="wp-block-list">
<li><strong>Cyan:</strong> is used for informational messages.</li>



<li><strong>Green:</strong> indicates successful operations.</li>



<li><strong>Yellow:</strong> highlights warnings.</li>



<li><strong>Bold:</strong> makes important messages easier to identify.</li>



<li><strong>Dim:</strong> is used when displaying elapsed execution times.</li>
</ul>



<p>Although optional, these formatting codes make the script much easier to follow, especially since downloading and loading large language models can take several minutes.</p>



<h4 class="wp-block-heading">Logging Progress</h4>



<p>Next, we define 3 small helper functions.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="19" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="36">function log(label, msg = "") {
 console.log(`${BOLD}${CYAN}[${label}]${RESET} ${msg}`);
}

function success(label, msg = "") {
 console.log(`${BOLD}${GREEN}[${label}]${RESET} ${msg}`);
}

function warn(msg) {
 console.log(`${BOLD}${YELLOW}[WARN]${RESET} ${msg}`);
}
</pre>



<p>Rather than repeatedly writing lengthy <code data-enlighter-language="python" class="EnlighterJSRAW">console.log()</code> statements throughout the program, these helper functions provide a consistent way to display messages.</p>



<p>For example:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">log()</code>: prints general progress updates</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">success()</code>: indicates that a step completed successfully</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">warn()</code>: displays warning messages that the user should be aware of</li>
</ul>



<p>Using dedicated helper functions also keeps the main inference pipeline much cleaner and easier to read.</p>



<h4 class="wp-block-heading">Measuring Execution Time</h4>



<p>The next helper measures how long each stage of the pipeline takes to execute.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="30" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="37">function elapsed(start) {
 return `${DIM}(${((Date.now() - start) / 1000).toFixed(1)}s)${RESET}`;
}
</pre>



<p>This function accepts a starting timestamp, computes the elapsed time using <code data-enlighter-language="python" class="EnlighterJSRAW">Date.now()</code>, converts the result to seconds, and formats it for display.</p>



<p>We will use this helper throughout the script to report how long it takes to load the processor, initialize the model, preprocess inputs, and generate responses.</p>



<h4 class="wp-block-heading">Displaying Model Download Progress</h4>



<p>The first time the script runs, <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> must download the ONNX (Open Neural Network Exchange) model files from Hugging Face. Depending on your network connection and the model size, this process may take several minutes.</p>



<p>To provide better feedback during the download, we define a helper function that renders a progress bar inside the terminal.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="33" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="38">// Single overall progress bar that updates in place
let progressStarted = false;
function renderOverallProgress(pct) {
 const clamped = Math.min(Math.round(pct ?? 0), 100);
 const filled = Math.round(clamped / 2);
 const bar = "█".repeat(filled) + "░".repeat(50 - filled);
 if (!progressStarted) {
   process.stdout.write("\n");
   progressStarted = true;
 }
 process.stdout.write(`\r  Downloading  [${CYAN}${bar}${RESET}] ${clamped.toString().padStart(3)}%`);
 if (clamped >= 100) process.stdout.write(`  ${GREEN}✓${RESET}\n`);
}
</pre>



<p>The function receives the current download percentage, converts it into a visual progress bar, and continuously updates the same terminal line as the download progresses.</p>



<p>Instead of printing hundreds of individual progress messages, the user sees a single progress bar that gradually fills until the download completes.</p>



<p>This provides a much cleaner and more user-friendly experience while large model files are being retrieved.</p>



<h4 class="wp-block-heading">Loading and Preprocessing Audio</h4>



<p>Finally, we define the <code data-enlighter-language="python" class="EnlighterJSRAW">loadAudioNode()</code> helper.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="46" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="39">// Load WAV audio manually for Node.js (no AudioContext needed)
async function loadAudioNode(url) {
 const tmpPath = join(tmpdir(), "audio_input.wav");
 const res = await fetch(url);
 if (!res.ok) throw new Error(`Failed to fetch audio: ${res.statusText}`);
 const dest = createWriteStream(tmpPath);
 await pipeline(res.body, dest);
 const wav = new WaveFile(readFileSync(tmpPath));
 wav.toBitDepth("32f");
 wav.toSampleRate(16000);
 let samples = wav.getSamples();
 // If stereo, use only the first channel
 if (Array.isArray(samples)) samples = samples[0];
 return new Float32Array(samples);
}
</pre>



<p>Unlike images, audio requires several preprocessing steps before it can be passed into Gemma 4.</p>



<p>This helper performs the entire pipeline automatically.</p>



<p>Specifically, it:</p>



<ul class="wp-block-list">
<li>downloads the audio file from the provided URL</li>



<li>saves it to a temporary location on disk</li>



<li>opens the WAV file using the <code data-enlighter-language="python" class="EnlighterJSRAW">WaveFile</code> library</li>



<li>converts the audio to <strong>32-bit floating-point</strong> samples</li>



<li>resamples the audio to <strong>16 kHz</strong>, which is the sampling rate expected by Gemma 4</li>



<li>If the audio contains multiple channels (e.g., stereo), it keeps only the first channel and converts the result into a <code data-enlighter-language="python" class="EnlighterJSRAW">Float32Array</code>.</li>
</ul>



<p>The returned <code data-enlighter-language="python" class="EnlighterJSRAW">Float32Array</code> contains the raw waveform samples that will later be passed to the processor alongside the text prompt and image.</p>



<p>By moving all of this logic into a dedicated helper function, the main inference pipeline remains concise and focused on the high-level workflow rather than the details of audio preprocessing.</p>



<h3 class="wp-block-heading">Loading the Gemma 4 Processor</h3>



<p>With the helper functions in place, we are ready to load the processor and the Gemma 4 model.</p>



<p>We begin by specifying the model that we will use throughout this lesson.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="61" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="40">const model_id = "onnx-community/gemma-4-E2B-it-ONNX";
</pre>



<p>Unlike the previous lessons that used Hugging Face Transformers in Python, <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> works with <strong>ONNX</strong> models. Here, we are loading the <strong>Gemma 4 E2B Instruct</strong> model that has been converted to the ONNX format and published by the Hugging Face community.</p>



<p>Using a single <code data-enlighter-language="python" class="EnlighterJSRAW">model_id</code> variable also makes it easy to switch to another compatible model later by changing just one line of code.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="62" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="41">// 1. Processor
log("1/5", `Loading processor from ${BOLD}${model_id}${RESET} ...`);
let t = Date.now();
const processor = await AutoProcessor.from_pretrained(model_id);
success("1/5", `Processor ready ${elapsed(t)}`);
</pre>



<p>Next, we load the processor.</p>



<p>The processor is responsible for converting our multimodal inputs into the format expected by Gemma 4.</p>



<p>Depending on the task, it handles operations such as:</p>



<ul class="wp-block-list">
<li>tokenizing text</li>



<li>preprocessing images</li>



<li>extracting audio features</li>



<li>packaging all modalities into tensors that can be passed directly to the model</li>
</ul>



<p>Calling <code data-enlighter-language="python" class="EnlighterJSRAW">AutoProcessor.from_pretrained()</code> automatically downloads the processor configuration (if it is not already available locally) and initializes the preprocessing pipeline associated with the selected model.</p>



<p>We also record the start time using <code data-enlighter-language="python" class="EnlighterJSRAW">Date.now()</code> so that our helper function can report how long the processor takes to load.</p>



<h3 class="wp-block-heading">Loading the Gemma 4 Model</h3>



<p>After the processor has been initialized, we can load the Gemma 4 model itself.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="67" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="42">// 2. Model
log("2/5", `Loading model (dtype=q4f16, device=cpu) — this may take several minutes on first run ...`);
warn("The ONNX model files are large. Progress bar will appear below as files download/load.");
t = Date.now();

const model = await Gemma4ForConditionalGeneration.from_pretrained(model_id, {
 dtype: "q4f16",
 device: "cpu",
 progress_callback: (info) => {
   if (info.status === "progress_total") {
     renderOverallProgress(info.progress);
   }
   // suppress all other per-file noise
 },
});
success("2/5", `Model loaded ${elapsed(t)}`);
</pre>



<p>Unlike the processor, loading the model can take noticeably longer because <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> may need to download several large ONNX weight files before inference can begin.</p>



<p>Let us look at the most important arguments passed to <code data-enlighter-language="python" class="EnlighterJSRAW">from_pretrained()</code>:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">dtype: "q4f16"</code>: loads a <strong>4-bit quantized</strong> version of the model. Quantization significantly reduces memory usage and improves inference speed while maintaining good generation quality.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">device: "cpu"</code>: instructs <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> to execute the model on the CPU. Depending on your environment, other execution backends may also be available.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">progress_callback</code>: receives updates while the model files are being downloaded and loaded. Instead of displaying numerous progress messages, we pass the overall download percentage to our <code data-enlighter-language="python" class="EnlighterJSRAW">renderOverallProgress()</code> helper, which renders a clean progress bar in the terminal.</li>
</ul>



<p>Once the model has been downloaded, subsequent executions become much faster because <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> reuses the locally cached files.</p>



<h3 class="wp-block-heading">Building the Multimodal Prompt</h3>



<p>With both the processor and model loaded, the next step is defining the conversation that will be sent to Gemma 4.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="83" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="43">// 3. Prompt
log("3/5", "Building chat prompt ...");
const messages = [
 {
   role: "user",
   content: [
     { type: "image" },
     { type: "audio" },
     {
       type: "text",
       text: "Describe this image in detail and transcribe this audio verbatim.",
     },
   ],
 },
];
</pre>



<p>Just like the Python Transformers library, <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> represents conversations as a list of chat messages.</p>



<p>Here, the user message contains <strong>3 different modalities</strong>:</p>



<ul class="wp-block-list">
<li>an image</li>



<li>an audio clip</li>



<li>a text instruction</li>
</ul>



<p>The text prompt asks Gemma 4 to perform 2 tasks simultaneously:</p>



<ul class="wp-block-list">
<li>describe the contents of the image</li>



<li>transcribe the accompanying audio</li>
</ul>



<p>Notice that the image and audio are represented using placeholder objects. The actual image and audio data will be loaded and attached in the next step.</p>



<p>Next, we convert these messages into the format expected by Gemma 4.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="98" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="44">const prompt = processor.apply_chat_template(messages, {
 enable_thinking: false,
 add_generation_prompt: true,
});
success("3/5", "Prompt ready");
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">apply_chat_template()</code> method formats the conversation according to Gemma 4&#8217;s chat template.</p>



<p>Here:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">enable_thinking: false</code>: disables the model&#8217;s thinking mode so that only the final response is generated.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">add_generation_prompt: true</code>: appends the appropriate assistant generation token, indicating where the model should begin generating its response.</li>
</ul>



<p>Using the model&#8217;s built-in chat template ensures that prompts follow the exact conversational format expected during training.</p>



<h3 class="wp-block-heading">Loading and Processing the Inputs</h3>



<p>Next, we load the image and audio that will be provided to the model.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="103" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="45">// 4. Inputs
log("4/5", "Fetching image and audio, then processing inputs ...");
t = Date.now();

const imageUrl = "https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/artemis.jpeg";
const audioUrl = "https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/jfk.wav";
</pre>



<p>For this example, both inputs are hosted remotely and downloaded at runtime.</p>



<p>We first load the image.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="109" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="46">log("4/5", `  Image → ${imageUrl}`);
const image = await load_image(imageUrl);
success("4/5", "  Image loaded");
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">load_image()</code> helper downloads the image and prepares it for the processor.</p>



<p>Next, we load the audio.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="112" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="47">log("4/5", `  Audio → ${audioUrl}`);
const audio = await loadAudioNode(audioUrl);
success("4/5", `  Audio loaded — ${audio.length} samples @ 16kHz`);
</pre>



<p>As discussed earlier, <code data-enlighter-language="python" class="EnlighterJSRAW">loadAudioNode()</code> downloads the WAV file, converts it to <strong>32-bit floating-point samples</strong>, resamples it to <strong>16 kHz</strong>, and returns a <code data-enlighter-language="python" class="EnlighterJSRAW">Float32Array</code> containing the waveform.</p>



<p>With both inputs available, we can prepare the model inputs.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="115" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="48">const inputs = await processor(prompt, image, audio, { add_special_tokens: false });
success("4/5", `Inputs ready ${elapsed(t)}`);
</pre>



<p>The processor combines the formatted prompt, image, and audio into a single set of tensors suitable for Gemma 4.</p>



<p>Notice that all 3 modalities are processed together through a single API call. This is one of the advantages of <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>, where the same processor handles text, images, and audio without requiring separate preprocessing pipelines.</p>



<h3 class="wp-block-heading">Running Inference</h3>



<p>With the inputs prepared, we can finally generate a response.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="117" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="49">// 5. Generate
log("5/5", "Running inference (CPU can be slow — please wait) ...");
warn("Token generation will stream below as it completes:\n");
t = Date.now();

let tokenCount = 0;
const outputs = await model.generate({
 ...inputs,
 max_new_tokens: 512,
 do_sample: false,
 streamer: new TextStreamer(processor.tokenizer, {
   skip_prompt: true,
   skip_special_tokens: false,
   callback_function: (text) => {
     tokenCount++;
     process.stdout.write(text);
   },
 }),
});

process.stdout.write("\n\n");
success("5/5", `Generation done — ${tokenCount} tokens ${elapsed(t)}`);
</pre>



<p>The <code data-enlighter-language="python" class="EnlighterJSRAW">generate()</code> method performs autoregressive text generation using the prepared multimodal inputs.</p>



<p>Some of the most important parameters are:</p>



<ul class="wp-block-list">
<li><code data-enlighter-language="python" class="EnlighterJSRAW">max_new_tokens: 512</code>: limits the maximum length of the generated response.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">do_sample: false</code>: disables random sampling, producing deterministic outputs for identical inputs.</li>



<li><code data-enlighter-language="python" class="EnlighterJSRAW">streamer</code>: enables token streaming so that text appears in the terminal as soon as it is generated.</li>
</ul>



<p>Without a streamer, the program would wait until generation finishes before displaying the complete response. By using <code data-enlighter-language="python" class="EnlighterJSRAW">TextStreamer</code>, users receive immediate feedback as Gemma 4 produces each token, resulting in a much more interactive experience.</p>



<h3 class="wp-block-heading">Decoding the Generated Output</h3>



<p>The output returned by <code data-enlighter-language="python" class="EnlighterJSRAW">generate()</code> consists of token IDs rather than human-readable text.</p>



<p>To convert those tokens back into natural language, we decode the generated sequence.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="js" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="true" data-enlighter-lineoffset="139" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="50">// Final decoded output
const decoded = processor.batch_decode(
 outputs.slice(null, [inputs.input_ids.dims.at(-1), null]),
 { skip_special_tokens: true },
);

console.log(`\n${BOLD}── Final Output ──────────────────────────────────${RESET}`);
console.log(decoded[0]);
console.log(`${BOLD}──────────────────────────────────────────────────${RESET}\n`);
</pre>



<p>Here, we first remove the original prompt tokens, keeping only the newly generated portion of the sequence. We then call <code data-enlighter-language="python" class="EnlighterJSRAW">batch_decode()</code> to transform the token IDs into readable text while removing any special control tokens.</p>



<p>Finally, we print the generated response to the terminal.</p>



<p>At this point, Gemma 4 has successfully processed the text prompt, image, and audio inputs, producing a unified multimodal response, all from a single JavaScript application running locally through <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>.</p>



<h3 class="wp-block-heading">Output</h3>



<p>In your terminal, run <code data-enlighter-language="python" class="EnlighterJSRAW">node index.mjs</code> to run inference using Gemma 4 and <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-63-scaled.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="732" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-63-1024x732.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54742"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-63-1024x732.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-63-1024x732.png?size=126x90&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-63-1024x732.png?size=252x180&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-63-1024x732.png?size=378x270&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-63-1024x732.png?size=504x360&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-63-1024x732.png?size=630x450&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Figure 30:</strong> Running Inference using Gemma 4 and <code>Transformers.js</code> (source: author)</figcaption></figure></div>


<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Choosing-Right-Runtime-Running-Gemma-4-Locally"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Choosing-Right-Runtime-Running-Gemma-4-Locally">Choosing the Right Runtime for Running Gemma 4 Locally</a></h2>



<p>Throughout this lesson, we have explored five different ways to run Gemma 4 locally. While each runtime supports local inference, they target different workflows and use cases. The right choice depends on your goals, preferred development environment, and hardware.</p>



<h3 class="wp-block-heading">Choose Ollama if…</h3>



<p>Use <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> when you want the quickest way to get started with Gemma 4.</p>



<p>It is ideal for developers who:</p>



<ul class="wp-block-list">
<li>want a simple installation process</li>



<li>prefer a command-line interface with minimal configuration</li>



<li>need an OpenAI-compatible API for existing applications</li>



<li>want to experiment with local models in just a few commands</li>
</ul>



<p>For most developers getting started with local LLMs, <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> is the easiest recommendation.</p>



<h3 class="wp-block-heading">Choose llama.cpp if…</h3>



<p>Use <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> when you need maximum control and performance.</p>



<p>It is a good fit if you:</p>



<ul class="wp-block-list">
<li>want to work directly with <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> models</li>



<li>need fine-grained control over inference settings</li>



<li>plan to optimize CPU or GPU performance</li>



<li>want to build custom local inference pipelines</li>
</ul>



<p>Although it requires more setup than <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code>, <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> offers the greatest flexibility.</p>



<h3 class="wp-block-heading">Choose MLX if…</h3>



<p>Use <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code> if you are working on an Apple Silicon Mac.</p>



<p>It is particularly useful when you:</p>



<ul class="wp-block-list">
<li>own an M-series Mac</li>



<li>want native Apple Silicon performance</li>



<li>need efficient multimodal inference</li>



<li>prefer using an OpenAI-compatible local server</li>
</ul>



<p>For Apple users, <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code> typically provides the best performance with the least amount of configuration.</p>



<h3 class="wp-block-heading">Choose LM Studio if…</h3>



<p>Use <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> when you prefer a graphical user interface over the command line.</p>



<p>It is well suited for users who:</p>



<ul class="wp-block-list">
<li>want to download and manage models visually</li>



<li>prefer chatting with models through a desktop application</li>



<li>need a local API server without compiling or configuring runtimes</li>



<li>are new to local AI and want the simplest desktop experience</li>
</ul>



<p><code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> is an excellent choice for beginners or anyone who prefers a GUI-based workflow.</p>



<h3 class="wp-block-heading">Choose Transformers.js if…</h3>



<p>Use <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> when you are building JavaScript applications.</p>



<p>It is the best option if you:</p>



<ul class="wp-block-list">
<li>develop with <code data-enlighter-language="python" class="EnlighterJSRAW">Node.js</code></li>



<li>want to perform inference without Python</li>



<li>need to integrate Gemma 4 directly into a JavaScript project</li>



<li>plan to eventually deploy models inside the browser</li>
</ul>



<p>Since <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> powers both <code data-enlighter-language="python" class="EnlighterJSRAW">Node.js</code> and browser-based inference, it provides a natural path from local development to fully client-side AI applications.</p>



<h3 class="wp-block-heading">At a Glance</h3>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://pyimagesearch.com/wp-content/uploads/2026/07/image-64.png" target="_blank" rel=" noreferrer noopener"><img decoding="async" width="1024" height="352" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-64-1024x352.png?lossy=2&strip=1&webp=1" alt="" class="wp-image-54743"   srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-64-1024x352.png?lossy=2&strip=1&webp=1 1024w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-64-1024x352.png?size=126x43&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-64-1024x352.png?size=252x87&lossy=2&strip=1&webp=1 252w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-64-1024x352.png?size=378x130&lossy=2&strip=1&webp=1 378w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-64-1024x352.png?size=504x173&lossy=2&strip=1&webp=1 504w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2026/07/image-64-1024x352.png?size=630x217&lossy=2&strip=1&webp=1 630w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption"><strong>Table 1:</strong> Recommended Gemma 4 Runtime by Use Case (source: author)</figcaption></figure></div>


<p>Ultimately, there is no single &#8220;best&#8221; runtime. Each excels in different scenarios. If you are just getting started, <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> or <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> are excellent entry points. If you need maximum flexibility, <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> is hard to beat. Apple Silicon users should strongly consider <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code>, while JavaScript developers will likely feel most at home with <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<div id="pitch" style="padding: 40px; width: 100%; background-color: #F4F6FA;">
	<h3>What's next? We recommend <a target="_blank" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend">PyImageSearch University</a>.</h3>

	<script src="https://fast.wistia.com/embed/medias/kno0cmko2z.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_kno0cmko2z videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img decoding="async" src="https://fast.wistia.com/embed/medias/kno0cmko2z/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>

	<div style="margin-top: 32px; margin-bottom: 32px; ">
		<strong>Course information:</strong><br/>
		120+ total classes • 115+ hours of on-demand code walkthrough videos • Last updated: September 2026<br/>
		<span style="color: #169FE6;">★★★★★</span> 4.84 (128 Ratings) • 16,000+ Students Enrolled
	</div>

	<p><strong>I strongly believe that if you had the right teacher you could <em>master</em> computer vision and deep learning.</strong></p>

	<p>Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?</p>

	<p>That’s <em>not</em> the case.</p>

	<p>All you need to master computer vision and deep learning is for someone to explain things to you in <em>simple, intuitive</em> terms. <em>And that’s exactly what I do</em>. My mission is to change education and how complex Artificial Intelligence topics are taught.</p>

	<p>If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to <em>successfully</em> and <em>confidently</em> apply computer vision to your work, research, and projects. Join me in computer vision mastery.</p>

	<p><strong>Inside PyImageSearch University you'll find:</strong></p>

	<ul style="margin-left: 0px;">
		<li style="list-style: none;">&check; <strong>120+ courses</strong> on essential computer vision, deep learning, and OpenCV topics</li>
		<li style="list-style: none;">&check; <strong>94+ Certificates</strong> of Completion</li>
		<li style="list-style: none;">&check; <strong>115+ hours</strong> of on-demand video</li>
		<li style="list-style: none;">&check; <strong>Brand new courses released <em>regularly</em></strong>, ensuring you can keep up with state-of-the-art techniques</li>
		<li style="list-style: none;">&check; <strong>Pre-configured Jupyter Notebooks in Google Colab</strong></li>
		<li style="list-style: none;">&check; Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)</li>
		<li style="list-style: none;">&check; Access to <strong>centralized code repos for <em>all</em> 540+ tutorials</strong> on PyImageSearch</li>
		<li style="list-style: none;">&check; <strong> Easy one-click downloads</strong> for code, datasets, pre-trained models, etc.</li>
		<li style="list-style: none;">&check; <strong>Access</strong> on mobile, laptop, desktop, etc.</li>
	</ul>

	<p style="text-align: center;">
		<a target="_blank" class="button link" href="https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend" style="background-color: #6DC713; border-bottom: none;">Click here to join PyImageSearch University</a>
	</p>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" id="h2-Summary"/>



<h2 class="wp-block-heading"><a href="#TOC-h2-Summary">Summary</a></h2>



<p>In this lesson, we explored 5 ways to run Gemma 4 locally, each designed to support a different development workflow.</p>



<p>We started with <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code>, the quickest way to download models, launch local inference, and expose an OpenAI-compatible API using just a few commands. We then moved to <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code>, where we built the runtime from source, downloaded <code data-enlighter-language="python" class="EnlighterJSRAW">GGUF</code> models, enabled hardware acceleration, and launched both command-line and API-based inference.</p>



<p>Next, we explored <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code>, Apple&#8217;s machine learning framework for Apple Silicon, and used <code data-enlighter-language="python" class="EnlighterJSRAW">MLX-VLM</code> to perform multimodal inference while exposing an OpenAI-compatible server. We then looked at <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code>, which provides a user-friendly graphical interface for downloading, managing, and interacting with Gemma 4 without requiring any command-line configuration.</p>



<p>Finally, we stepped into the JavaScript ecosystem with <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>, demonstrating how to load an ONNX version of Gemma 4, process text, images, and audio, and perform multimodal inference entirely from a <code data-enlighter-language="python" class="EnlighterJSRAW">Node.js</code> application without relying on Python.</p>



<p>As we have seen throughout this lesson, there is no single &#8220;best&#8221; way to run Gemma 4. The ideal runtime depends on your workflow, hardware, and application requirements. <code data-enlighter-language="python" class="EnlighterJSRAW">Ollama</code> offers the fastest path to getting started, <code data-enlighter-language="python" class="EnlighterJSRAW">llama.cpp</code> delivers maximum flexibility and performance, <code data-enlighter-language="python" class="EnlighterJSRAW">MLX</code> is an excellent choice for Apple Silicon devices, <code data-enlighter-language="python" class="EnlighterJSRAW">LM Studio</code> provides an intuitive desktop experience, and <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> enables JavaScript developers to integrate Gemma 4 directly into their applications.</p>



<p>While all of these approaches run Gemma 4 locally, they still execute as traditional desktop or server-side applications. But what if we could eliminate even that requirement and run Gemma 4 entirely inside a web browser?</p>



<p>In the next lesson, we will build on the <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code> foundation introduced here and take the next step by running Gemma 4 directly in the browser. We will learn how to load <code data-enlighter-language="python" class="EnlighterJSRAW">ONNX</code> models with <code data-enlighter-language="python" class="EnlighterJSRAW">Transformers.js</code>, leverage browser technologies such as <code data-enlighter-language="python" class="EnlighterJSRAW">WebGPU</code> and <code data-enlighter-language="python" class="EnlighterJSRAW">WebAssembly</code> for local inference, and build fully client-side multimodal AI applications that require no Python backend or dedicated inference server.</p>



<p>See you in the next lesson!</p>



<h3 class="wp-block-heading">Citation Information</h3>



<p><strong>Thakur, P</strong><strong>. </strong>“Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More,” <em>PyImageSearch</em>, S. Huot, G. Kudriavtsev,and A. Sharma, eds., 2026, <a href="https://pyimg.co/1rpad" target="_blank" rel="noreferrer noopener">https://pyimg.co/1rpad</a> </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="classic" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More" data-enlighter-group="51">@incollection{Thakur_2026_running-gemma-4-locally-ollama-llama-cpp-mlx,
  author = {Piyush Thakur},
  title = {{Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More}},
  booktitle = {PyImageSearch},
  editor = {Susan Huot and Georgii Kudriavtsev and Aditya Sharma},
  year = {2026},
  url = {https://pyimg.co/1rpad},
}
</pre>



<p><strong>To download the source code to this post (and be notified when future tutorials are published here on PyImageSearch), </strong><em><strong>simply enter your email address in the form below!</strong></em></p>



<div id="download-the-code" class="post-cta-wrap">
<div class="gpd-post-cta">
	<div class="gpd-post-cta-content">
		

			<div class="gpd-post-cta-top">
				<div class="gpd-post-cta-top-image"><img decoding="async" src="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1" alt="" srcset="https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?lossy=2&strip=1&webp=1 410w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=126x174&lossy=2&strip=1&webp=1 126w,https://b2633864.assetcdn.net/2633864/wp-content/uploads/2020/01/cta-source-guide-1.png?size=252x348&lossy=2&strip=1&webp=1 252w" sizes="(max-width: 410px) 100vw, 410px" /></div>
				
				<div class="gpd-post-cta-top-title"><h4>Download the Source Code and FREE 17-page Resource Guide</h4></div>
				<div class="gpd-post-cta-top-desc"><p>Enter your email address below to get a .zip of the code and a <strong>FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.</strong> Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!</p></div>


			</div>

			<div class="gpd-post-cta-bottom">
				<form id="footer-cta-code" class="footer-cta" action="https://www.getdrip.com/forms/4130035/submissions" method="post" target="blank" data-drip-embedded-form="4130035">
					<input name="fields[email]" type="email" value="" placeholder="Your email address" class="form-control" />

					<button type="submit">Download the code!</button>

					<div style="display: none;" aria-hidden="true"><label for="website">Website</label><br /><input type="text" id="website" name="website" tabindex="-1" autocomplete="false" value="" /></div>
				</form>
			</div>


		
	</div>

</div>
</div>
<p>The post <a rel="nofollow" href="https://pyimagesearch.com/2026/07/20/running-gemma-4-locally-ollama-llama-cpp-mlx-and-more/">Running Gemma 4 Locally: Ollama, llama.cpp, MLX, and More</a> appeared first on <a rel="nofollow" href="https://pyimagesearch.com">PyImageSearch</a>.</p>
]]></content:encoded>
					
		
		<enclosure url="https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/jfk.wav" length="1015" type="audio/wav" />

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