Train a Fashion CNN with MLflow
Train a FashionMNIST CNN and compare tracked MLflow experiments locally.
Introduction
30 Second Summary
A promising training result can disappear as soon as its terminal window closes. Without a record of the settings behind it, choosing the best result becomes guesswork.
In this project, you will use PyTorch to train a convolutional neural network on FashionMNIST images. You will track controlled experiment runs in MLflow before selecting one model as your deployment candidate.
What You'll Build
You'll watch a FashionMNIST classifier learn while every run becomes a traceable experiment record in your browser.
By the end of this project, you'll have:
- A CNN training loop where you watch loss shift as test accuracy responds.
- A browser dashboard that lets you compare controlled learning-rate runs side by side.
- A deployment handoff where the chosen run preserves its model checkpoint, source file, and dependency file.
- Secret Mission: A controlled stronger-dropout run that tests whether regularization changes the classifier's test accuracy.
Are there any prerequisites?
You should be comfortable with basic Python from building a few machine learning models. You also need a Windows computer with PowerShell and VS Code.
Before We Start
Before any hands-on work, commit to training a FashionMNIST classifier. Comparing controlled runs gives each experiment a clear purpose: selecting a deployment candidate.
Set Up the Training Environment
Training results are difficult to trust when packages come from a global Python installation. A reproducible experiment needs a known runtime plus an isolated dependency boundary.
PowerShell provides that boundary for the FashionMNIST classifier. VS Code keeps the project files together while exact package pins control the runtime.
In this step, get ready to:
- Confirm that your Python version supports the pinned dependencies.
- Create an isolated virtual environment inside the project folder.
- Install the pinned training packages and verify their versions.
Prepare Python and the project folder
The pinned packages support Python versions from 3.10 through 3.14. The 3.14.1 release is excluded because TorchVision does not support it.
- Press the Windows key to open Windows search.
- Type PowerShell into the search field.
- Press Enter to open PowerShell.
- Check your installed Python version by running:
python --version
What does this check?
This command asks the active Python interpreter for its version. The result determines whether the pinned packages can be installed in the next substep.
✔️ I see a supported version
Good start. Your Python release is inside the supported range for this project.
ⓧ I see an unsupported version
Versions below 3.10 cannot use the pinned packages. Versions above 3.14 fall outside this project's supported range.
The 3.14.1 release is also excluded. Install a 3.13 release to continue with a supported runtime.
- Open the official Python releases for Windows page.
- Download a Windows installer for a 3.13 release.
- Run the downloaded installer.
- Enable the installer option that makes Python available in PowerShell.
- Complete the installation.
- Close the current PowerShell window.
- Open a fresh PowerShell window through Windows search.
- Run the version check above again. Continue when it reports a supported version.
ⓧ Command not found
PowerShell cannot find a Python installation. Install a 3.13 release before creating the project environment.
- Open the official Python releases for Windows page.
- Download a Windows installer for a 3.13 release.
- Run the downloaded installer.
- Enable the installer option that makes Python available in PowerShell.
- Complete the installation.
- Close the current PowerShell window.
- Open a fresh PowerShell window through Windows search.
- Run the version check above again. Continue when it reports a supported version.
- Move to your Desktop by running this command:
Set-Location -Path "$HOME\Desktop"
What does this command do?
PowerShell changes its current location to your Desktop. Creating the project here gives the folder a predictable location that is easy to find.
- Create the fashion-cnn-mlops folder and enter it by running these commands:
New-Item -ItemType Directory -Name "fashion-cnn-mlops"
Set-Location -Path "fashion-cnn-mlops"
What did these commands do?
- The first command creates the fashion-cnn-mlops directory on your Desktop.
- The second command makes that directory PowerShell's current location.
- Confirm your current project location by running:
Get-Location
What does this confirm?
PowerShell prints the directory used by future commands. The path should end with Desktop\fashion-cnn-mlops.
You should see a path ending with Desktop\fashion-cnn-mlops. Your project folder is ready to hold the isolated environment.
Is the path incorrect?
Check whether the folder was created on your Desktop. Return to the Desktop command above if the displayed path points somewhere else.
If the folder already existed before this step, enter that existing folder before continuing. Help me find the correct fashion-cnn-mlops folder in PowerShell.
- Open the current project folder in VS Code by running:
code .
What does this command do?
The code command launches VS Code. The period tells it to open PowerShell's current folder.
VS Code opens with fashion-cnn-mlops at the top of the Explorer sidebar. This confirms that the editor and terminal point to the same project folder.
Did VS Code stay closed?
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open VS Code.
- Select File from the top menu.
- Select Open Folder.
- Choose the fashion-cnn-mlops folder on your Desktop.
Help me open my PowerShell project folder in VS Code.
Create the isolated environment
A virtual environment gives this project its own Python packages. The isolation prevents global package versions from changing the experiment.
- Switch back to the PowerShell window inside fashion-cnn-mlops.
- Create the .venv environment by running:
python -m venv .venv
What did Python create?
Python creates the .venv directory inside fashion-cnn-mlops. It contains an isolated interpreter plus a private package directory for this project.
- Return to VS Code.
You should see .venv in the Explorer sidebar. Its appearance confirms that Python created the environment inside the correct folder.
Do you see no .venv folder?
Confirm that PowerShell's path ends with fashion-cnn-mlops. Run the environment creation command again after returning to the correct folder.
Help me diagnose why Python did not create .venv in my project folder.
- Switch back to the same PowerShell window.
- Activate the project environment by running:
.\.venv\Scripts\Activate.ps1
What does activation change?
Activation makes the environment's Python interpreter the default for this PowerShell session. Package installations now stay inside .venv.
Your PowerShell prompt should now begin with (.venv). That's the isolation boundary in place for every package you install next.
Was the activation script blocked?
PowerShell may restrict local scripts on some Windows accounts. Follow the official Python virtual-environment guidance to enable local activation scripts for your user account.
Run the activation command again after updating the policy. Help me activate .venv safely in PowerShell.
The dependency file records the exact package releases used by the training pipeline. Keeping these pins in requirements.txt makes the environment reproducible.
- Return to VS Code.
- Select the Explorer view from the Activity Bar.
- Select the New File button in the Explorer sidebar.
- Enter requirements.txt as the file name.
- Add the pinned dependencies to requirements.txt by pasting this content:
torch==2.14.1
torchvision==0.29.1
mlflow==3.17.2
What do these dependencies provide?
- PyTorch provides the neural-network layers plus the training operations at version 2.14.1.
- TorchVision provides the FashionMNIST dataset integration at version 0.29.1.
- MLflow provides experiment tracking at version 3.17.2.
- Save requirements.txt by pressing Ctrl+S.
- Switch back to the activated PowerShell window.
- Display the saved dependency pins by running:
Get-Content -Path requirements.txt
What does this check prove?
PowerShell reads the saved requirements.txt file from the current project folder. Its output should match the three lines you entered in VS Code.
You should see the three package pins printed in the terminal. This confirms that the dependency file is saved in fashion-cnn-mlops.
Does the file look different?
Confirm that the file is named requirements.txt with no extra extension. Compare every package name plus version against the complete file below.
Help me correct my requirements.txt file.
✔️ Awesome, I've got everything!
Great. Double check that requirements.txt is saved in VS Code.
ⓧ I'd like to double check the full code
Compare your complete requirements.txt file against this reference.
torch==2.14.1
torchvision==0.29.1
mlflow==3.17.2
Install and verify the pinned packages
The activated environment is ready to receive the training dependencies. Installing from requirements.txt keeps the package versions tied to the project.
This installation can take several minutes while the large machine-learning packages download. A quiet terminal during part of that time is normal.
- Install the pinned dependencies into .venv by running:
pip install -r requirements.txt
What does this command do?
The pip command reads each pin from requirements.txt. It installs those releases into the active .venv environment.
The command should finish without an installation error before returning to the (.venv) prompt.
Did the installation fail?
Check that the PowerShell prompt begins with (.venv). Confirm that your Python version is supported before repeating the installation command.
A network interruption can also stop a package download. Help me diagnose the failed dependency installation.
Before you run the final check, do you expect all three imported version strings to match the pins in requirements.txt?
- Verify the installed package versions by running:
python -c "import torch, torchvision, mlflow; print(torch.__version__, torchvision.__version__, mlflow.__version__)"
What should you see?
The first version string should begin with 2.14.1. The second should begin with 0.29.1.
The final version string should begin with 3.17.2. These values confirm that the activated environment contains every pinned dependency.
Your training environment is now working. The isolated interpreter can import every package required by the FashionMNIST pipeline.
Do the versions not match?
Confirm that your prompt still begins with (.venv). Repeat the dependency installation after activating the environment if PowerShell is using another interpreter.
Compare requirements.txt against the full-code tab if only one package has the wrong version. Help me fix mismatched package versions in .venv.
Your pinned environment is ready. Next, you will train the console-only CNN baseline and watch its first evaluation metrics appear.
Train a Console-Only CNN Baseline
Your environment can now run PyTorch. The next proof is a model that learns from real fashion images.
You will build a console-only convolutional neural network baseline. Its first result gives you a concrete experiment to inspect before you decide what evidence the terminal preserves.
In this step, get ready to:
- Prepare FashionMNIST data for training in reusable batches.
- Define the model training and evaluation logic.
- Train the baseline for one CPU epoch and inspect what the console preserves.
Build the data pipeline and CNN
FashionMNIST provides labelled images for training and testing. The data pipeline turns those images into tensors before grouping them into batches.
- Create a file named model.py from the file sidebar in VS Code.
- Add the required imports to model.py by pasting this code:
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
What Do These Imports Provide?
- The torch package supplies tensor operations used throughout the model.
- The nn module supplies the layers and loss function.
- The DataLoader class groups dataset examples into batches.
- The TorchVision imports provide FashionMNIST and its image transform.
- Save model.py.
You should see model.py listed beside requirements.txt in the VS Code file sidebar.
Seeing Unresolved Imports?
Confirm VS Code is using the Python interpreter from the existing .venv environment. The imports depend on the packages you installed there.
Ask for help if the package names remain underlined after you select that interpreter: help me connect VS Code to the .venv that contains PyTorch and TorchVision
- Add the data-loading function below the import group in model.py by pasting this code:
def make_loaders(batch_size):
transform = transforms.ToTensor()
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=transform,
)
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=transform,
)
train_loader = DataLoader(
training_data,
batch_size=batch_size,
shuffle=True,
num_workers=0,
)
test_loader = DataLoader(
test_data,
batch_size=batch_size,
shuffle=False,
num_workers=0,
)
return train_loader, test_loader
What Does the Data Pipeline Do?
- The ToTensor transform converts each image into a tensor that the model can process.
- The two FashionMNIST objects separate the training images from the test images.
- The training loader shuffles examples before creating each batch.
- The test loader keeps a stable order because evaluation does not update the model.
- The num_workers=0 setting loads the data in the main process for a straightforward Windows workflow.
- Save model.py.
The file now contains a complete make_loaders(batch_size) function that returns separate training and test loaders.
Seeing an Indentation Warning?
Keep each dataset argument inside its matching parentheses. Keep the final return line indented once beneath the function definition.
Use this help prompt if the function structure still looks wrong: help me compare the indentation in my make_loaders function against the expected model.py code
The model uses convolution and pooling layers to detect image patterns. Its classifier converts those learned patterns into scores for ten clothing classes.
- Add the FashionCNN class below make_loaders(batch_size) by pasting this code:
class FashionCNN(nn.Module):
def __init__(self, dropout):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
nn.Conv2d(16, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
)
self.classifier = nn.Sequential(
nn.Linear(32 * 7 * 7, 64),
nn.ReLU(),
nn.Dropout(p=dropout),
nn.Linear(64, 10),
)
def forward(self, images):
features = self.features(images)
flattened = torch.flatten(features, start_dim=1)
return self.classifier(flattened)
How Does FashionCNN Process an Image?
- The feature layers use two-dimensional convolution to detect patterns in each image.
- The pooling layers reduce the spatial size of the feature maps.
- The classifier uses dropout during training to randomly suppress some activations.
- The forward() method flattens the learned features before producing ten class scores.
- Save model.py.
Your model file now contains the FashionCNN architecture beneath the data-loading function.
Seeing Mismatched Parentheses?
Check that each nn.Sequential block has its own closing parenthesis. Confirm the forward() method aligns with __init__() inside the class.
Use this prompt for a focused comparison: help me find mismatched parentheses or indentation in my FashionCNN class
Add training and evaluation functions
A mini-batch training loop repeatedly predicts labels before calculating loss. Backpropagation then calculates the gradients used to update the model.
- Add the one-epoch training function below the FashionCNN class in model.py by pasting this code:
def train_one_epoch(dataloader, model, loss_fn, optimizer):
model.train()
total_loss = 0.0
for images, labels in dataloader:
predictions = model(images)
loss = loss_fn(predictions, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
return total_loss / len(dataloader.dataset)
What Happens During Training?
- The model.train() call enables training behavior such as dropout.
- Each batch produces predictions and a loss value.
- The backward pass calculates gradients for the model parameters.
- The optimizer uses those gradients to update the parameters.
- The function returns the average loss across the entire training dataset.
- Save model.py.
The file now contains a complete train_one_epoch() function beneath the model class.
Training Function Looks Misaligned?
Keep the prediction and optimizer lines inside the for loop. Keep the final average-loss calculation outside that loop.
Use this prompt if you need help locating the indentation problem: help me fix the block structure in train_one_epoch
Evaluation measures the trained model without changing its parameters. Evaluation mode also disables dropout so predictions use the complete network.
- Add the evaluation function below train_one_epoch() in model.py by pasting this code:
def evaluate(dataloader, model, loss_fn):
model.eval()
total_loss = 0.0
correct = 0
with torch.no_grad():
for images, labels in dataloader:
predictions = model(images)
total_loss += loss_fn(predictions, labels).item() * images.size(0)
correct += (predictions.argmax(dim=1) == labels).sum().item()
average_loss = total_loss / len(dataloader.dataset)
accuracy = correct / len(dataloader.dataset)
return average_loss, accuracy
What Happens During Evaluation?
- The model.eval() call switches the network to evaluation behavior.
- The torch.no_grad() context disables gradient calculation to reduce evaluation memory use.
- The predicted class is compared with the correct label for every test image.
- The function returns average test loss and test accuracy.
- Save model.py.
Your reusable model module now covers data loading, prediction, training, and evaluation.
Evaluation Function Showing an Error?
Confirm the average-loss and accuracy calculations sit outside the torch.no_grad() block. Confirm the final line returns both values in the same order.
Use this prompt for help with the function structure: help me compare my evaluate function with the expected model.py implementation
✔️ Awesome, I've got everything!
Great. Double check that you saved model.py in VS Code.
ⓧ I'd like to double check the full code
Compare your complete model.py file with this reference.
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
def make_loaders(batch_size):
transform = transforms.ToTensor()
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=transform,
)
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=transform,
)
train_loader = DataLoader(
training_data,
batch_size=batch_size,
shuffle=True,
num_workers=0,
)
test_loader = DataLoader(
test_data,
batch_size=batch_size,
shuffle=False,
num_workers=0,
)
return train_loader, test_loader
class FashionCNN(nn.Module):
def __init__(self, dropout):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
nn.Conv2d(16, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2),
)
self.classifier = nn.Sequential(
nn.Linear(32 * 7 * 7, 64),
nn.ReLU(),
nn.Dropout(p=dropout),
nn.Linear(64, 10),
)
def forward(self, images):
features = self.features(images)
flattened = torch.flatten(features, start_dim=1)
return self.classifier(flattened)
def train_one_epoch(dataloader, model, loss_fn, optimizer):
model.train()
total_loss = 0.0
for images, labels in dataloader:
predictions = model(images)
loss = loss_fn(predictions, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
return total_loss / len(dataloader.dataset)
def evaluate(dataloader, model, loss_fn):
model.eval()
total_loss = 0.0
correct = 0
with torch.no_grad():
for images, labels in dataloader:
predictions = model(images)
total_loss += loss_fn(predictions, labels).item() * images.size(0)
correct += (predictions.argmax(dim=1) == labels).sum().item()
average_loss = total_loss / len(dataloader.dataset)
accuracy = correct / len(dataloader.dataset)
return average_loss, accuracy
Run the console baseline
The baseline script connects the reusable model functions to one fixed experiment. A fixed seed keeps the starting conditions consistent when you run the same configuration again.
- Create a file named baseline.py from the file sidebar in VS Code.
- Add the imports and baseline settings to baseline.py by pasting this code:
import torch
from torch import nn
from model import FashionCNN, evaluate, make_loaders, train_one_epoch
BATCH_SIZE = 64
DROPOUT = 0.2
LEARNING_RATE = 0.01
MOMENTUM = 0.9
SEED = 42
What Defines This Baseline?
- The imports connect the script to the model functions you just created.
- The batch size controls how many images the model processes before each update.
- The dropout and learning-rate values define two configuration choices you can compare later.
- The momentum value influences the optimizer updates.
- The seed fixes the initial random state for a more controlled comparison.
- Save baseline.py.
You should see baseline.py listed beside model.py in the VS Code file sidebar.
Baseline Imports Underlined?
Confirm baseline.py and model.py are in the same fashion-cnn-mlops folder. The import depends on both files sharing that location.
Use this prompt if the model import still fails: help me troubleshoot why baseline.py cannot import functions from model.py
- Add the main training flow below the constants in baseline.py by pasting this code:
def main():
torch.manual_seed(SEED)
train_loader, test_loader = make_loaders(BATCH_SIZE)
model = FashionCNN(DROPOUT)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(
model.parameters(),
lr=LEARNING_RATE,
momentum=MOMENTUM,
)
train_loss = train_one_epoch(train_loader, model, loss_fn, optimizer)
test_loss, test_accuracy = evaluate(test_loader, model, loss_fn)
print(
"epoch=1 "
f"train_loss={train_loss:.4f} "
f"test_loss={test_loss:.4f} "
f"test_accuracy={test_accuracy:.4f}"
)
if __name__ == "__main__":
main()
How Does the Baseline Run?
- The seed establishes the random starting state.
- The script creates the loaders before building the model and optimizer.
- The training function updates the model for one pass through the training data.
- The evaluation function calculates loss and accuracy on the test data.
- The final print statement formats all three metrics on one epoch line.
- Save baseline.py.
The baseline script now connects the data pipeline, model, optimizer, training loop, and evaluation loop.
Seeing a Syntax Warning?
Check the indentation inside main() first. Confirm the final guard begins at the far-left edge of the file.
Use this prompt if the structure still fails validation: help me find a syntax or indentation problem in baseline.py
✔️ Awesome, I've got everything!
Great. Double check that you saved baseline.py in VS Code.
ⓧ I'd like to double check the full code
Compare your complete baseline.py file with this reference.
import torch
from torch import nn
from model import FashionCNN, evaluate, make_loaders, train_one_epoch
BATCH_SIZE = 64
DROPOUT = 0.2
LEARNING_RATE = 0.01
MOMENTUM = 0.9
SEED = 42
def main():
torch.manual_seed(SEED)
train_loader, test_loader = make_loaders(BATCH_SIZE)
model = FashionCNN(DROPOUT)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(
model.parameters(),
lr=LEARNING_RATE,
momentum=MOMENTUM,
)
train_loss = train_one_epoch(train_loader, model, loss_fn, optimizer)
test_loss, test_accuracy = evaluate(test_loader, model, loss_fn)
print(
"epoch=1 "
f"train_loss={train_loss:.4f} "
f"test_loss={test_loss:.4f} "
f"test_accuracy={test_accuracy:.4f}"
)
if __name__ == "__main__":
main()
Before you run the baseline, which parts of the experiment do you think one printed line will preserve?
The first run downloads FashionMNIST before CPU training begins. A quiet pause during training is normal.
- Switch back to the activated PowerShell terminal from the previous step.
- Run the console baseline with this command:
python baseline.py
What Should the Command Produce?
The command downloads FashionMNIST into data before training when the dataset is missing. It then prints one line containing the epoch number and three numeric metrics.
You should see a line containing epoch=1, train_loss, test_loss, and test_accuracy with numeric values. That is the baseline working: your CNN has completed one training epoch and one test evaluation.
Baseline Did Not Finish?
If the script reports a missing name from model.py, compare that file with the full-code tab above. A spelling mismatch in one imported function stops the baseline before training.
If the dataset download fails, check your internet connection before running the same command again.
Use this prompt if the error continues: help me diagnose why python baseline.py fails before printing the epoch metrics
- Identify the experiment evidence that remains outside the terminal output.
What Did the Baseline Leave Behind?
- The terminal does not group the complete configuration with the result.
- The single line does not preserve a metric history across multiple epochs.
- The result does not carry a snapshot of the source code.
- The result does not carry a snapshot of the dependency versions.
- The trained checkpoint is not saved as part of a traceable experiment.
Your CNN now learns from FashionMNIST and reports measurable results. Next, you will use MLflow to group the configuration, metrics, code, dependencies, and model weights into one traceable experiment.
Track the Baseline in MLflow
Your console-only PyTorch baseline proved that the convolutional neural network can learn from FashionMNIST. Its configuration and result still exist as separate pieces.
This step uses MLflow to group the parameters, metric history, source code, dependencies, and checkpoint inside one experiment run. That audit trail makes the baseline easier to inspect later.
In this step, get ready to:
- Start a local MLflow tracking server.
- Create a training script that logs the baseline experiment.
- Run the tracked baseline and inspect its evidence.
Start the local MLflow server
The tracking server receives each run from your training script. It also provides the browser interface where you inspect the recorded evidence.
Why MLflow instead of TensorBoard?
TensorBoard focuses on visualizing events such as scalar histories. MLflow groups parameters, metrics, and deployable artifacts around a named experiment run.
That run-based structure creates a clearer handoff from training to deployment.
- Create a second PowerShell terminal from the terminal panel in VS Code.
- Activate the existing .venv environment in the second terminal by running:
.\.venv\Scripts\Activate.ps1
What does this command do?
The activation script connects this second terminal to the same isolated environment. That gives the terminal access to the installed MLflow command.
- Confirm that the virtual environment name appears at the start of the second terminal prompt.
Environment name missing?
Check that the second terminal is inside the fashion-cnn-mlops folder. The activation path only works from that folder.
If PowerShell blocks the activation script, help me activate the existing virtual environment safely.
The server keeps this terminal occupied while it runs. Leave the process active so the training script can send experiment data to it.
- Start the local tracking server on port 5000 by running:
mlflow server --port 5000
What does this command do?
The command starts the MLflow tracking server on the selected port. The server exposes its interface at http://127.0.0.1:5000.
This process stores local experiment metadata in mlflow.db. It stores run artifacts under mlartifacts.
- Confirm that the server output reports http://127.0.0.1:5000.
- Keep the second PowerShell terminal running.
- Switch back to the first activated PowerShell terminal from earlier.
Your tracking service is live. The next run now has a permanent destination for its experiment evidence.
Server not starting?
Confirm that the second terminal still uses the activated .venv environment. MLflow was installed inside that environment.
If the terminal reports a port problem, stop any earlier local MLflow server before trying again.
For environment-specific help, help me diagnose why my local MLflow server does not start.
Build the tracked training script
The new script keeps the existing model functions. It adds command-line settings plus explicit calls that record the experiment contract.
- Use the file sidebar in VS Code to create train.py inside the fashion-cnn-mlops folder.
- Add the imports and tracking constants by copying this code into train.py.
import argparse
from pathlib import Path
import mlflow
import torch
from torch import nn
from model import FashionCNN, evaluate, make_loaders, train_one_epoch
TRACKING_URI = "http://127.0.0.1:5000"
EXPERIMENT_NAME = "fashion-cnn"
MOMENTUM = 0.9
SEED = 42
What does this code do?
- The imports bring in argument parsing, checkpoint paths, experiment tracking, and the existing training helpers.
- TRACKING_URI points every logged run at your local server.
- EXPERIMENT_NAME groups the runs under fashion-cnn.
- MOMENTUM and SEED preserve the baseline training conditions.
- Save train.py.
- Confirm that the file ends with SEED = 42.
Imports showing errors?
Confirm that train.py sits beside model.py. The local import depends on both files sharing the same folder.
If VS Code cannot resolve the installed packages, help me connect VS Code to my existing virtual environment.
Command-line arguments turn each experiment setting into an explicit run input. This makes controlled comparisons possible without editing the script between runs.
- Add parse_args() below the constants by copying:
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--learning-rate", type=float, default=0.01)
parser.add_argument("--dropout", type=float, default=0.2)
parser.add_argument("--epochs", type=int, default=1)
parser.add_argument("--batch-size", type=int, default=64)
parser.add_argument("--run-name", default="tracked-run")
return parser.parse_args()
What does this code do?
- The parser accepts learning rate, dropout, epoch count, batch size, and run name from PowerShell.
- Each default matches the original baseline unless the command supplies a different value.
- The returned arguments become the configuration for one experiment run.
- Save train.py.
- Confirm that parse_args() contains five argument definitions.
Parser marked as invalid?
Check the indentation beneath def parse_args():. Every parser line needs four leading spaces.
For help comparing the function, help me find the syntax issue in parse_args().
The first part of main() recreates the baseline model from the selected arguments. It opens an MLflow run before recording the complete configuration.
- Add the first part of main() below parse_args() by copying:
def main():
args = parse_args()
torch.manual_seed(SEED)
mlflow.set_tracking_uri(TRACKING_URI)
mlflow.set_experiment(EXPERIMENT_NAME)
train_loader, test_loader = make_loaders(args.batch_size)
model = FashionCNN(args.dropout)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(
model.parameters(),
lr=args.learning_rate,
momentum=MOMENTUM,
)
with mlflow.start_run(run_name=args.run_name):
mlflow.log_params(
{
"batch_size": args.batch_size,
"dropout": args.dropout,
"epochs": args.epochs,
"learning_rate": args.learning_rate,
"model_class": "FashionCNN",
"momentum": MOMENTUM,
"optimizer": "SGD",
"seed": SEED,
"torch_version": torch.__version__,
}
)
What does this code do?
- The setup applies the fixed seed before creating the data loaders and model.
- The tracking calls connect to the local server and select the fashion-cnn experiment.
- The optimizer uses the argument-driven learning rate with the fixed momentum.
- The active run records every setting needed to explain how the model was trained.
- Save train.py.
- Confirm that the parameter dictionary ends with the recorded PyTorch version.
Main function showing indentation errors?
Keep the parameter dictionary inside mlflow.log_params(). Keep that call inside the active run block.
If the nested indentation is unclear, help me fix the first part of main().
Metrics gain a history when each epoch is logged with its step number. This links the console result to the exact point in training that produced it.
- Add the epoch loop directly below mlflow.log_params() by copying:
for epoch in range(1, args.epochs + 1):
train_loss = train_one_epoch(
train_loader,
model,
loss_fn,
optimizer,
)
test_loss, test_accuracy = evaluate(test_loader, model, loss_fn)
mlflow.log_metrics(
{
"train_loss": train_loss,
"test_loss": test_loss,
"test_accuracy": test_accuracy,
},
step=epoch,
)
print(
f"epoch={epoch} "
f"train_loss={train_loss:.4f} "
f"test_loss={test_loss:.4f} "
f"test_accuracy={test_accuracy:.4f}"
)
What does this code do?
- The loop trains the model before evaluating it once per epoch.
- The metric logging call records train loss, test loss, and test accuracy at the current epoch step.
- The print call preserves the immediate console feedback from the baseline script.
- Save train.py.
- Confirm that the loop ends with the formatted test_accuracy value.
Metric loop showing an error?
Keep the loop at eight spaces so it remains inside the active MLflow run. Keep the training statements at twelve spaces so they remain inside the loop.
For help with the nested block, help me fix the metric loop indentation.
A run becomes useful for deployment when it includes the learned weights plus the code that produced them. The final block logs those files before removing the temporary local checkpoint.
- Add the artifact logging and script entry point below the epoch loop by copying:
checkpoint_path = Path("fashion_cnn.pt")
torch.save(model.state_dict(), checkpoint_path)
mlflow.log_artifact(str(checkpoint_path), artifact_path="model")
mlflow.log_artifact("model.py", artifact_path="code")
mlflow.log_artifact("requirements.txt", artifact_path="code")
checkpoint_path.unlink(missing_ok=True)
print("Tracked run complete. Open http://127.0.0.1:5000 to inspect it.")
if __name__ == "__main__":
main()
What does this code do?
- The checkpoint stores the model state inside fashion_cnn.pt.
- The artifact calls place the checkpoint under model.
- The source file and dependency file are stored under code.
- The cleanup removes the temporary checkpoint after MLflow has copied it into the run.
- Save train.py.
- Confirm that the file ends with the call to main().
Artifact block showing an error?
Keep the checkpoint statements inside the active run block. Keep the final completion message outside that block.
If VS Code marks the final lines as invalid, help me fix the end of train.py.
✔️ Awesome, I've got everything!
Great. Double check that you saved train.py in VS Code.
ⓧ I'd like to double check the full code
Compare your complete train.py file with this reference.
import argparse
from pathlib import Path
import mlflow
import torch
from torch import nn
from model import FashionCNN, evaluate, make_loaders, train_one_epoch
TRACKING_URI = "http://127.0.0.1:5000"
EXPERIMENT_NAME = "fashion-cnn"
MOMENTUM = 0.9
SEED = 42
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--learning-rate", type=float, default=0.01)
parser.add_argument("--dropout", type=float, default=0.2)
parser.add_argument("--epochs", type=int, default=1)
parser.add_argument("--batch-size", type=int, default=64)
parser.add_argument("--run-name", default="tracked-run")
return parser.parse_args()
def main():
args = parse_args()
torch.manual_seed(SEED)
mlflow.set_tracking_uri(TRACKING_URI)
mlflow.set_experiment(EXPERIMENT_NAME)
train_loader, test_loader = make_loaders(args.batch_size)
model = FashionCNN(args.dropout)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(
model.parameters(),
lr=args.learning_rate,
momentum=MOMENTUM,
)
with mlflow.start_run(run_name=args.run_name):
mlflow.log_params(
{
"batch_size": args.batch_size,
"dropout": args.dropout,
"epochs": args.epochs,
"learning_rate": args.learning_rate,
"model_class": "FashionCNN",
"momentum": MOMENTUM,
"optimizer": "SGD",
"seed": SEED,
"torch_version": torch.__version__,
}
)
for epoch in range(1, args.epochs + 1):
train_loss = train_one_epoch(
train_loader,
model,
loss_fn,
optimizer,
)
test_loss, test_accuracy = evaluate(test_loader, model, loss_fn)
mlflow.log_metrics(
{
"train_loss": train_loss,
"test_loss": test_loss,
"test_accuracy": test_accuracy,
},
step=epoch,
)
print(
f"epoch={epoch} "
f"train_loss={train_loss:.4f} "
f"test_loss={test_loss:.4f} "
f"test_accuracy={test_accuracy:.4f}"
)
checkpoint_path = Path("fashion_cnn.pt")
torch.save(model.state_dict(), checkpoint_path)
mlflow.log_artifact(str(checkpoint_path), artifact_path="model")
mlflow.log_artifact("model.py", artifact_path="code")
mlflow.log_artifact("requirements.txt", artifact_path="code")
checkpoint_path.unlink(missing_ok=True)
print("Tracked run complete. Open http://127.0.0.1:5000 to inspect it.")
if __name__ == "__main__":
main()
Run and inspect the tracked baseline
The script is ready to send one complete baseline run to the local server. The command supplies the baseline learning rate plus dropout value with a descriptive run name.
Before you run it, do you expect the new result to exist only in PowerShell or also appear as a grouped experiment run?
- Run the tracked baseline from the first activated PowerShell terminal by entering:
python train.py --learning-rate 0.01 --dropout 0.2 --run-name tracked-baseline
What does this command do?
- The learning rate stays at 0.01.
- The dropout probability stays at 0.2.
- The run receives the name tracked-baseline.
- The remaining settings keep their baseline defaults.
The terminal may stay quiet while the CPU completes the epoch. Keep it running until the metric line and completion message appear.
You will see numeric train_loss, test_loss, and test_accuracy values. The final message points you to the local MLflow interface.
Tracked run failing?
Confirm that the second PowerShell terminal still runs the MLflow server. The training script needs that server at http://127.0.0.1:5000.
Check train.py against the full-code reference if the terminal reports a Python syntax problem.
For targeted help, help me diagnose why the tracked baseline failed.
- Enter http://127.0.0.1:5000 in your browser address bar.
- Select the fashion-cnn experiment.
- Select the tracked-baseline run.
- Confirm that the run records learning_rate=0.01.
- Confirm that the run records dropout=0.2.
- Inspect the histories for train_loss, test_loss, and test_accuracy.
- Inspect the run artifacts for model/fashion_cnn.pt.
- Inspect the run artifacts for code/model.py and code/requirements.txt.
- Return to the VS Code file sidebar.
- Confirm that no temporary fashion_cnn.pt file remains beside train.py.
That is the missing audit trail solved. Your baseline now carries its settings, metric history, code, dependencies, and learned weights as one traceable run.
Run missing from the interface?
Refresh the browser after the terminal prints the completion message. Confirm that you selected the fashion-cnn experiment.
If the run appears without artifacts, confirm that model.py and requirements.txt remain beside train.py.
For help locating the evidence, help me inspect my tracked-baseline run in MLflow.
Your baseline is now reproducible and auditable. Next up, you will change one training setting and compare the two runs using tracked evidence.
Compare Runs and Select a Candidate
Your first MLflow run now keeps its configuration, metrics, code, dependencies, and checkpoint together. A single tracked result still cannot show whether a lower learning rate improves test performance.
This step creates a controlled second run for comparison. The seed, batch size, epoch count, model architecture, and dropout stay fixed.
In this step, get ready to:
- Train a second run with a lower learning rate.
- Compare both runs through their parameters and metric histories.
- Select the run with the higher final test accuracy as your deployment candidate.
Change one training variable
A controlled experiment changes one hyperparameter while holding the rest of the training setup constant. Here, the learning rate is the only changed input.
Expect this CPU run to take several minutes on many Windows laptops. Faster or slower completion is normal.
Before you run it, do you expect the lower learning rate to produce a higher or lower test accuracy?
- Start the controlled second run from your activated PowerShell terminal by running this command:
python train.py --learning-rate 0.005 --dropout 0.2 --run-name lower-learning-rate
What Does This Command Control?
- The --learning-rate 0.005 argument changes the optimizer's learning rate.
- The --dropout 0.2 argument keeps dropout equal to the first run.
- The default arguments keep the epoch count at 1.
- The default arguments keep the batch size at 64.
- The --run-name lower-learning-rate argument gives the new run a distinct name in MLflow.
You should see one epoch line containing numeric train_loss, test_loss, and test_accuracy values. The final message confirms that the tracked run completed.
- Switch back to the browser tab showing http://127.0.0.1:5000.
- Refresh the fashion-cnn experiment page.
You should see lower-learning-rate beside tracked-baseline. Both runs should show a completed state.
Good work. You now have two tracked results that differ by one training choice.
Missing the Second Run?
- Confirm that the PowerShell terminal running the MLflow server is still active.
- Check that the training terminal reached its completion message before refreshing the browser.
- Return to the fashion-cnn experiment if MLflow opened a different experiment.
Help me find a completed MLflow run that is missing from my fashion-cnn experiment.
Compare the tracked evidence
The parameter table shows whether the experiment stayed controlled. The metric histories show how each configuration performed during training and evaluation.
- Select the checkbox beside tracked-baseline in the fashion-cnn experiment.
- Select the checkbox beside lower-learning-rate.
- Open the comparison view for the selected runs.
- Compare the learning_rate parameter across both runs.
- Confirm that dropout remains 0.2 in both runs.
- Confirm that batch_size, epochs, model_class, optimizer, momentum, and seed match across both runs.
- Compare the train_loss histories across both runs.
- Compare the test_loss and test_accuracy histories across both runs.
You should see 0.01 for the first learning rate. You should see 0.005 for the second learning rate.
Why Keep the Other Settings Fixed?
Changing only the learning rate gives the comparison a clear question. A difference in the tracked metrics can now support a decision about that one configuration choice.
Each metric history contains one recorded training step because both runs use one epoch. The final point is the value you use for this deployment decision.
Build the deployment handoff
A deployment candidate needs evidence that another person can inspect. Its run record should connect the final metric to the exact configuration and saved artifacts.
Before you compare the final values, which run do you expect to have the higher test_accuracy?
- Find the final test_accuracy value for tracked-baseline.
- Find the final test_accuracy value for lower-learning-rate.
- Identify the run with the higher final test_accuracy as your deployment candidate.
Your chosen run now has the stronger final test result in this controlled comparison. Its tracked record supplies the evidence for your handoff decision.
- Open the chosen run from the comparison view.
- Confirm its learning_rate parameter.
- Confirm its dropout parameter.
- Confirm its final test_accuracy metric.
- Inspect the saved checkpoint at model/fashion_cnn.pt.
- Inspect the tracked source file at code/model.py.
- Inspect the tracked dependency file at code/requirements.txt.
You should see a checkpoint, source file, and dependency file attached to the chosen run. These artifacts make the candidate traceable beyond its final metric.
That's the decision made. Your deployment candidate is backed by a controlled comparison and a complete set of tracked artifacts.
Secret mission
Test a Regularization Hypothesis
Raise dropout from 0.2 to 0.5 in a third tracked training run. Compare the evidence from all three runs to defend your final deployment candidate.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your Windows computer at no charge. Decide whether to keep the resources active, pause the local server, or delete the generated data.
Resources you used:
- The MLflow server process running at http://127.0.0.1:5000.
- The .venv virtual environment containing the pinned Python packages.
- The data folder containing the downloaded FashionMNIST dataset.
- The mlflow.db database containing the experiment metadata and metric history.
- The mlartifacts folder containing the tracked run files.
Keep everything running
No action is needed. Choose this option if you want to continue comparing the three runs or inspecting their artifacts.
- Leave the MLflow server terminal running.
- Return to http://127.0.0.1:5000 whenever you want to inspect the fashion-cnn experiment.
- Keep the entire fashion-cnn-mlops folder on your computer.
Pause - I'll come back to this later
Stop the running server to free its memory. Your experiment history stays on disk for your next session.
- Switch back to the PowerShell terminal running the MLflow server.
- Press Ctrl+C to stop the server.
The server stops accepting requests. Every file in fashion-cnn-mlops remains available.
Delete - I don't want to use this again
Deleting these generated resources is permanent. Your source files remain in fashion-cnn-mlops for a future rebuild.
- Switch back to the PowerShell terminal running the MLflow server.
- Press Ctrl+C to stop the server.
- Return to the activated PowerShell terminal inside fashion-cnn-mlops.
- Delete the generated resources by running these commands:
Remove-Item -Recurse -Force .venv, data, mlartifacts -ErrorAction SilentlyContinue
Remove-Item -Force mlflow.db -ErrorAction SilentlyContinue
What Do These Commands Remove?
- The .venv target removes the isolated environment and its installed packages.
- The data target removes the downloaded FashionMNIST dataset.
- The mlartifacts target removes the tracked checkpoints and copied files.
- The mlflow.db target removes the experiment metadata and metric history.
The -ErrorAction SilentlyContinue option allows cleanup to continue when a target is already absent.
- Return to the VS Code window from earlier.
- Confirm that .venv, data, mlartifacts, and mlflow.db no longer appear in the project file list.
Cleanup is complete. Your source files remain ready for another experiment.
Generated Resources Still Present?
- Confirm that the MLflow server process has stopped.
- Confirm that your PowerShell terminal is inside fashion-cnn-mlops.
- Close any process using the generated files.
- Run the cleanup commands again.
Help me troubleshoot generated resources that remain after cleanup.
Nice Work!
Nice Work!
Congratulations! You built a local PyTorch image-classification pipeline that trains on FashionMNIST. Its MLflow records now support a deployment decision with traceable evidence.
You learned how to:
- Built a convolutional neural network for FashionMNIST. Applied mini-batch training to process image batches. Used backpropagation to update model weights. Switched to evaluation mode to measure test loss plus test accuracy.
- Experienced why terminal-only training cannot support repeatable experiments. Tracked hyperparameters inside an MLflow experiment. Preserved each run's metric history. Attached a reproducibility bundle containing checkpoint weights plus source and dependency files.
- Performed controlled run comparisons with consistent training settings. Changed only the learning rate between the first two tracked runs. Used final test accuracy to identify a traceable deployment candidate.
- Secret Mission: Predicted how stronger dropout could affect generalization. Tested the hypothesis with a third tracked run. Defended the final deployment candidate using evidence from all three runs.
Ready to quiz yourself?